nonebot-plugin-avatar-manager 0.1.0__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_avatar_manager-0.1.0/LICENSE +21 -0
- nonebot_plugin_avatar_manager-0.1.0/PKG-INFO +141 -0
- nonebot_plugin_avatar_manager-0.1.0/README.md +128 -0
- nonebot_plugin_avatar_manager-0.1.0/pyproject.toml +104 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/__init__.py +36 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/config.py +7 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/handlers.py +438 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/models.py +13 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/scheduler.py +213 -0
- nonebot_plugin_avatar_manager-0.1.0/src/nonebot_plugin_avatar_manager/utils.py +46 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 nonebot-plugin-avatar-manager contributors
|
|
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.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nonebot-plugin-avatar-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NoneBot2 头像管理插件,支持机器人与群资料的立即修改和定时修改。
|
|
5
|
+
Author-Email: Akiyy-Lab <2806578374@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: nonebot2>=2.3.1
|
|
9
|
+
Requires-Dist: nonebot-adapter-onebot>=2.4.6
|
|
10
|
+
Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
|
|
11
|
+
Requires-Dist: httpx>=0.27.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<div align="center">
|
|
15
|
+
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
|
|
16
|
+
<br>
|
|
17
|
+
<p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div align="center">
|
|
21
|
+
|
|
22
|
+
# nonebot-plugin-avatar-manager
|
|
23
|
+
|
|
24
|
+
_✨ NoneBot2 头像管理插件,支持机器人和群资料的立即修改与定时修改 ✨_
|
|
25
|
+
|
|
26
|
+
<a href="./LICENSE">
|
|
27
|
+
<img src="https://img.shields.io/github/license/Akiyy-dev/nonebot-plugin-avatar-manager.svg" alt="license">
|
|
28
|
+
</a>
|
|
29
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-avatar-manager">
|
|
30
|
+
<img src="https://img.shields.io/pypi/v/nonebot-plugin-avatar-manager.svg" alt="pypi">
|
|
31
|
+
</a>
|
|
32
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
## 📖 介绍
|
|
37
|
+
|
|
38
|
+
nonebot-plugin-avatar-manager 是一个基于 NoneBot2 和 OneBot V11 的资料管理插件,提供机器人头像/昵称、群头像/群名称的立即修改和定时修改能力,并支持任务持久化恢复。
|
|
39
|
+
|
|
40
|
+
插件目前支持的核心场景:
|
|
41
|
+
|
|
42
|
+
- 超级管理员在私聊中查看机器人信息和可管理群列表
|
|
43
|
+
- 群主或管理员在群聊中直接修改当前群头像或群名称
|
|
44
|
+
- 超级管理员修改机器人自身头像或昵称
|
|
45
|
+
- 为群资料或机器人资料创建定时修改任务
|
|
46
|
+
- 启动时自动恢复已保存任务
|
|
47
|
+
|
|
48
|
+
## 💿 安装
|
|
49
|
+
|
|
50
|
+
<details open>
|
|
51
|
+
<summary>使用 nb-cli 安装</summary>
|
|
52
|
+
|
|
53
|
+
在 nonebot2 项目的根目录下打开命令行,输入以下指令即可安装:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
nb plugin install nonebot-plugin-avatar-manager
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>使用包管理器安装</summary>
|
|
63
|
+
|
|
64
|
+
在 nonebot2 项目的插件目录下打开命令行,根据你使用的包管理器,输入相应的安装命令。
|
|
65
|
+
|
|
66
|
+
<details>
|
|
67
|
+
<summary>pip</summary>
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install nonebot-plugin-avatar-manager
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
</details>
|
|
74
|
+
|
|
75
|
+
安装完成后,打开 nonebot2 项目根目录下的 pyproject.toml 文件,在 `[tool.nonebot]` 部分追加写入:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
plugins = ["nonebot_plugin_avatar_manager"]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
## ⚙️ 配置
|
|
84
|
+
|
|
85
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置项:
|
|
86
|
+
|
|
87
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
88
|
+
|:-----:|:----:|:----:|:----|
|
|
89
|
+
| SUPERUSERS | 是 | 无 | NoneBot 超级管理员账号列表,私聊管理机器人资料时必需 |
|
|
90
|
+
| ENABLE_SELF_AVATAR | 否 | true | 是否允许修改机器人自身头像与昵称 |
|
|
91
|
+
| ENABLE_GROUP_AVATAR | 否 | true | 是否允许修改群头像与群名称 |
|
|
92
|
+
|
|
93
|
+
示例:
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
SUPERUSERS=["123456789"]
|
|
97
|
+
ENABLE_SELF_AVATAR=true
|
|
98
|
+
ENABLE_GROUP_AVATAR=true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 🎉 使用
|
|
102
|
+
|
|
103
|
+
### 指令表
|
|
104
|
+
|
|
105
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
106
|
+
|:----|:----|:----:|:----:|:----|
|
|
107
|
+
| 头像帮助 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 查看插件帮助 |
|
|
108
|
+
| 头像信息 | 超级管理员 | 否 | 私聊 | 查看机器人账号、昵称、头像地址与所在群列表 |
|
|
109
|
+
| 群管 | 超级管理员 | 否 | 私聊 | 查看机器人在哪些群具备管理权限 |
|
|
110
|
+
| 修改 | 群管理员 / 群主 | 否 | 群聊 | 立即修改当前群头像或群名称 |
|
|
111
|
+
| 定时修改 | 群管理员 / 群主 | 否 | 群聊 | 为当前群创建定时修改任务 |
|
|
112
|
+
| bot修改 | 超级管理员 | 否 | 私聊 / 群聊 | 立即修改机器人头像或昵称 |
|
|
113
|
+
| bot定时修改 | 超级管理员 | 否 | 私聊 / 群聊 | 为机器人自身创建定时修改任务 |
|
|
114
|
+
| 定时列表 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 查看任务列表;群聊中只显示当前群任务 |
|
|
115
|
+
| 删除定时 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 删除指定任务;群聊中仅可删除当前群任务 |
|
|
116
|
+
|
|
117
|
+
### 使用示例
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
修改 https://example.com/avatar.jpg
|
|
121
|
+
修改 新群名
|
|
122
|
+
修改 https://example.com/avatar.jpg 新群名
|
|
123
|
+
定时修改 0 8 * * * https://example.com/avatar.jpg
|
|
124
|
+
定时修改 0 8 * * * 新群名
|
|
125
|
+
bot修改 https://example.com/avatar.jpg 新昵称
|
|
126
|
+
bot定时修改 0 9 * * 1 https://example.com/avatar.jpg
|
|
127
|
+
删除定时 avatar_group_20260409100000
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Cron 示例
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
0 8 * * * 每天 8 点执行
|
|
134
|
+
0 9 * * 1 每周一 9 点执行
|
|
135
|
+
*/30 * * * * 每 30 分钟执行一次
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 任务存储
|
|
139
|
+
|
|
140
|
+
- `data/avatar_manager/tasks.json`:保存定时任务
|
|
141
|
+
- `data/avatar_manager/temp`:保存下载的临时图片
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
|
|
3
|
+
<br>
|
|
4
|
+
<p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
# nonebot-plugin-avatar-manager
|
|
10
|
+
|
|
11
|
+
_✨ NoneBot2 头像管理插件,支持机器人和群资料的立即修改与定时修改 ✨_
|
|
12
|
+
|
|
13
|
+
<a href="./LICENSE">
|
|
14
|
+
<img src="https://img.shields.io/github/license/Akiyy-dev/nonebot-plugin-avatar-manager.svg" alt="license">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-avatar-manager">
|
|
17
|
+
<img src="https://img.shields.io/pypi/v/nonebot-plugin-avatar-manager.svg" alt="pypi">
|
|
18
|
+
</a>
|
|
19
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
## 📖 介绍
|
|
24
|
+
|
|
25
|
+
nonebot-plugin-avatar-manager 是一个基于 NoneBot2 和 OneBot V11 的资料管理插件,提供机器人头像/昵称、群头像/群名称的立即修改和定时修改能力,并支持任务持久化恢复。
|
|
26
|
+
|
|
27
|
+
插件目前支持的核心场景:
|
|
28
|
+
|
|
29
|
+
- 超级管理员在私聊中查看机器人信息和可管理群列表
|
|
30
|
+
- 群主或管理员在群聊中直接修改当前群头像或群名称
|
|
31
|
+
- 超级管理员修改机器人自身头像或昵称
|
|
32
|
+
- 为群资料或机器人资料创建定时修改任务
|
|
33
|
+
- 启动时自动恢复已保存任务
|
|
34
|
+
|
|
35
|
+
## 💿 安装
|
|
36
|
+
|
|
37
|
+
<details open>
|
|
38
|
+
<summary>使用 nb-cli 安装</summary>
|
|
39
|
+
|
|
40
|
+
在 nonebot2 项目的根目录下打开命令行,输入以下指令即可安装:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
nb plugin install nonebot-plugin-avatar-manager
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
</details>
|
|
47
|
+
|
|
48
|
+
<details>
|
|
49
|
+
<summary>使用包管理器安装</summary>
|
|
50
|
+
|
|
51
|
+
在 nonebot2 项目的插件目录下打开命令行,根据你使用的包管理器,输入相应的安装命令。
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary>pip</summary>
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install nonebot-plugin-avatar-manager
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
安装完成后,打开 nonebot2 项目根目录下的 pyproject.toml 文件,在 `[tool.nonebot]` 部分追加写入:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
plugins = ["nonebot_plugin_avatar_manager"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
</details>
|
|
69
|
+
|
|
70
|
+
## ⚙️ 配置
|
|
71
|
+
|
|
72
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置项:
|
|
73
|
+
|
|
74
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
75
|
+
|:-----:|:----:|:----:|:----|
|
|
76
|
+
| SUPERUSERS | 是 | 无 | NoneBot 超级管理员账号列表,私聊管理机器人资料时必需 |
|
|
77
|
+
| ENABLE_SELF_AVATAR | 否 | true | 是否允许修改机器人自身头像与昵称 |
|
|
78
|
+
| ENABLE_GROUP_AVATAR | 否 | true | 是否允许修改群头像与群名称 |
|
|
79
|
+
|
|
80
|
+
示例:
|
|
81
|
+
|
|
82
|
+
```env
|
|
83
|
+
SUPERUSERS=["123456789"]
|
|
84
|
+
ENABLE_SELF_AVATAR=true
|
|
85
|
+
ENABLE_GROUP_AVATAR=true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 🎉 使用
|
|
89
|
+
|
|
90
|
+
### 指令表
|
|
91
|
+
|
|
92
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
93
|
+
|:----|:----|:----:|:----:|:----|
|
|
94
|
+
| 头像帮助 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 查看插件帮助 |
|
|
95
|
+
| 头像信息 | 超级管理员 | 否 | 私聊 | 查看机器人账号、昵称、头像地址与所在群列表 |
|
|
96
|
+
| 群管 | 超级管理员 | 否 | 私聊 | 查看机器人在哪些群具备管理权限 |
|
|
97
|
+
| 修改 | 群管理员 / 群主 | 否 | 群聊 | 立即修改当前群头像或群名称 |
|
|
98
|
+
| 定时修改 | 群管理员 / 群主 | 否 | 群聊 | 为当前群创建定时修改任务 |
|
|
99
|
+
| bot修改 | 超级管理员 | 否 | 私聊 / 群聊 | 立即修改机器人头像或昵称 |
|
|
100
|
+
| bot定时修改 | 超级管理员 | 否 | 私聊 / 群聊 | 为机器人自身创建定时修改任务 |
|
|
101
|
+
| 定时列表 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 查看任务列表;群聊中只显示当前群任务 |
|
|
102
|
+
| 删除定时 | 超级管理员 / 群管理员 / 群主 | 否 | 私聊 / 群聊 | 删除指定任务;群聊中仅可删除当前群任务 |
|
|
103
|
+
|
|
104
|
+
### 使用示例
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
修改 https://example.com/avatar.jpg
|
|
108
|
+
修改 新群名
|
|
109
|
+
修改 https://example.com/avatar.jpg 新群名
|
|
110
|
+
定时修改 0 8 * * * https://example.com/avatar.jpg
|
|
111
|
+
定时修改 0 8 * * * 新群名
|
|
112
|
+
bot修改 https://example.com/avatar.jpg 新昵称
|
|
113
|
+
bot定时修改 0 9 * * 1 https://example.com/avatar.jpg
|
|
114
|
+
删除定时 avatar_group_20260409100000
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Cron 示例
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
0 8 * * * 每天 8 点执行
|
|
121
|
+
0 9 * * 1 每周一 9 点执行
|
|
122
|
+
*/30 * * * * 每 30 分钟执行一次
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 任务存储
|
|
126
|
+
|
|
127
|
+
- `data/avatar_manager/tasks.json`:保存定时任务
|
|
128
|
+
- `data/avatar_manager/temp`:保存下载的临时图片
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nonebot-plugin-avatar-manager"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "NoneBot2 头像管理插件,支持机器人与群资料的立即修改和定时修改。"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Akiyy-Lab", email = "2806578374@qq.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"nonebot2>=2.3.1",
|
|
10
|
+
"nonebot-adapter-onebot>=2.4.6",
|
|
11
|
+
"nonebot-plugin-apscheduler>=0.5.0",
|
|
12
|
+
"httpx>=0.27.0",
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
|
|
17
|
+
[project.license]
|
|
18
|
+
text = "MIT"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = [
|
|
22
|
+
"pdm-backend",
|
|
23
|
+
]
|
|
24
|
+
build-backend = "pdm.backend"
|
|
25
|
+
|
|
26
|
+
[tool.pdm]
|
|
27
|
+
distribution = true
|
|
28
|
+
|
|
29
|
+
[tool.pdm.build]
|
|
30
|
+
includes = [
|
|
31
|
+
"src",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.pdm.dev-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"black>=24.4.2",
|
|
37
|
+
"isort>=5.13.2",
|
|
38
|
+
"ruff>=0.4.6",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.pdm.scripts]
|
|
42
|
+
test = "python -c 'print(\">>> Just \\\"pytest -W ignore -s\\\" when you complete your testsuite\")'"
|
|
43
|
+
|
|
44
|
+
[tool.pdm.scripts.format]
|
|
45
|
+
composite = [
|
|
46
|
+
"isort . ",
|
|
47
|
+
"black . ",
|
|
48
|
+
"ruff check .",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.black]
|
|
52
|
+
line-length = 90
|
|
53
|
+
target-version = [
|
|
54
|
+
"py310",
|
|
55
|
+
"py311",
|
|
56
|
+
"py312",
|
|
57
|
+
]
|
|
58
|
+
include = "\\\\.pyi?$"
|
|
59
|
+
extend-exclude = ""
|
|
60
|
+
|
|
61
|
+
[tool.isort]
|
|
62
|
+
profile = "black"
|
|
63
|
+
line_length = 90
|
|
64
|
+
length_sort = true
|
|
65
|
+
skip_gitignore = true
|
|
66
|
+
force_sort_within_sections = true
|
|
67
|
+
extra_standard_library = [
|
|
68
|
+
"typing_extensions",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 90
|
|
73
|
+
target-version = "py310"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = [
|
|
77
|
+
"E",
|
|
78
|
+
"W",
|
|
79
|
+
"F",
|
|
80
|
+
"UP",
|
|
81
|
+
"C",
|
|
82
|
+
"T",
|
|
83
|
+
"PYI",
|
|
84
|
+
"PT",
|
|
85
|
+
"Q",
|
|
86
|
+
]
|
|
87
|
+
ignore = [
|
|
88
|
+
"C901",
|
|
89
|
+
"T201",
|
|
90
|
+
"E731",
|
|
91
|
+
"E402",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.pyright]
|
|
95
|
+
pythonVersion = "3.10"
|
|
96
|
+
pythonPlatform = "All"
|
|
97
|
+
typeCheckingMode = "basic"
|
|
98
|
+
|
|
99
|
+
[tool.nonebot]
|
|
100
|
+
adapters = []
|
|
101
|
+
plugin_dirs = [
|
|
102
|
+
"src",
|
|
103
|
+
]
|
|
104
|
+
builtin_plugins = []
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# nonebot2
|
|
2
|
+
# nonebot-adapter-onebot
|
|
3
|
+
# nonebot-plugin-apscheduler
|
|
4
|
+
|
|
5
|
+
from nonebot import get_driver, logger, require
|
|
6
|
+
from nonebot.plugin import PluginMetadata
|
|
7
|
+
|
|
8
|
+
require("nonebot_plugin_apscheduler")
|
|
9
|
+
|
|
10
|
+
from . import handlers # noqa: E402,F401
|
|
11
|
+
from . import scheduler # noqa: E402
|
|
12
|
+
|
|
13
|
+
__plugin_meta__ = PluginMetadata(
|
|
14
|
+
name="头像管理器",
|
|
15
|
+
description="支持定时修改机器人自身头像/昵称以及群头像/群名称(基于 OneBot V11)",
|
|
16
|
+
usage="发送 头像帮助 查看详细指令",
|
|
17
|
+
type="application",
|
|
18
|
+
supported_adapters={"~onebot.v11"},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
driver = get_driver()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@driver.on_startup
|
|
25
|
+
async def _on_startup() -> None:
|
|
26
|
+
await scheduler.init_scheduler()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@driver.on_shutdown
|
|
30
|
+
async def _on_shutdown() -> None:
|
|
31
|
+
await scheduler.cleanup_temp_files()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["__plugin_meta__", "handlers", "scheduler"]
|
|
35
|
+
|
|
36
|
+
logger.success("头像管理器插件加载完成")
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from nonebot import get_driver, logger, on_command
|
|
6
|
+
from nonebot.adapters.onebot.v11 import (
|
|
7
|
+
Bot,
|
|
8
|
+
GroupMessageEvent,
|
|
9
|
+
Message,
|
|
10
|
+
PrivateMessageEvent,
|
|
11
|
+
)
|
|
12
|
+
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
|
|
13
|
+
from nonebot.params import CommandArg
|
|
14
|
+
from nonebot.permission import SUPERUSER
|
|
15
|
+
from nonebot.rule import Rule
|
|
16
|
+
|
|
17
|
+
from .config import Config
|
|
18
|
+
from .models import ScheduleTask
|
|
19
|
+
from .scheduler import add_job, remove_job, run_task_now, tasks
|
|
20
|
+
from .utils import download_image
|
|
21
|
+
|
|
22
|
+
driver = get_driver()
|
|
23
|
+
plugin_config = Config.model_validate(driver.config.dict())
|
|
24
|
+
manage_permission = SUPERUSER | GROUP_ADMIN | GROUP_OWNER
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _private_only(event: PrivateMessageEvent) -> bool:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _group_only(event: GroupMessageEvent) -> bool:
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _looks_like_url(value: str) -> bool:
|
|
36
|
+
return value.startswith(("http://", "https://"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
avatar_help = on_command(
|
|
40
|
+
"头像帮助",
|
|
41
|
+
aliases={"avatar_help"},
|
|
42
|
+
permission=manage_permission,
|
|
43
|
+
priority=5,
|
|
44
|
+
block=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
avatar_info = on_command(
|
|
48
|
+
"头像信息",
|
|
49
|
+
aliases={"avatar_info"},
|
|
50
|
+
permission=SUPERUSER,
|
|
51
|
+
rule=Rule(_private_only),
|
|
52
|
+
priority=5,
|
|
53
|
+
block=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
group_manage = on_command(
|
|
57
|
+
"群管",
|
|
58
|
+
permission=SUPERUSER,
|
|
59
|
+
rule=Rule(_private_only),
|
|
60
|
+
priority=5,
|
|
61
|
+
block=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
group_modify = on_command(
|
|
65
|
+
"修改",
|
|
66
|
+
permission=GROUP_ADMIN | GROUP_OWNER,
|
|
67
|
+
rule=Rule(_group_only),
|
|
68
|
+
priority=5,
|
|
69
|
+
block=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
group_schedule = on_command(
|
|
73
|
+
"定时修改",
|
|
74
|
+
permission=GROUP_ADMIN | GROUP_OWNER,
|
|
75
|
+
rule=Rule(_group_only),
|
|
76
|
+
priority=5,
|
|
77
|
+
block=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
bot_modify = on_command(
|
|
81
|
+
"bot修改",
|
|
82
|
+
permission=SUPERUSER,
|
|
83
|
+
priority=5,
|
|
84
|
+
block=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
bot_schedule = on_command(
|
|
88
|
+
"bot定时修改",
|
|
89
|
+
permission=SUPERUSER,
|
|
90
|
+
priority=5,
|
|
91
|
+
block=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
schedule_list = on_command(
|
|
95
|
+
"定时列表",
|
|
96
|
+
aliases={"schedule_list"},
|
|
97
|
+
permission=manage_permission,
|
|
98
|
+
priority=5,
|
|
99
|
+
block=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
del_schedule = on_command(
|
|
103
|
+
"删除定时",
|
|
104
|
+
aliases={"del_schedule"},
|
|
105
|
+
permission=manage_permission,
|
|
106
|
+
priority=5,
|
|
107
|
+
block=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_image_input(arg: Message) -> str | None:
|
|
112
|
+
for segment in arg:
|
|
113
|
+
if segment.type != "image":
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
image_url = segment.data.get("url")
|
|
117
|
+
if image_url:
|
|
118
|
+
return str(image_url)
|
|
119
|
+
|
|
120
|
+
image_file = segment.data.get("file")
|
|
121
|
+
if image_file and Path(str(image_file)).exists():
|
|
122
|
+
return str(image_file)
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_job_id(target_type: str) -> str:
|
|
128
|
+
return f"avatar_{target_type}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _resolve_image_value(image_input: str | None) -> str | None:
|
|
132
|
+
if image_input is None:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if image_input.startswith(("http://", "https://")):
|
|
136
|
+
downloaded_path = await download_image(image_input)
|
|
137
|
+
if downloaded_path is None:
|
|
138
|
+
raise ValueError("图片下载失败")
|
|
139
|
+
return str(downloaded_path)
|
|
140
|
+
|
|
141
|
+
return image_input
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _parse_modify_payload(arg: Message) -> tuple[str | None, str | None]:
|
|
145
|
+
plain_text = arg.extract_plain_text().strip()
|
|
146
|
+
parts = shlex.split(plain_text) if plain_text else []
|
|
147
|
+
|
|
148
|
+
image_input = _extract_image_input(arg)
|
|
149
|
+
if image_input is None and parts and _looks_like_url(parts[0]):
|
|
150
|
+
image_input = parts.pop(0)
|
|
151
|
+
|
|
152
|
+
image_path_value = await _resolve_image_value(image_input)
|
|
153
|
+
new_name = " ".join(parts).strip() or None
|
|
154
|
+
if image_path_value is None and new_name is None:
|
|
155
|
+
raise ValueError("至少提供头像图片或新名称之一")
|
|
156
|
+
|
|
157
|
+
return image_path_value, new_name
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _parse_timed_modify_payload(arg: Message) -> tuple[str, str | None, str | None]:
|
|
161
|
+
plain_text = arg.extract_plain_text().strip()
|
|
162
|
+
if not plain_text:
|
|
163
|
+
raise ValueError("参数不能为空")
|
|
164
|
+
|
|
165
|
+
parts = shlex.split(plain_text)
|
|
166
|
+
if len(parts) < 5:
|
|
167
|
+
raise ValueError("cron 格式错误,需要 5 段表达式")
|
|
168
|
+
|
|
169
|
+
cron = " ".join(parts[:5])
|
|
170
|
+
payload_parts = parts[5:]
|
|
171
|
+
|
|
172
|
+
image_input = _extract_image_input(arg)
|
|
173
|
+
if image_input is None and payload_parts and _looks_like_url(payload_parts[0]):
|
|
174
|
+
image_input = payload_parts.pop(0)
|
|
175
|
+
|
|
176
|
+
image_path_value = await _resolve_image_value(image_input)
|
|
177
|
+
new_name = " ".join(payload_parts).strip() or None
|
|
178
|
+
if image_path_value is None and new_name is None:
|
|
179
|
+
raise ValueError("至少提供头像图片或新名称之一")
|
|
180
|
+
|
|
181
|
+
return cron, image_path_value, new_name
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@avatar_help.handle()
|
|
185
|
+
async def avatar_help_handler(
|
|
186
|
+
event: PrivateMessageEvent | GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
187
|
+
) -> None:
|
|
188
|
+
help_text = """
|
|
189
|
+
头像管理器
|
|
190
|
+
|
|
191
|
+
可用命令:
|
|
192
|
+
- 头像帮助 / avatar_help
|
|
193
|
+
- 头像信息 / avatar_info
|
|
194
|
+
- 群管
|
|
195
|
+
- 修改
|
|
196
|
+
- 定时修改
|
|
197
|
+
- bot修改
|
|
198
|
+
- bot定时修改
|
|
199
|
+
- 定时列表 / schedule_list
|
|
200
|
+
- 删除定时 / del_schedule
|
|
201
|
+
|
|
202
|
+
示例:
|
|
203
|
+
- 群聊中发送:修改 https://example.com/avatar.jpg
|
|
204
|
+
- 群聊中发送:修改 example
|
|
205
|
+
- 群聊中发送:修改 https://example.com/avatar.jpg example
|
|
206
|
+
- 群聊中发送:定时修改 0 8 * * * https://example.com/avatar.jpg
|
|
207
|
+
- 私聊或群聊中超级管理员发送:bot修改 https://example.com/avatar.jpg
|
|
208
|
+
- 私聊或群聊中超级管理员发送:bot定时修改 0 8 * * * https://example.com/avatar.jpg
|
|
209
|
+
|
|
210
|
+
权限说明:
|
|
211
|
+
- 私聊中:仅超级管理员可操作全部目标
|
|
212
|
+
- 群聊中:群管理员和群主可配置当前群
|
|
213
|
+
|
|
214
|
+
注意:
|
|
215
|
+
- 具体 API 可用性取决于你使用的 OneBot V11 实现。
|
|
216
|
+
""".strip()
|
|
217
|
+
await avatar_help.finish(help_text)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@avatar_info.handle()
|
|
221
|
+
async def avatar_info_handler(
|
|
222
|
+
event: PrivateMessageEvent, bot: Bot, arg=CommandArg()
|
|
223
|
+
) -> None:
|
|
224
|
+
try:
|
|
225
|
+
login_info = await bot.get_login_info()
|
|
226
|
+
group_list = await bot.get_group_list()
|
|
227
|
+
except Exception as exception:
|
|
228
|
+
await avatar_info.finish(f"获取头像信息失败: {exception}")
|
|
229
|
+
|
|
230
|
+
lines = [
|
|
231
|
+
"头像管理器信息",
|
|
232
|
+
f"机器人 QQ: {bot.self_id}",
|
|
233
|
+
f"机器人昵称: {login_info.get('nickname', '未知')}",
|
|
234
|
+
f"机器人头像: http://q.qlogo.cn/headimg_dl?dst_uin={bot.self_id}&spec=640",
|
|
235
|
+
"所在群列表:",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
for group in group_list:
|
|
239
|
+
group_id = int(group["group_id"])
|
|
240
|
+
group_name = str(group.get("group_name", "未知群名"))
|
|
241
|
+
lines.append(
|
|
242
|
+
f"- {group_name} ({group_id}) | 群头像: http://p.qlogo.cn/gh/{group_id}/{group_id}/640"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
await avatar_info.finish("\n".join(lines))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@group_manage.handle()
|
|
249
|
+
async def group_manage_handler(
|
|
250
|
+
event: PrivateMessageEvent, bot: Bot, arg=CommandArg()
|
|
251
|
+
) -> None:
|
|
252
|
+
try:
|
|
253
|
+
group_list = await bot.get_group_list()
|
|
254
|
+
except Exception as exception:
|
|
255
|
+
await group_manage.finish(f"获取群列表失败: {exception}")
|
|
256
|
+
|
|
257
|
+
manageable_groups: list[str] = []
|
|
258
|
+
for group in group_list:
|
|
259
|
+
group_id = int(group["group_id"])
|
|
260
|
+
try:
|
|
261
|
+
member_info = await bot.get_group_member_info(
|
|
262
|
+
group_id=group_id,
|
|
263
|
+
user_id=int(bot.self_id),
|
|
264
|
+
)
|
|
265
|
+
except Exception as exception:
|
|
266
|
+
logger.warning(f"查询群 {group_id} 权限失败: {exception}")
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
role = str(member_info.get("role", "member"))
|
|
270
|
+
if role in {"owner", "admin"}:
|
|
271
|
+
group_name = str(group.get("group_name", "未知群名"))
|
|
272
|
+
manageable_groups.append(f"- {group_id} | {group_name} | {role}")
|
|
273
|
+
|
|
274
|
+
if not manageable_groups:
|
|
275
|
+
await group_manage.finish("无管理权限")
|
|
276
|
+
|
|
277
|
+
await group_manage.finish("可管理群列表:\n" + "\n".join(manageable_groups))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@group_modify.handle()
|
|
281
|
+
async def group_modify_handler(
|
|
282
|
+
event: GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
283
|
+
) -> None:
|
|
284
|
+
try:
|
|
285
|
+
if not plugin_config.enable_group_avatar:
|
|
286
|
+
await group_modify.finish("当前未启用群头像/群名称修改功能")
|
|
287
|
+
|
|
288
|
+
image_path, new_name = await _parse_modify_payload(arg)
|
|
289
|
+
task = ScheduleTask(
|
|
290
|
+
job_id=_build_job_id("group"),
|
|
291
|
+
target_type="group",
|
|
292
|
+
target_id=int(event.group_id),
|
|
293
|
+
cron="0 0 1 1 *",
|
|
294
|
+
new_name=new_name,
|
|
295
|
+
image_path=image_path,
|
|
296
|
+
)
|
|
297
|
+
success, message = await run_task_now(task)
|
|
298
|
+
if not success:
|
|
299
|
+
await group_modify.finish(f"立即修改失败: {message}")
|
|
300
|
+
except ValueError as exception:
|
|
301
|
+
await group_modify.finish(str(exception))
|
|
302
|
+
except Exception as exception:
|
|
303
|
+
await group_modify.finish(f"立即修改失败: {exception}")
|
|
304
|
+
|
|
305
|
+
await group_modify.finish("已立即修改当前群配置")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@group_schedule.handle()
|
|
309
|
+
async def group_schedule_handler(
|
|
310
|
+
event: GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
311
|
+
) -> None:
|
|
312
|
+
try:
|
|
313
|
+
if not plugin_config.enable_group_avatar:
|
|
314
|
+
await group_schedule.finish("当前未启用群头像/群名称修改功能")
|
|
315
|
+
|
|
316
|
+
cron, image_path, new_name = await _parse_timed_modify_payload(arg)
|
|
317
|
+
task = ScheduleTask(
|
|
318
|
+
job_id=_build_job_id("group"),
|
|
319
|
+
target_type="group",
|
|
320
|
+
target_id=int(event.group_id),
|
|
321
|
+
cron=cron,
|
|
322
|
+
new_name=new_name,
|
|
323
|
+
image_path=image_path,
|
|
324
|
+
)
|
|
325
|
+
add_job(task)
|
|
326
|
+
except ValueError as exception:
|
|
327
|
+
await group_schedule.finish(str(exception))
|
|
328
|
+
except Exception as exception:
|
|
329
|
+
await group_schedule.finish(f"添加定时任务失败: {exception}")
|
|
330
|
+
|
|
331
|
+
await group_schedule.finish(f"已添加定时任务 ID: {task.job_id}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@bot_modify.handle()
|
|
335
|
+
async def bot_modify_handler(
|
|
336
|
+
event: PrivateMessageEvent | GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
337
|
+
) -> None:
|
|
338
|
+
try:
|
|
339
|
+
if not plugin_config.enable_self_avatar:
|
|
340
|
+
await bot_modify.finish("当前未启用机器人自身头像/昵称修改功能")
|
|
341
|
+
|
|
342
|
+
image_path, new_name = await _parse_modify_payload(arg)
|
|
343
|
+
task = ScheduleTask(
|
|
344
|
+
job_id=_build_job_id("self"),
|
|
345
|
+
target_type="self",
|
|
346
|
+
cron="0 0 1 1 *",
|
|
347
|
+
new_name=new_name,
|
|
348
|
+
image_path=image_path,
|
|
349
|
+
)
|
|
350
|
+
success, message = await run_task_now(task)
|
|
351
|
+
if not success:
|
|
352
|
+
await bot_modify.finish(f"立即修改失败: {message}")
|
|
353
|
+
except ValueError as exception:
|
|
354
|
+
await bot_modify.finish(str(exception))
|
|
355
|
+
except Exception as exception:
|
|
356
|
+
await bot_modify.finish(f"立即修改失败: {exception}")
|
|
357
|
+
|
|
358
|
+
await bot_modify.finish("已立即修改机器人配置")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@bot_schedule.handle()
|
|
362
|
+
async def bot_schedule_handler(
|
|
363
|
+
event: PrivateMessageEvent | GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
364
|
+
) -> None:
|
|
365
|
+
try:
|
|
366
|
+
if not plugin_config.enable_self_avatar:
|
|
367
|
+
await bot_schedule.finish("当前未启用机器人自身头像/昵称修改功能")
|
|
368
|
+
|
|
369
|
+
cron, image_path, new_name = await _parse_timed_modify_payload(arg)
|
|
370
|
+
task = ScheduleTask(
|
|
371
|
+
job_id=_build_job_id("self"),
|
|
372
|
+
target_type="self",
|
|
373
|
+
cron=cron,
|
|
374
|
+
new_name=new_name,
|
|
375
|
+
image_path=image_path,
|
|
376
|
+
)
|
|
377
|
+
add_job(task)
|
|
378
|
+
except ValueError as exception:
|
|
379
|
+
await bot_schedule.finish(str(exception))
|
|
380
|
+
except Exception as exception:
|
|
381
|
+
await bot_schedule.finish(f"添加定时任务失败: {exception}")
|
|
382
|
+
|
|
383
|
+
await bot_schedule.finish(f"已添加定时任务 ID: {task.job_id}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@schedule_list.handle()
|
|
387
|
+
async def schedule_list_handler(
|
|
388
|
+
event: PrivateMessageEvent | GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
389
|
+
) -> None:
|
|
390
|
+
filtered_tasks = list(tasks.values())
|
|
391
|
+
if isinstance(event, GroupMessageEvent):
|
|
392
|
+
filtered_tasks = [
|
|
393
|
+
task
|
|
394
|
+
for task in filtered_tasks
|
|
395
|
+
if task.target_type == "group" and task.target_id == int(event.group_id)
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
if not filtered_tasks:
|
|
399
|
+
await schedule_list.finish("当前没有定时任务")
|
|
400
|
+
|
|
401
|
+
lines = [
|
|
402
|
+
" | ".join(
|
|
403
|
+
[
|
|
404
|
+
f"- {task.job_id}",
|
|
405
|
+
f"target={task.target_type}",
|
|
406
|
+
f"target_id={task.target_id or '-'}",
|
|
407
|
+
f"cron={task.cron}",
|
|
408
|
+
f"name={task.new_name or '-'}",
|
|
409
|
+
f"image={task.image_path or '-'}",
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
for task in filtered_tasks
|
|
413
|
+
]
|
|
414
|
+
await schedule_list.finish("已保存定时任务:\n" + "\n".join(lines))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@del_schedule.handle()
|
|
418
|
+
async def del_schedule_handler(
|
|
419
|
+
event: PrivateMessageEvent | GroupMessageEvent, bot: Bot, arg=CommandArg()
|
|
420
|
+
) -> None:
|
|
421
|
+
job_id = arg.extract_plain_text().strip()
|
|
422
|
+
if not job_id:
|
|
423
|
+
await del_schedule.finish("请提供要删除的任务 ID")
|
|
424
|
+
|
|
425
|
+
task = tasks.get(job_id)
|
|
426
|
+
if isinstance(event, GroupMessageEvent):
|
|
427
|
+
invalid_group_task = (
|
|
428
|
+
task is None
|
|
429
|
+
or task.target_type != "group"
|
|
430
|
+
or task.target_id != int(event.group_id)
|
|
431
|
+
)
|
|
432
|
+
if invalid_group_task:
|
|
433
|
+
await del_schedule.finish("未找到本群对应的任务 ID")
|
|
434
|
+
|
|
435
|
+
if not remove_job(job_id):
|
|
436
|
+
await del_schedule.finish(f"未找到任务 ID: {job_id}")
|
|
437
|
+
|
|
438
|
+
await del_schedule.finish(f"已删除定时任务 ID: {job_id}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScheduleTask(BaseModel):
|
|
7
|
+
job_id: str
|
|
8
|
+
target_type: str
|
|
9
|
+
target_id: int | None = None
|
|
10
|
+
cron: str
|
|
11
|
+
new_name: str | None = None
|
|
12
|
+
image_path: str | None = None
|
|
13
|
+
create_time: datetime = Field(default_factory=datetime.now)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import nonebot
|
|
6
|
+
from nonebot import logger
|
|
7
|
+
from nonebot.adapters.onebot.v11 import Bot
|
|
8
|
+
from nonebot.exception import ActionFailed
|
|
9
|
+
from nonebot_plugin_apscheduler import scheduler
|
|
10
|
+
|
|
11
|
+
from .models import ScheduleTask
|
|
12
|
+
from .utils import TEMP_DIR, image_to_base64
|
|
13
|
+
|
|
14
|
+
tasks: dict[str, ScheduleTask] = {}
|
|
15
|
+
data_dir = Path("data/avatar_manager")
|
|
16
|
+
tasks_file = data_dir / "tasks.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_tasks() -> dict[str, ScheduleTask]:
|
|
20
|
+
if not tasks_file.exists():
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
raw_data = json.loads(tasks_file.read_text(encoding="utf-8"))
|
|
25
|
+
except (json.JSONDecodeError, OSError) as exception:
|
|
26
|
+
logger.error(f"读取任务文件失败: {exception}")
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
loaded_tasks: dict[str, ScheduleTask] = {}
|
|
30
|
+
for job_id, task_data in raw_data.items():
|
|
31
|
+
try:
|
|
32
|
+
loaded_tasks[job_id] = ScheduleTask.model_validate(task_data)
|
|
33
|
+
except Exception as exception:
|
|
34
|
+
logger.error(f"加载任务 {job_id} 失败: {exception}")
|
|
35
|
+
return loaded_tasks
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_tasks() -> None:
|
|
39
|
+
try:
|
|
40
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
payload = {
|
|
42
|
+
job_id: task.model_dump(mode="json")
|
|
43
|
+
for job_id, task in tasks.items()
|
|
44
|
+
}
|
|
45
|
+
tasks_file.write_text(
|
|
46
|
+
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
except OSError as exception:
|
|
50
|
+
logger.error(f"保存任务文件失败: {exception}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cron_to_kwargs(cron: str) -> dict[str, str]:
|
|
54
|
+
parts = cron.split()
|
|
55
|
+
if len(parts) != 5:
|
|
56
|
+
raise ValueError("cron 格式错误,需要 5 段表达式")
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"minute": parts[0],
|
|
60
|
+
"hour": parts[1],
|
|
61
|
+
"day": parts[2],
|
|
62
|
+
"month": parts[3],
|
|
63
|
+
"day_of_week": parts[4],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _resolve_bot() -> Bot | None:
|
|
68
|
+
bot = next(
|
|
69
|
+
(
|
|
70
|
+
candidate
|
|
71
|
+
for candidate in nonebot.get_bots().values()
|
|
72
|
+
if isinstance(candidate, Bot)
|
|
73
|
+
),
|
|
74
|
+
None,
|
|
75
|
+
)
|
|
76
|
+
if bot is None:
|
|
77
|
+
logger.warning("当前没有可用的 OneBot V11 Bot,任务已跳过")
|
|
78
|
+
return bot
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def change_avatar_job(task: ScheduleTask, bot: Bot) -> tuple[bool, str]:
|
|
82
|
+
try:
|
|
83
|
+
upload_payload: str | None = None
|
|
84
|
+
if task.image_path:
|
|
85
|
+
image_path = Path(task.image_path)
|
|
86
|
+
if image_path.exists():
|
|
87
|
+
base64_str = await image_to_base64(image_path)
|
|
88
|
+
upload_payload = f"base64://{base64_str}"
|
|
89
|
+
else:
|
|
90
|
+
message = f"任务 {task.job_id} 的图片不存在: {task.image_path}"
|
|
91
|
+
logger.warning(message)
|
|
92
|
+
return False, message
|
|
93
|
+
|
|
94
|
+
if task.target_type == "self":
|
|
95
|
+
if upload_payload is not None:
|
|
96
|
+
await bot.call_api("set_qq_avatar", file=upload_payload)
|
|
97
|
+
if task.new_name:
|
|
98
|
+
await bot.call_api("set_qq_profile", nickname=task.new_name)
|
|
99
|
+
elif task.target_type == "group" and task.target_id is not None:
|
|
100
|
+
if upload_payload is not None:
|
|
101
|
+
await bot.call_api(
|
|
102
|
+
"set_group_portrait",
|
|
103
|
+
group_id=task.target_id,
|
|
104
|
+
file=upload_payload,
|
|
105
|
+
)
|
|
106
|
+
if task.new_name:
|
|
107
|
+
await bot.call_api(
|
|
108
|
+
"set_group_name",
|
|
109
|
+
group_id=task.target_id,
|
|
110
|
+
group_name=task.new_name,
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
message = f"任务 {task.job_id} 的目标配置无效,已跳过执行"
|
|
114
|
+
logger.warning(message)
|
|
115
|
+
return False, message
|
|
116
|
+
|
|
117
|
+
success_message = f"定时任务执行成功: {task.job_id}"
|
|
118
|
+
logger.success(success_message)
|
|
119
|
+
return True, success_message
|
|
120
|
+
except ActionFailed as exception:
|
|
121
|
+
message = f"任务 {task.job_id} 调用 API 失败: {exception}"
|
|
122
|
+
logger.error(message)
|
|
123
|
+
return False, message
|
|
124
|
+
except Exception as exception:
|
|
125
|
+
message = f"任务 {task.job_id} 执行异常: {exception}"
|
|
126
|
+
logger.exception(message)
|
|
127
|
+
return False, message
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def _run_task(job_id: str) -> None:
|
|
131
|
+
task = tasks.get(job_id)
|
|
132
|
+
if task is None:
|
|
133
|
+
logger.warning(f"未找到任务 ID: {job_id}")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
bot = await _resolve_bot()
|
|
137
|
+
if bot is None:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
await change_avatar_job(task, bot)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def run_task_now(task: ScheduleTask) -> tuple[bool, str]:
|
|
144
|
+
bot = await _resolve_bot()
|
|
145
|
+
if bot is None:
|
|
146
|
+
return False, "当前没有可用的 OneBot V11 Bot"
|
|
147
|
+
|
|
148
|
+
return await change_avatar_job(task, bot)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_job(task: ScheduleTask) -> None:
|
|
152
|
+
cron_kwargs = _cron_to_kwargs(task.cron)
|
|
153
|
+
tasks[task.job_id] = task
|
|
154
|
+
scheduler.add_job(
|
|
155
|
+
_run_task,
|
|
156
|
+
"cron",
|
|
157
|
+
id=task.job_id,
|
|
158
|
+
args=[task.job_id],
|
|
159
|
+
replace_existing=True,
|
|
160
|
+
**cron_kwargs,
|
|
161
|
+
)
|
|
162
|
+
save_tasks()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def remove_job(job_id: str) -> bool:
|
|
166
|
+
task = tasks.pop(job_id, None)
|
|
167
|
+
if task is None:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
scheduler.remove_job(job_id)
|
|
172
|
+
except Exception as exception:
|
|
173
|
+
logger.warning(f"移除调度任务失败: {job_id} | error={exception}")
|
|
174
|
+
|
|
175
|
+
save_tasks()
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def init_scheduler() -> None:
|
|
180
|
+
tasks.clear()
|
|
181
|
+
tasks.update(load_tasks())
|
|
182
|
+
|
|
183
|
+
restored_count = 0
|
|
184
|
+
for task in tasks.values():
|
|
185
|
+
try:
|
|
186
|
+
cron_kwargs = _cron_to_kwargs(task.cron)
|
|
187
|
+
scheduler.add_job(
|
|
188
|
+
_run_task,
|
|
189
|
+
"cron",
|
|
190
|
+
id=task.job_id,
|
|
191
|
+
args=[task.job_id],
|
|
192
|
+
replace_existing=True,
|
|
193
|
+
**cron_kwargs,
|
|
194
|
+
)
|
|
195
|
+
restored_count += 1
|
|
196
|
+
except Exception as exception:
|
|
197
|
+
logger.error(f"恢复任务失败: {task.job_id} | error={exception}")
|
|
198
|
+
|
|
199
|
+
logger.info(f"头像管理器定时任务已恢复,共 {restored_count} 个任务")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def cleanup_temp_files() -> None:
|
|
203
|
+
if not TEMP_DIR.exists():
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
for path in TEMP_DIR.iterdir():
|
|
207
|
+
if not path.is_file():
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
await asyncio.to_thread(path.unlink)
|
|
212
|
+
except OSError as exception:
|
|
213
|
+
logger.warning(f"清理临时文件失败: {path} | error={exception}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from nonebot import logger
|
|
8
|
+
|
|
9
|
+
TEMP_DIR = Path("data/avatar_manager/temp")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def download_image(url: str) -> Path | None:
|
|
13
|
+
"""下载图片并保存到 data/temp 目录,失败时返回 None。"""
|
|
14
|
+
try:
|
|
15
|
+
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
suffix = Path(url.split("?", maxsplit=1)[0]).suffix or ".jpg"
|
|
17
|
+
|
|
18
|
+
async with httpx.AsyncClient(
|
|
19
|
+
timeout=httpx.Timeout(20.0, connect=10.0),
|
|
20
|
+
follow_redirects=True,
|
|
21
|
+
) as client:
|
|
22
|
+
response = await client.get(url)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
|
|
25
|
+
def _write_file() -> Path:
|
|
26
|
+
with tempfile.NamedTemporaryFile(
|
|
27
|
+
dir=TEMP_DIR,
|
|
28
|
+
prefix="avatar_",
|
|
29
|
+
suffix=suffix,
|
|
30
|
+
delete=False,
|
|
31
|
+
) as temp_file:
|
|
32
|
+
temp_file.write(response.content)
|
|
33
|
+
return Path(temp_file.name)
|
|
34
|
+
|
|
35
|
+
return await asyncio.to_thread(_write_file)
|
|
36
|
+
except httpx.HTTPError as exception:
|
|
37
|
+
logger.error(f"下载图片失败: {exception}")
|
|
38
|
+
except OSError as exception:
|
|
39
|
+
logger.error(f"写入图片文件失败: {exception}")
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def image_to_base64(image_path: Path) -> str:
|
|
45
|
+
image_bytes = await asyncio.to_thread(image_path.read_bytes)
|
|
46
|
+
return base64.b64encode(image_bytes).decode("utf-8")
|