nonebot-plugin-git-poller 0.1.5__tar.gz
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.
- nonebot_plugin_git_poller-0.1.5/PKG-INFO +103 -0
- nonebot_plugin_git_poller-0.1.5/README.md +87 -0
- nonebot_plugin_git_poller-0.1.5/pyproject.toml +40 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/__init__.py +449 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/archive.py +145 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/command_args.py +31 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/config.py +17 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/file_server.py +126 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/git.py +430 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/message.py +135 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/mirror.py +485 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/models.py +153 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/repository.py +161 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/schedule.py +137 -0
- nonebot_plugin_git_poller-0.1.5/src/nonebot_plugin_git_poller/state.py +229 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nonebot-plugin-git-poller
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: 按群订阅 Git 仓库更新的 NoneBot2 插件,支持多仓库、多分支定时拉取
|
|
5
|
+
Author: kusadact
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Dist: nonebot2>=2.0.0
|
|
8
|
+
Requires-Dist: nonebot-adapter-onebot>=2.1.3
|
|
9
|
+
Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
|
|
10
|
+
Requires-Dist: nonebot-plugin-localstore>=0.6.0
|
|
11
|
+
Requires-Dist: dulwich>=1.2.6
|
|
12
|
+
Requires-Dist: py7zr>=1.1.0
|
|
13
|
+
Requires-Dist: urllib3>=2.0.0
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
<a href="https://v2.nonebot.dev/store">
|
|
19
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo">
|
|
20
|
+
</a>
|
|
21
|
+
|
|
22
|
+
## nonebot-plugin-git-poller
|
|
23
|
+
|
|
24
|
+
[](./LICENSE)
|
|
25
|
+
[](https://pypi.org/project/nonebot-plugin-git-poller/)
|
|
26
|
+
[](https://www.python.org)
|
|
27
|
+
[](https://github.com/astral-sh/uv)
|
|
28
|
+
[](https://codecov.io/gh/kusadact/nonebot-plugin-git-poller)
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
按群订阅 Git 仓库更新的 NoneBot2 插件,支持多仓库、多分支定时拉取,自动推送 commit 更新摘要并上传源码压缩包。
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
在 NoneBot 项目目录中安装插件:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add nonebot-plugin-git-poller
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
在 `pyproject.toml` 中加载插件:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[tool.nonebot]
|
|
46
|
+
plugins = ["nonebot_plugin_git_poller"]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 配置
|
|
50
|
+
|
|
51
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| `git_poller_default_schedule` | 否 | `每日04:00` | 新关注仓库的默认定时规则;留空关闭默认定时注册。 |
|
|
54
|
+
| `git_poller_timezone` | 否 | `Asia/Shanghai` | 定时任务时区。 |
|
|
55
|
+
| `git_poller_proxy` | 否 | 空 | HTTP/HTTPS Git 拉取代理。 |
|
|
56
|
+
| `git_poller_timeout` | 否 | `60.0` | HTTP/HTTPS Git 拉取超时,单位秒。 |
|
|
57
|
+
| `git_poller_archive_password` | 否 | 空 | 全局默认压缩包密码;为空时默认不设置密码。 |
|
|
58
|
+
| `git_poller_file_base_url` | 条件 | 空 | 上传压缩包时使用的 NoneBot HTTP 服务根地址;Bot 和 OneBot/NapCat 不在同一个文件系统时必须配置,例如 http://nonebot:8088。 |
|
|
59
|
+
|
|
60
|
+
## 指令
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
/关注仓库 仓库url [--分支名]
|
|
64
|
+
/取关仓库 仓库url [--分支名]
|
|
65
|
+
/设置仓库 仓库url [--分支名]
|
|
66
|
+
/拉取仓库 仓库url [--分支名]
|
|
67
|
+
/仓库摘要 仓库url [--分支名]
|
|
68
|
+
/仓库列表
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
带 URL 的命令都支持可选分支后缀,例如 `/关注仓库 https://example.test/repo.git --dev`。`/关注仓库` 不写分支时追踪远端默认分支;其他命令不写分支时优先使用本群本仓库的唯一订阅,若同仓库关注了多个分支则需要写 `--分支名`。
|
|
72
|
+
|
|
73
|
+
`/关注仓库` 在当前群关注仓库。同一个群可以关注同一仓库的不同分支。
|
|
74
|
+
|
|
75
|
+
`/取关仓库` 移除当前群的对应仓库分支订阅,不影响其他群。
|
|
76
|
+
|
|
77
|
+
`/设置仓库` 进入设置流程。Bot 会回复:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
输入设置数字选项
|
|
81
|
+
1. 修改当前仓库推送抓取时间
|
|
82
|
+
2. 修改当前仓库上传压缩包密码(选择后输入无则清除当前仓库密码回到全局默认)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
回复 `1` 后,下一条消息必须是合法定时格式。回复 `2` 后,下一条消息会保存为当前仓库压缩包密码,此时输入 `无` 会清除当前仓库密码并回到全局默认。输入非法会取消。
|
|
86
|
+
|
|
87
|
+
`/仓库列表` 显示当前群关注的仓库、分支、定时、启用状态、`last_success_sha` 和压缩包密码来源。
|
|
88
|
+
|
|
89
|
+
`/拉取仓库` 立即拉取当前群已关注的仓库,上传最新的源码压缩包。
|
|
90
|
+
|
|
91
|
+
`/仓库摘要` 仅拉取远端并展示本群记录与远端 HEAD 的差异。
|
|
92
|
+
|
|
93
|
+
## 定时格式
|
|
94
|
+
|
|
95
|
+
支持:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
每日hh:mm
|
|
99
|
+
每x天hh:mm
|
|
100
|
+
周xhh:mm
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`每x天` 的 `x` 使用 1 到 30 的整数;`周x` 只支持汉字 `一二三四五六日/天`
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://v2.nonebot.dev/store">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo">
|
|
4
|
+
</a>
|
|
5
|
+
|
|
6
|
+
## nonebot-plugin-git-poller
|
|
7
|
+
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://pypi.org/project/nonebot-plugin-git-poller/)
|
|
10
|
+
[](https://www.python.org)
|
|
11
|
+
[](https://github.com/astral-sh/uv)
|
|
12
|
+
[](https://codecov.io/gh/kusadact/nonebot-plugin-git-poller)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
按群订阅 Git 仓库更新的 NoneBot2 插件,支持多仓库、多分支定时拉取,自动推送 commit 更新摘要并上传源码压缩包。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
在 NoneBot 项目目录中安装插件:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add nonebot-plugin-git-poller
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
在 `pyproject.toml` 中加载插件:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[tool.nonebot]
|
|
30
|
+
plugins = ["nonebot_plugin_git_poller"]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 配置
|
|
34
|
+
|
|
35
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `git_poller_default_schedule` | 否 | `每日04:00` | 新关注仓库的默认定时规则;留空关闭默认定时注册。 |
|
|
38
|
+
| `git_poller_timezone` | 否 | `Asia/Shanghai` | 定时任务时区。 |
|
|
39
|
+
| `git_poller_proxy` | 否 | 空 | HTTP/HTTPS Git 拉取代理。 |
|
|
40
|
+
| `git_poller_timeout` | 否 | `60.0` | HTTP/HTTPS Git 拉取超时,单位秒。 |
|
|
41
|
+
| `git_poller_archive_password` | 否 | 空 | 全局默认压缩包密码;为空时默认不设置密码。 |
|
|
42
|
+
| `git_poller_file_base_url` | 条件 | 空 | 上传压缩包时使用的 NoneBot HTTP 服务根地址;Bot 和 OneBot/NapCat 不在同一个文件系统时必须配置,例如 http://nonebot:8088。 |
|
|
43
|
+
|
|
44
|
+
## 指令
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/关注仓库 仓库url [--分支名]
|
|
48
|
+
/取关仓库 仓库url [--分支名]
|
|
49
|
+
/设置仓库 仓库url [--分支名]
|
|
50
|
+
/拉取仓库 仓库url [--分支名]
|
|
51
|
+
/仓库摘要 仓库url [--分支名]
|
|
52
|
+
/仓库列表
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
带 URL 的命令都支持可选分支后缀,例如 `/关注仓库 https://example.test/repo.git --dev`。`/关注仓库` 不写分支时追踪远端默认分支;其他命令不写分支时优先使用本群本仓库的唯一订阅,若同仓库关注了多个分支则需要写 `--分支名`。
|
|
56
|
+
|
|
57
|
+
`/关注仓库` 在当前群关注仓库。同一个群可以关注同一仓库的不同分支。
|
|
58
|
+
|
|
59
|
+
`/取关仓库` 移除当前群的对应仓库分支订阅,不影响其他群。
|
|
60
|
+
|
|
61
|
+
`/设置仓库` 进入设置流程。Bot 会回复:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
输入设置数字选项
|
|
65
|
+
1. 修改当前仓库推送抓取时间
|
|
66
|
+
2. 修改当前仓库上传压缩包密码(选择后输入无则清除当前仓库密码回到全局默认)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
回复 `1` 后,下一条消息必须是合法定时格式。回复 `2` 后,下一条消息会保存为当前仓库压缩包密码,此时输入 `无` 会清除当前仓库密码并回到全局默认。输入非法会取消。
|
|
70
|
+
|
|
71
|
+
`/仓库列表` 显示当前群关注的仓库、分支、定时、启用状态、`last_success_sha` 和压缩包密码来源。
|
|
72
|
+
|
|
73
|
+
`/拉取仓库` 立即拉取当前群已关注的仓库,上传最新的源码压缩包。
|
|
74
|
+
|
|
75
|
+
`/仓库摘要` 仅拉取远端并展示本群记录与远端 HEAD 的差异。
|
|
76
|
+
|
|
77
|
+
## 定时格式
|
|
78
|
+
|
|
79
|
+
支持:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
每日hh:mm
|
|
83
|
+
每x天hh:mm
|
|
84
|
+
周xhh:mm
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`每x天` 的 `x` 使用 1 到 30 的整数;`周x` 只支持汉字 `一二三四五六日/天`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nonebot-plugin-git-poller"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "按群订阅 Git 仓库更新的 NoneBot2 插件,支持多仓库、多分支定时拉取"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "kusadact"},
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
dependencies = [
|
|
10
|
+
"nonebot2>=2.0.0",
|
|
11
|
+
"nonebot-adapter-onebot>=2.1.3",
|
|
12
|
+
"nonebot-plugin-apscheduler>=0.5.0",
|
|
13
|
+
"nonebot-plugin-localstore>=0.6.0",
|
|
14
|
+
"dulwich>=1.2.6",
|
|
15
|
+
"py7zr>=1.1.0",
|
|
16
|
+
"urllib3>=2.0.0",
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.10"
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.0.0",
|
|
24
|
+
"pytest-cov>=7.1.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
29
|
+
build-backend = "uv_build"
|
|
30
|
+
|
|
31
|
+
[tool.uv]
|
|
32
|
+
required-version = ">=0.11.0"
|
|
33
|
+
default-groups = ["dev"]
|
|
34
|
+
|
|
35
|
+
[tool.uv.build-backend]
|
|
36
|
+
module-name = "nonebot_plugin_git_poller"
|
|
37
|
+
module-root = "src"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from nonebot import get_bots, logger, on_command, require
|
|
7
|
+
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message
|
|
8
|
+
from nonebot.exception import FinishedException
|
|
9
|
+
from nonebot.matcher import Matcher
|
|
10
|
+
from nonebot.params import ArgPlainText, CommandArg
|
|
11
|
+
from nonebot.plugin import PluginMetadata
|
|
12
|
+
|
|
13
|
+
require("nonebot_plugin_apscheduler")
|
|
14
|
+
require("nonebot_plugin_localstore")
|
|
15
|
+
|
|
16
|
+
from nonebot_plugin_apscheduler import scheduler
|
|
17
|
+
|
|
18
|
+
from .command_args import parse_repo_command_args
|
|
19
|
+
from .config import Config, plugin_config
|
|
20
|
+
from .file_server import register_archive_file_route
|
|
21
|
+
from .message import (
|
|
22
|
+
ArchiveUploadUriError,
|
|
23
|
+
build_archive_delivery_text,
|
|
24
|
+
send_update_to_group,
|
|
25
|
+
upload_archive_to_group,
|
|
26
|
+
)
|
|
27
|
+
from .mirror import GitPollerService
|
|
28
|
+
from .schedule import parse_schedule
|
|
29
|
+
|
|
30
|
+
__plugin_meta__ = PluginMetadata(
|
|
31
|
+
name="Git Poller",
|
|
32
|
+
description="按群订阅 Git 仓库更新,支持多仓库、多分支定时拉取",
|
|
33
|
+
usage=(
|
|
34
|
+
"/关注仓库 仓库url [--分支名]\n"
|
|
35
|
+
"/取关仓库 仓库url [--分支名]\n"
|
|
36
|
+
"/设置仓库 仓库url [--分支名]\n"
|
|
37
|
+
"/仓库列表\n"
|
|
38
|
+
"/拉取仓库 仓库url [--分支名]\n"
|
|
39
|
+
"/仓库摘要 仓库url [--分支名]"
|
|
40
|
+
),
|
|
41
|
+
type="application",
|
|
42
|
+
config=Config,
|
|
43
|
+
supported_adapters={"~onebot.v11"},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
service = GitPollerService(plugin_config)
|
|
47
|
+
COMMAND_PRIORITY = 10
|
|
48
|
+
|
|
49
|
+
follow_repo = on_command(
|
|
50
|
+
"关注仓库",
|
|
51
|
+
priority=COMMAND_PRIORITY,
|
|
52
|
+
block=True,
|
|
53
|
+
)
|
|
54
|
+
unfollow_repo = on_command(
|
|
55
|
+
"取关仓库",
|
|
56
|
+
priority=COMMAND_PRIORITY,
|
|
57
|
+
block=True,
|
|
58
|
+
)
|
|
59
|
+
configure_repo = on_command(
|
|
60
|
+
"设置仓库",
|
|
61
|
+
priority=COMMAND_PRIORITY,
|
|
62
|
+
block=True,
|
|
63
|
+
)
|
|
64
|
+
list_repos = on_command(
|
|
65
|
+
"仓库列表",
|
|
66
|
+
priority=COMMAND_PRIORITY,
|
|
67
|
+
block=True,
|
|
68
|
+
)
|
|
69
|
+
pull_repo = on_command(
|
|
70
|
+
"拉取仓库",
|
|
71
|
+
priority=COMMAND_PRIORITY,
|
|
72
|
+
block=True,
|
|
73
|
+
)
|
|
74
|
+
summarize_repo = on_command(
|
|
75
|
+
"仓库摘要",
|
|
76
|
+
priority=COMMAND_PRIORITY,
|
|
77
|
+
block=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@follow_repo.handle()
|
|
82
|
+
async def _(event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()) -> None:
|
|
83
|
+
try:
|
|
84
|
+
parsed = _repo_args(args)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
await matcher.finish(f"用法:/关注仓库 仓库url [--分支名]\n{exc}")
|
|
87
|
+
if parsed is None:
|
|
88
|
+
await matcher.finish("用法:/关注仓库 仓库url [--分支名]")
|
|
89
|
+
group_id = int(event.group_id)
|
|
90
|
+
try:
|
|
91
|
+
result = await service.follow_repo(group_id, parsed.url, parsed.branch)
|
|
92
|
+
if result.already_following:
|
|
93
|
+
await matcher.finish(
|
|
94
|
+
f"本群已经关注:{result.identity.display_name}\n"
|
|
95
|
+
f"分支:{result.subscription.branch}\n"
|
|
96
|
+
f"定时:{result.subscription.schedule}"
|
|
97
|
+
)
|
|
98
|
+
await matcher.finish(
|
|
99
|
+
f"已关注:{result.identity.display_name}\n"
|
|
100
|
+
f"分支:{result.subscription.branch}\n"
|
|
101
|
+
f"定时:{result.subscription.schedule}\n"
|
|
102
|
+
f"当前 commit 已记录,后续有新提交时推送。"
|
|
103
|
+
)
|
|
104
|
+
except FinishedException:
|
|
105
|
+
raise
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.exception("git poller follow command failed")
|
|
108
|
+
await matcher.finish(f"关注仓库失败:{exc}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@unfollow_repo.handle()
|
|
112
|
+
async def _(event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()) -> None:
|
|
113
|
+
try:
|
|
114
|
+
parsed = _repo_args(args)
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
await matcher.finish(f"用法:/取关仓库 仓库url [--分支名]\n{exc}")
|
|
117
|
+
if parsed is None:
|
|
118
|
+
await matcher.finish("用法:/取关仓库 仓库url [--分支名]")
|
|
119
|
+
try:
|
|
120
|
+
identity, removed = service.unfollow_repo(
|
|
121
|
+
int(event.group_id),
|
|
122
|
+
parsed.url,
|
|
123
|
+
parsed.branch,
|
|
124
|
+
)
|
|
125
|
+
if removed:
|
|
126
|
+
_schedule_repo_cleanup(identity.key)
|
|
127
|
+
await matcher.finish(f"已取关:{identity.display_name}")
|
|
128
|
+
await matcher.finish(f"本群没有关注:{identity.display_name}")
|
|
129
|
+
except FinishedException:
|
|
130
|
+
raise
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.exception("git poller unfollow command failed")
|
|
133
|
+
await matcher.finish(f"取关仓库失败:{exc}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@configure_repo.handle()
|
|
137
|
+
async def _(
|
|
138
|
+
event: GroupMessageEvent,
|
|
139
|
+
matcher: Matcher,
|
|
140
|
+
args: Message = CommandArg(),
|
|
141
|
+
) -> None:
|
|
142
|
+
try:
|
|
143
|
+
parsed = _repo_args(args)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
await matcher.finish(f"用法:/设置仓库 仓库url [--分支名]\n{exc}")
|
|
146
|
+
if parsed is None:
|
|
147
|
+
await matcher.finish("用法:/设置仓库 仓库url [--分支名]")
|
|
148
|
+
try:
|
|
149
|
+
identity, subscription = service.get_repo_subscription(
|
|
150
|
+
int(event.group_id),
|
|
151
|
+
parsed.url,
|
|
152
|
+
parsed.branch,
|
|
153
|
+
)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.exception("git poller configure command failed")
|
|
156
|
+
await matcher.finish(f"设置仓库失败:{exc}")
|
|
157
|
+
matcher.state["repo_url"] = parsed.url
|
|
158
|
+
matcher.state["repo_branch"] = subscription.branch
|
|
159
|
+
matcher.state["repo_identity_name"] = identity.display_name
|
|
160
|
+
matcher.state["repo_subscription_branch"] = subscription.branch
|
|
161
|
+
matcher.state["repo_config_group_id"] = int(event.group_id)
|
|
162
|
+
matcher.state["repo_config_user_id"] = int(event.user_id)
|
|
163
|
+
await matcher.send(
|
|
164
|
+
"输入设置数字选项\n"
|
|
165
|
+
"1. 修改当前仓库推送抓取时间\n"
|
|
166
|
+
"2. 修改当前仓库上传压缩包密码(选择后输入无则清除当前仓库密码回到全局默认)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@configure_repo.got("setting_choice")
|
|
171
|
+
async def _(
|
|
172
|
+
event: GroupMessageEvent,
|
|
173
|
+
matcher: Matcher,
|
|
174
|
+
choice: str = ArgPlainText("setting_choice"),
|
|
175
|
+
) -> None:
|
|
176
|
+
if not _same_config_session(event, matcher):
|
|
177
|
+
await matcher.finish("输入非法,已取消设置。")
|
|
178
|
+
choice = choice.strip()
|
|
179
|
+
if choice not in {"1", "2"}:
|
|
180
|
+
await matcher.finish("输入非法,已取消设置。")
|
|
181
|
+
matcher.state["setting_choice"] = choice
|
|
182
|
+
if choice == "1":
|
|
183
|
+
await matcher.send("请输入新的推送抓取时间,例如:每日04:00、每3天04:00、周一04:00")
|
|
184
|
+
else:
|
|
185
|
+
await matcher.send("请输入新的压缩包密码。输入 无 则清除当前仓库密码回到全局默认。")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@configure_repo.got("setting_value")
|
|
189
|
+
async def _(
|
|
190
|
+
event: GroupMessageEvent,
|
|
191
|
+
matcher: Matcher,
|
|
192
|
+
value: str = ArgPlainText("setting_value"),
|
|
193
|
+
) -> None:
|
|
194
|
+
if not _same_config_session(event, matcher):
|
|
195
|
+
await matcher.finish("输入非法,已取消设置。")
|
|
196
|
+
group_id = int(event.group_id)
|
|
197
|
+
url = str(matcher.state["repo_url"])
|
|
198
|
+
branch = matcher.state.get("repo_branch")
|
|
199
|
+
if branch is not None:
|
|
200
|
+
branch = str(branch)
|
|
201
|
+
choice = str(matcher.state["setting_choice"])
|
|
202
|
+
value = value.strip()
|
|
203
|
+
if not value:
|
|
204
|
+
await matcher.finish("输入非法,已取消设置。")
|
|
205
|
+
|
|
206
|
+
if choice == "1":
|
|
207
|
+
try:
|
|
208
|
+
parse_schedule(value, plugin_config.git_poller_timezone)
|
|
209
|
+
identity, subscription = service.update_repo_schedule(
|
|
210
|
+
group_id,
|
|
211
|
+
url,
|
|
212
|
+
branch,
|
|
213
|
+
value,
|
|
214
|
+
)
|
|
215
|
+
_register_schedule(subscription.schedule)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.exception("git poller schedule setting failed")
|
|
218
|
+
await matcher.finish(f"输入非法,已取消设置:{exc}")
|
|
219
|
+
await matcher.finish(
|
|
220
|
+
f"设置成功:{identity.display_name}\n"
|
|
221
|
+
f"分支:{subscription.branch}\n"
|
|
222
|
+
f"推送抓取时间:{subscription.schedule}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
password = None if value in {"无", "none", "None", "NONE", "clear", "Clear", "CLEAR"} else value
|
|
226
|
+
try:
|
|
227
|
+
identity, subscription = service.update_repo_archive_password(
|
|
228
|
+
group_id,
|
|
229
|
+
url,
|
|
230
|
+
branch,
|
|
231
|
+
password,
|
|
232
|
+
)
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
logger.exception("git poller archive password setting failed")
|
|
235
|
+
await matcher.finish(f"输入非法,已取消设置:{exc}")
|
|
236
|
+
status = "回到全局默认" if subscription.archive_password is None else "已设置当前仓库密码"
|
|
237
|
+
await matcher.finish(
|
|
238
|
+
f"设置成功:{identity.display_name}\n"
|
|
239
|
+
f"分支:{subscription.branch}\n"
|
|
240
|
+
f"压缩包密码:{status}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@list_repos.handle()
|
|
245
|
+
async def _(event: GroupMessageEvent, matcher: Matcher) -> None:
|
|
246
|
+
subscriptions = service.list_group_subscriptions(int(event.group_id))
|
|
247
|
+
if not subscriptions:
|
|
248
|
+
await matcher.finish("本群还没有关注任何仓库。")
|
|
249
|
+
lines = ["本群关注的仓库:"]
|
|
250
|
+
for index, (repo_key, subscription) in enumerate(subscriptions.items(), start=1):
|
|
251
|
+
status = "启用" if subscription.enabled else "停用"
|
|
252
|
+
last_sha = subscription.last_success_sha[:8] if subscription.last_success_sha else "未记录"
|
|
253
|
+
password_status = "仓库密码" if subscription.archive_password else "全局默认"
|
|
254
|
+
lines.append(
|
|
255
|
+
f"{index}. {subscription.url}\n"
|
|
256
|
+
f" key: {repo_key}\n"
|
|
257
|
+
f" branch: {subscription.branch} / schedule: {subscription.schedule} / {status}\n"
|
|
258
|
+
f" last_success_sha: {last_sha}\n"
|
|
259
|
+
f" archive_password: {password_status}"
|
|
260
|
+
)
|
|
261
|
+
await matcher.finish("\n".join(lines))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@pull_repo.handle()
|
|
265
|
+
async def _(bot: Bot, event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()) -> None:
|
|
266
|
+
try:
|
|
267
|
+
parsed = _repo_args(args)
|
|
268
|
+
except ValueError as exc:
|
|
269
|
+
await matcher.finish(f"用法:/拉取仓库 仓库url [--分支名]\n{exc}")
|
|
270
|
+
if parsed is None:
|
|
271
|
+
await matcher.finish("用法:/拉取仓库 仓库url [--分支名]")
|
|
272
|
+
group_id = int(event.group_id)
|
|
273
|
+
try:
|
|
274
|
+
result = await service.pull_repo(group_id, parsed.url, parsed.branch)
|
|
275
|
+
await upload_archive_to_group(
|
|
276
|
+
bot,
|
|
277
|
+
group_id,
|
|
278
|
+
result.archive,
|
|
279
|
+
config=plugin_config,
|
|
280
|
+
)
|
|
281
|
+
service.mark_pull_success(group_id, result.identity.key, result.target_sha)
|
|
282
|
+
await matcher.finish(
|
|
283
|
+
build_archive_delivery_text(
|
|
284
|
+
result.payload,
|
|
285
|
+
result.archive,
|
|
286
|
+
title="拉取完成",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
except FinishedException:
|
|
290
|
+
raise
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
logger.exception("git poller pull command failed")
|
|
293
|
+
await matcher.finish(f"拉取仓库失败:{exc}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@summarize_repo.handle()
|
|
297
|
+
async def _(bot: Bot, event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()) -> None:
|
|
298
|
+
try:
|
|
299
|
+
parsed = _repo_args(args)
|
|
300
|
+
except ValueError as exc:
|
|
301
|
+
await matcher.finish(f"用法:/仓库摘要 仓库url [--分支名]\n{exc}")
|
|
302
|
+
if parsed is None:
|
|
303
|
+
await matcher.finish("用法:/仓库摘要 仓库url [--分支名]")
|
|
304
|
+
group_id = int(event.group_id)
|
|
305
|
+
try:
|
|
306
|
+
summary = await service.summarize_repo(group_id, parsed.url, parsed.branch)
|
|
307
|
+
payload = summary.result.payload
|
|
308
|
+
if payload.previous_sha == payload.target_sha:
|
|
309
|
+
await matcher.finish(
|
|
310
|
+
f"仓库摘要:{payload.repo_name}\n"
|
|
311
|
+
f"分支:{payload.branch}\n"
|
|
312
|
+
f"本地与远程相同:{payload.target_short_sha}"
|
|
313
|
+
)
|
|
314
|
+
await send_update_to_group(bot, group_id, payload)
|
|
315
|
+
behind_text = (
|
|
316
|
+
f"本地记录落后远程 {summary.behind_count} 个 commit。"
|
|
317
|
+
if summary.behind_count is not None
|
|
318
|
+
else "本地记录与远程存在差异。"
|
|
319
|
+
)
|
|
320
|
+
await matcher.finish(
|
|
321
|
+
f"仓库摘要已发送:{payload.repo_name}\n"
|
|
322
|
+
f"分支:{payload.branch}\n"
|
|
323
|
+
f"{behind_text}\n"
|
|
324
|
+
f"本地记录未更新。"
|
|
325
|
+
)
|
|
326
|
+
except FinishedException:
|
|
327
|
+
raise
|
|
328
|
+
except Exception as exc:
|
|
329
|
+
logger.exception("git poller summary command failed")
|
|
330
|
+
await matcher.finish(f"仓库摘要失败:{exc}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def run_scheduled_check(schedule: str) -> None:
|
|
334
|
+
bots = get_bots()
|
|
335
|
+
if not bots:
|
|
336
|
+
logger.warning("git poller scheduled check skipped: no bot is connected")
|
|
337
|
+
return
|
|
338
|
+
bot = next(iter(bots.values()))
|
|
339
|
+
try:
|
|
340
|
+
async for result in service.poll_schedule(schedule):
|
|
341
|
+
try:
|
|
342
|
+
await upload_archive_to_group(
|
|
343
|
+
bot,
|
|
344
|
+
result.result.group_id,
|
|
345
|
+
result.archive,
|
|
346
|
+
config=plugin_config,
|
|
347
|
+
)
|
|
348
|
+
await bot.send_group_msg(
|
|
349
|
+
group_id=int(result.result.group_id),
|
|
350
|
+
message=build_archive_delivery_text(
|
|
351
|
+
result.result.payload,
|
|
352
|
+
result.archive,
|
|
353
|
+
title="拉取完成",
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
service.mark_success(result.result)
|
|
357
|
+
except ArchiveUploadUriError as exc:
|
|
358
|
+
logger.exception(
|
|
359
|
+
f"git poller scheduled archive upload URI failed: "
|
|
360
|
+
f"group={result.result.group_id}, repo={result.result.repo_key}"
|
|
361
|
+
)
|
|
362
|
+
try:
|
|
363
|
+
await bot.send_group_msg(
|
|
364
|
+
group_id=int(result.result.group_id),
|
|
365
|
+
message=str(exc),
|
|
366
|
+
)
|
|
367
|
+
except Exception:
|
|
368
|
+
logger.exception(
|
|
369
|
+
f"git poller scheduled upload error notification failed: "
|
|
370
|
+
f"group={result.result.group_id}, repo={result.result.repo_key}"
|
|
371
|
+
)
|
|
372
|
+
continue
|
|
373
|
+
except Exception:
|
|
374
|
+
logger.exception(
|
|
375
|
+
f"git poller scheduled push failed: "
|
|
376
|
+
f"group={result.result.group_id}, repo={result.result.repo_key}"
|
|
377
|
+
)
|
|
378
|
+
continue
|
|
379
|
+
except Exception:
|
|
380
|
+
logger.exception(f"git poller scheduled check failed: {schedule}")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
async def cleanup_unsubscribed_repo(repo_key: str) -> None:
|
|
384
|
+
try:
|
|
385
|
+
await asyncio.to_thread(service.cleanup_unsubscribed_repo, repo_key)
|
|
386
|
+
except Exception:
|
|
387
|
+
logger.exception(f"git poller delayed cleanup failed: repo={repo_key}")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _register_schedules() -> None:
|
|
391
|
+
for schedule_rule in sorted(service.scheduled_rules()):
|
|
392
|
+
_register_schedule(schedule_rule)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _cleanup_orphaned_storage_on_startup() -> None:
|
|
396
|
+
try:
|
|
397
|
+
service.cleanup_orphaned_storage()
|
|
398
|
+
except Exception:
|
|
399
|
+
logger.exception("git poller startup orphan cleanup failed")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _register_schedule(schedule_rule: str) -> None:
|
|
403
|
+
try:
|
|
404
|
+
spec = parse_schedule(schedule_rule, plugin_config.git_poller_timezone)
|
|
405
|
+
except ValueError:
|
|
406
|
+
logger.exception(f"git poller skipped invalid schedule: {schedule_rule}")
|
|
407
|
+
return
|
|
408
|
+
if spec is None:
|
|
409
|
+
return
|
|
410
|
+
scheduler.add_job(
|
|
411
|
+
run_scheduled_check,
|
|
412
|
+
spec.trigger,
|
|
413
|
+
args=[schedule_rule],
|
|
414
|
+
id=f"git_poller:{schedule_rule}",
|
|
415
|
+
max_instances=1,
|
|
416
|
+
coalesce=True,
|
|
417
|
+
replace_existing=True,
|
|
418
|
+
**spec.trigger_kwargs,
|
|
419
|
+
)
|
|
420
|
+
logger.info(f"git poller scheduler registered: {spec.description}")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _schedule_repo_cleanup(repo_key: str) -> None:
|
|
424
|
+
run_at = datetime.now() + timedelta(hours=1)
|
|
425
|
+
scheduler.add_job(
|
|
426
|
+
cleanup_unsubscribed_repo,
|
|
427
|
+
"date",
|
|
428
|
+
args=[repo_key],
|
|
429
|
+
id=f"git_poller:cleanup:{repo_key}",
|
|
430
|
+
run_date=run_at,
|
|
431
|
+
replace_existing=True,
|
|
432
|
+
)
|
|
433
|
+
logger.info(f"git poller delayed cleanup scheduled: repo={repo_key}, run_at={run_at}")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _repo_args(args: Message):
|
|
437
|
+
return parse_repo_command_args(str(args))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _same_config_session(event: GroupMessageEvent, matcher: Matcher) -> bool:
|
|
441
|
+
return (
|
|
442
|
+
int(event.group_id) == int(matcher.state.get("repo_config_group_id", -1))
|
|
443
|
+
and int(event.user_id) == int(matcher.state.get("repo_config_user_id", -1))
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
_register_schedules()
|
|
448
|
+
_cleanup_orphaned_storage_on_startup()
|
|
449
|
+
register_archive_file_route(plugin_config)
|