nonebot-plugin-github-release 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ """状态与订阅的持久化(JSON,原子写入)。
2
+
3
+ - `state.json`:每个仓库的推送基线
4
+ - `subscriptions.json`:哪个群订阅了哪个仓库
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import asdict, dataclass, field
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Set
15
+
16
+ from .config import Config
17
+
18
+
19
+ def resolve_data_dir(cfg: Config) -> Path:
20
+ """插件数据目录:优先用 nonebot-plugin-localstore,退化为 data/<插件名>。"""
21
+ if cfg.gh_watch_data_dir:
22
+ return Path(cfg.gh_watch_data_dir).expanduser()
23
+ try:
24
+ import nonebot_plugin_localstore as store
25
+
26
+ return store.get_plugin_data_dir()
27
+ except Exception: # noqa: BLE001 - 没装 localstore 或未初始化时退化
28
+ return Path("data") / "github_release"
29
+
30
+
31
+ def state_path(data_dir: Path) -> Path:
32
+ return data_dir / "state.json"
33
+
34
+
35
+ def subscriptions_path(data_dir: Path) -> Path:
36
+ return data_dir / "subscriptions.json"
37
+
38
+
39
+ def now_iso() -> str:
40
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
41
+
42
+
43
+ @dataclass
44
+ class RepoState:
45
+ """单个仓库的监控状态。"""
46
+
47
+ initialized: bool = False
48
+ pushed: List[str] = field(default_factory=list)
49
+ latest: Optional[str] = None
50
+ last_check: Optional[str] = None
51
+ last_ok: Optional[str] = None
52
+ last_error: Optional[str] = None
53
+ last_source: Optional[str] = None
54
+
55
+ def copy(self) -> "RepoState":
56
+ return RepoState(
57
+ initialized=self.initialized,
58
+ pushed=list(self.pushed),
59
+ latest=self.latest,
60
+ last_check=self.last_check,
61
+ last_ok=self.last_ok,
62
+ last_error=self.last_error,
63
+ last_source=self.last_source,
64
+ )
65
+
66
+ @classmethod
67
+ def from_dict(cls, raw: Any) -> "RepoState":
68
+ if not isinstance(raw, dict):
69
+ return cls()
70
+ pushed = raw.get("pushed")
71
+ return cls(
72
+ initialized=bool(raw.get("initialized")),
73
+ pushed=[str(tag) for tag in pushed if isinstance(tag, str)]
74
+ if isinstance(pushed, list)
75
+ else [],
76
+ latest=raw.get("latest") or None,
77
+ last_check=raw.get("last_check") or None,
78
+ last_ok=raw.get("last_ok") or None,
79
+ last_error=raw.get("last_error") or None,
80
+ last_source=raw.get("last_source") or None,
81
+ )
82
+
83
+
84
+ def _atomic_write_json(path: Path, payload: Any) -> None:
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ tmp = path.with_name(path.name + ".tmp")
87
+ tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
88
+ os.replace(tmp, path)
89
+
90
+
91
+ def _load_json(path: Path) -> Any:
92
+ if not path.exists():
93
+ return None
94
+ try:
95
+ return json.loads(path.read_text(encoding="utf-8"))
96
+ except (OSError, ValueError):
97
+ return None
98
+
99
+
100
+ def load_states(path: Path) -> Dict[str, RepoState]:
101
+ """读取所有仓库状态;文件缺失或损坏时返回空表。"""
102
+ raw = _load_json(path)
103
+ if not isinstance(raw, dict):
104
+ return {}
105
+ repos = raw.get("repos")
106
+ source = repos if isinstance(repos, dict) else raw
107
+ return {
108
+ str(name): RepoState.from_dict(value)
109
+ for name, value in source.items()
110
+ if isinstance(name, str)
111
+ }
112
+
113
+
114
+ def save_states(path: Path, states: Dict[str, RepoState]) -> None:
115
+ payload = {"repos": {name: asdict(state) for name, state in sorted(states.items())}}
116
+ for value in payload["repos"].values():
117
+ value["pushed"] = sorted(set(value["pushed"]))
118
+ _atomic_write_json(path, payload)
119
+
120
+
121
+ def load_subscriptions(path: Path) -> Dict[str, Set[int]]:
122
+ """读取「仓库 → 订阅群」映射。"""
123
+ raw = _load_json(path)
124
+ if not isinstance(raw, dict):
125
+ return {}
126
+ result: Dict[str, Set[int]] = {}
127
+ for repo, groups in raw.items():
128
+ if not isinstance(repo, str) or not isinstance(groups, list):
129
+ continue
130
+ parsed: Set[int] = set()
131
+ for item in groups:
132
+ try:
133
+ parsed.add(int(item))
134
+ except (TypeError, ValueError):
135
+ continue
136
+ if parsed:
137
+ result[repo] = parsed
138
+ return result
139
+
140
+
141
+ def save_subscriptions(path: Path, subscriptions: Dict[str, Set[int]]) -> None:
142
+ payload = {
143
+ repo: sorted(groups)
144
+ for repo, groups in sorted(subscriptions.items())
145
+ if groups
146
+ }
147
+ _atomic_write_json(path, payload)
@@ -0,0 +1,52 @@
1
+ """版本号解析与「正式版」判定。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import NamedTuple, Optional
7
+
8
+ # 兼容 `v0.1.5`、`dsh-v0.1.5`、`dsh-v0.1.5-rc.1` 这几种 tag 写法
9
+ _TAG_RE = re.compile(
10
+ r"^(?:[^0-9]*?)v?(\d+)\.(\d+)\.(\d+)"
11
+ r"(?:-([0-9A-Za-z.\-]+))?"
12
+ r"(?:\+[0-9A-Za-z.\-]+)?$"
13
+ )
14
+
15
+
16
+ class Version(NamedTuple):
17
+ """语义化版本。"""
18
+
19
+ major: int
20
+ minor: int
21
+ patch: int
22
+ prerelease: Optional[str]
23
+
24
+ @property
25
+ def is_stable(self) -> bool:
26
+ """没有预发布后缀才算正式版。"""
27
+ return self.prerelease is None
28
+
29
+ @property
30
+ def core(self) -> str:
31
+ return f"{self.major}.{self.minor}.{self.patch}"
32
+
33
+
34
+ def parse_version(tag: str) -> Optional[Version]:
35
+ """解析 tag 中的版本号,解析不出来返回 None。"""
36
+ match = _TAG_RE.match((tag or "").strip())
37
+ if match is None:
38
+ return None
39
+ major, minor, patch, prerelease = match.groups()
40
+ return Version(int(major), int(minor), int(patch), prerelease)
41
+
42
+
43
+ def is_stable_release(tag: str, *, prerelease_flag: bool = False) -> bool:
44
+ """判断是否为正式版。
45
+
46
+ 两个条件同时满足才算正式版:GitHub 没有标记为 prerelease,且 tag 中没有
47
+ 预发布后缀。解析不出版本号的 tag(例如 `nightly`)一律不算,避免误报。
48
+ """
49
+ if prerelease_flag:
50
+ return False
51
+ version = parse_version(tag)
52
+ return bool(version and version.is_stable)
@@ -0,0 +1,198 @@
1
+ """定时检查 + 推送。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Dict, Iterable, List, Optional, Set, Tuple
8
+
9
+ import nonebot
10
+ from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot
11
+ from nonebot.adapters.onebot.v11 import Message, MessageSegment
12
+ from nonebot.log import logger
13
+
14
+ from .card import render_card
15
+ from .config import Config, load_config
16
+ from .models import Release
17
+ from .plan import plan, select_releases
18
+ from .render import render_release
19
+ from .source import ReleaseFetchError, fetch_releases
20
+ from .store import (
21
+ RepoState,
22
+ load_states,
23
+ load_subscriptions,
24
+ now_iso,
25
+ resolve_data_dir,
26
+ save_states,
27
+ state_path,
28
+ subscriptions_path,
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class CheckResult:
34
+ """一次检查的结果。"""
35
+
36
+ status: str
37
+ detail: str
38
+ repo_count: int = 0
39
+ pushed: List[str] = field(default_factory=list)
40
+ failed: List[str] = field(default_factory=list)
41
+ errors: List[str] = field(default_factory=list)
42
+
43
+
44
+ def effective_subscriptions(
45
+ cfg: Config, stored: Dict[str, Set[int]]
46
+ ) -> Dict[str, Set[int]]:
47
+ """配置里的默认订阅 + 群内动态订阅。
48
+
49
+ `GH_WATCH_REPOS × GH_WATCH_GROUPS` 的组合等价于「这些群都订阅这些仓库」。
50
+ """
51
+ result: Dict[str, Set[int]] = {repo: set(groups) for repo, groups in stored.items()}
52
+ default_groups = set(cfg.gh_watch_groups)
53
+ for repo in cfg.gh_watch_repos:
54
+ result.setdefault(repo, set()).update(default_groups)
55
+ return {repo: groups for repo, groups in result.items() if groups}
56
+
57
+
58
+ async def build_announce_message(release: Release, repo: str, cfg: Config) -> "str | Message":
59
+ """构造要发送的消息:优先卡片图,失败回退纯文本。"""
60
+ if cfg.gh_watch_send_image:
61
+ png = await render_card(
62
+ release,
63
+ repo=repo,
64
+ max_chars=cfg.gh_watch_card_max_chars,
65
+ show_reactions=cfg.gh_watch_card_show_reactions,
66
+ cut_foreign=cfg.gh_watch_body_language != "all",
67
+ width=cfg.gh_watch_card_width,
68
+ body_max_height=cfg.gh_watch_card_max_height,
69
+ )
70
+ if png is not None:
71
+ stage = "预发布" if release.prerelease else "正式版"
72
+ # 图片里的链接点不了,附一行文字方便复制
73
+ caption = f"【{repo} {stage}】{release.title}\n完整公告:{release.url}"
74
+ return Message(MessageSegment.image(png)) + f"\n{caption}"
75
+
76
+ return render_release(
77
+ release,
78
+ repo=repo,
79
+ tz_name=cfg.gh_watch_timezone,
80
+ max_chars=cfg.gh_watch_max_chars,
81
+ strip_mentions=cfg.gh_watch_strip_mentions,
82
+ cut_foreign=cfg.gh_watch_body_language != "all",
83
+ )
84
+
85
+
86
+ async def push_to_groups(
87
+ message: "str | Message", groups: Iterable[int]
88
+ ) -> Tuple[List[int], List[str]]:
89
+ """推送消息,返回 (成功的群, 错误信息)。"""
90
+ try:
91
+ bots = [
92
+ bot for bot in nonebot.get_bots().values() if isinstance(bot, OneBotV11Bot)
93
+ ]
94
+ except Exception as exc: # pragma: no cover - 依赖运行时状态
95
+ return [], [f"获取机器人实例失败:{exc.__class__.__name__}: {exc}"]
96
+
97
+ if not bots:
98
+ return [], ["当前没有已连接的 OneBot V11 机器人"]
99
+
100
+ succeeded: List[int] = []
101
+ errors: List[str] = []
102
+ for group_id in groups:
103
+ for bot in bots:
104
+ try:
105
+ await bot.send_group_msg(group_id=int(group_id), message=message)
106
+ except Exception as exc: # noqa: BLE001 - 发送失败原因很多,统一记录
107
+ errors.append(
108
+ f"群 {group_id} 推送失败(bot {bot.self_id}):"
109
+ f"{exc.__class__.__name__}: {exc}"
110
+ )
111
+ continue
112
+ succeeded.append(int(group_id))
113
+ break
114
+ return succeeded, errors
115
+
116
+
117
+ async def check_once(cfg: Optional[Config] = None) -> CheckResult:
118
+ """遍历所有订阅的仓库,把新 release 推到对应的群。"""
119
+ cfg = cfg or load_config()
120
+ data_dir = resolve_data_dir(cfg)
121
+ states_file = state_path(data_dir)
122
+ states = load_states(states_file)
123
+ subscriptions = effective_subscriptions(cfg, load_subscriptions(subscriptions_path(data_dir)))
124
+
125
+ if not subscriptions:
126
+ return CheckResult(
127
+ status="no-repo",
128
+ detail="没有任何订阅:在群里发 /gh订阅 owner/repo,或配置 GH_WATCH_REPOS",
129
+ )
130
+
131
+ pushed: List[str] = []
132
+ failed: List[str] = []
133
+ errors: List[str] = []
134
+ limit = max(1, cfg.gh_watch_max_per_round)
135
+
136
+ for repo, groups in sorted(subscriptions.items()):
137
+ state = states.get(repo) or RepoState()
138
+ try:
139
+ fetched = await fetch_releases(cfg, repo)
140
+ except ReleaseFetchError as exc:
141
+ state.last_check = now_iso()
142
+ state.last_error = str(exc)
143
+ states[repo] = state
144
+ save_states(states_file, states)
145
+ errors.append(f"{repo}:{exc}")
146
+ logger.warning(f"[gh-release] {repo} 拉取失败:{exc}")
147
+ continue
148
+
149
+ announcements = select_releases(
150
+ fetched.releases,
151
+ include_prerelease=cfg.gh_watch_include_prerelease,
152
+ require_body=cfg.gh_watch_require_body,
153
+ )
154
+ state, pending = plan(state, announcements)
155
+ state.last_check = now_iso()
156
+ state.last_ok = now_iso()
157
+ state.last_error = None
158
+ state.last_source = fetched.source
159
+ if announcements:
160
+ state.latest = announcements[-1].tag
161
+ states[repo] = state
162
+ save_states(states_file, states)
163
+
164
+ for release in pending[:limit]:
165
+ message = await build_announce_message(release, repo, cfg)
166
+ succeeded, send_errors = await push_to_groups(message, groups)
167
+ errors.extend(send_errors)
168
+ for error in send_errors:
169
+ logger.warning(f"[gh-release] {error}")
170
+ if succeeded:
171
+ state.pushed = sorted(set(state.pushed) | {release.tag})
172
+ states[repo] = state
173
+ save_states(states_file, states)
174
+ pushed.append(f"{repo} {release.tag}")
175
+ logger.success(f"[gh-release] 已推送 {repo} {release.tag} 到 {len(succeeded)} 个群")
176
+ else:
177
+ failed.append(f"{repo} {release.tag}")
178
+
179
+ if pushed:
180
+ detail = "已推送 " + "、".join(pushed)
181
+ if failed or errors:
182
+ detail += f"({len(failed)} 条失败、{len(errors)} 条错误)"
183
+ status = "pushed"
184
+ elif errors:
185
+ status = "error"
186
+ detail = ";".join(errors[:3])
187
+ else:
188
+ status = "no-update"
189
+ detail = f"没有新 release(监控 {len(subscriptions)} 个仓库)"
190
+
191
+ return CheckResult(
192
+ status=status,
193
+ detail=detail,
194
+ repo_count=len(subscriptions),
195
+ pushed=pushed,
196
+ failed=failed,
197
+ errors=errors,
198
+ )
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: nonebot-plugin-github-release
3
+ Version: 0.1.0
4
+ Summary: 监控任意 GitHub 仓库的 release,把新版本推送到 QQ 群
5
+ Project-URL: Homepage, https://github.com/NSQX13579/nonebot-plugin-github-release
6
+ Project-URL: Repository, https://github.com/NSQX13579/nonebot-plugin-github-release
7
+ Author-email: ns <102937666+NSQX13579@users.noreply.github.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 ns
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: github,nonebot,nonebot2,release,watch
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Requires-Python: >=3.9
35
+ Requires-Dist: httpx>=0.26
36
+ Requires-Dist: nonebot-adapter-onebot>=2.4.0
37
+ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
38
+ Requires-Dist: nonebot-plugin-localstore>=0.7.0
39
+ Requires-Dist: nonebot2>=2.4.0
40
+ Requires-Dist: pydantic>=2.0
41
+ Requires-Dist: tzdata; sys_platform == 'win32'
42
+ Provides-Extra: image
43
+ Requires-Dist: nonebot-plugin-htmlrender>=0.6.7; extra == 'image'
44
+ Provides-Extra: test
45
+ Requires-Dist: pytest>=7.0; extra == 'test'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # nonebot-plugin-github-release
49
+
50
+ 监控任意 GitHub 仓库的 release,把新版本推送到 QQ 群。按群订阅,每个群只收自己关心的仓库。
51
+
52
+ 本插件由 [nonebot-plugin-dsh-release](https://github.com/NSQX13579/nonebot-plugin-dsh-release) 改进而得。
53
+
54
+ ## 安装
55
+
56
+ ```bash
57
+ pip install nonebot-plugin-github-release
58
+ playwright install chromium # 卡片图片需要,不装会自动回退纯文本
59
+ ```
60
+
61
+ 依赖 `nonebot-plugin-apscheduler`(定时)和 `nonebot-plugin-localstore`(数据目录),装包时会一并带上。
62
+
63
+ ## 配置
64
+
65
+ `.env`(完整模板见 `.env.example`):
66
+
67
+ ```dotenv
68
+ # 监控哪些仓库(留空也行,直接在群里 /gh订阅 更灵活)
69
+ GH_WATCH_REPOS=[]
70
+ GH_WATCH_GROUPS=[]
71
+
72
+ # 可选:匿名 API 限额 60 次/小时且按出口 IP 共享,多仓库建议配上
73
+ GH_WATCH_GITHUB_TOKEN=
74
+ ```
75
+
76
+ 插件不带任何默认仓库——用群里的 `/gh订阅 owner/repo` 加,或者把上面的列表填上。
77
+
78
+ ## 指令
79
+
80
+ | 指令 | 权限 | 说明 |
81
+ | --- | --- | --- |
82
+ | `/gh订阅 owner/repo` | 群管 | 本群订阅某个仓库 |
83
+ | `/gh退订 owner/repo` | 群管 | 取消订阅 |
84
+ | `/gh列表` | 所有人 | 看本群订阅了哪些仓库 |
85
+ | `/gh公告 owner/repo` | 所有人 | 最新一条 release 全文 |
86
+ | `/gh公告 owner/repo 5` | 所有人 | 列最近 5 条(最多 10) |
87
+ | `/gh公告 owner/repo v1.2` | 所有人 | 按版本号检索 |
88
+ | `/gh状态` | 所有人 | 推送状态 |
89
+ | `/gh检查` | 超管 | 立刻检查一次,不等下一轮 |
90
+
91
+ ## 消息长什么样
92
+
93
+ ![示例](docs/card-example.png)
94
+
95
+ 图下面另附一行带链接的文字——图片里的地址点不了。卡片上的头像、发布时间、版本号、正文、表情计数都是 GitHub 的真实数据。
96
+
97
+ **渲染出任何问题都会回退纯文本**(没装 htmlrender、没装 chromium、超时都算),只打一条警告日志,推送不受影响。
98
+
99
+ ## 行为说明
100
+
101
+ - 数据源:Releases API 为主,被限流时自动切到不限流的 `releases.atom`
102
+ - 首次运行只建推送基线,不会把历史 release 刷进群
103
+ - 以 tag 去重,失败的不写进已推送列表,下一轮自然重试
104
+ - 中英双语正文默认只保留中文段,`GH_WATCH_BODY_LANGUAGE=all` 可改成两段都留
105
+ - `GH_WATCH_INCLUDE_PRERELEASE=False` 则只推正式版
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,16 @@
1
+ nonebot_plugin_github_release/__init__.py,sha256=r0gyMcjx9Zp-FESRBdzm1qrPD6MwacRSspDc-m6qAFE,2261
2
+ nonebot_plugin_github_release/card.py,sha256=iQGQkCLFCuNNMUCkBDJZB7R5CU3VRiIoX-3xDU_GoA8,12326
3
+ nonebot_plugin_github_release/commands.py,sha256=hhjSEUjOWZ-Wo3EShBHOyKI5Kpu0T_iI8R0d_CKYvAY,9982
4
+ nonebot_plugin_github_release/config.py,sha256=jX1kOBKTciNclKrx-HMhGd20bgVcLm5eVOLT3gHiyMI,2117
5
+ nonebot_plugin_github_release/latest.py,sha256=kK30Y262aYogkKINlg0QfXYz8d-NEEipOccMtdQawqQ,1972
6
+ nonebot_plugin_github_release/models.py,sha256=t6VNTclLlZNromcdsU8Lyq5JnBlh943KINRnZj83QeM,2031
7
+ nonebot_plugin_github_release/plan.py,sha256=QJm5LqINleSyWwjSWW5dBRD1jgtvupSE2vNoq-Dicko,1648
8
+ nonebot_plugin_github_release/render.py,sha256=rb8R8rdK7YaOOw-KKggpBfGHtR9tNMsQCWDTRRy8uM0,6004
9
+ nonebot_plugin_github_release/source.py,sha256=mVAGwGslOSIeX_0JqAxf12JkBWqAmIw08hakDVGVTyM,5802
10
+ nonebot_plugin_github_release/store.py,sha256=0fkqDbFHny0yZj3Zn-l6Dkbx3cWnYaNthhhV7-zPC9s,4520
11
+ nonebot_plugin_github_release/versions.py,sha256=qBS2O8RPF5M4tLW-mFxCwX9GolFKEHugoH2ekjZZwjg,1492
12
+ nonebot_plugin_github_release/watch.py,sha256=mBvdUuM3zVGoPuIOL5CPTSvEUAcV06lqMr5WjkRBhiY,7026
13
+ nonebot_plugin_github_release-0.1.0.dist-info/METADATA,sha256=fcK9tVX2mOLxJAQGQgLNSIY7Q4hpD-ZyKJbmXIuqDMA,4655
14
+ nonebot_plugin_github_release-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
15
+ nonebot_plugin_github_release-0.1.0.dist-info/licenses/LICENSE,sha256=0WfBnUlo5DQbHA5SAIVlDFlYbkIzl0OkjiI7DqTC7CA,1059
16
+ nonebot_plugin_github_release-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ns
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.