nonebot-plugin-sublike 0.1.2__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_sublike-0.1.2/PKG-INFO +151 -0
- nonebot_plugin_sublike-0.1.2/README.md +135 -0
- nonebot_plugin_sublike-0.1.2/pyproject.toml +69 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/__init__.py +22 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/config.py +44 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/matcher.py +278 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/models.py +80 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/scheduler.py +46 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/service.py +355 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/store.py +164 -0
- nonebot_plugin_sublike-0.1.2/src/nonebot_plugin_sublike/utils.py +32 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nonebot-plugin-sublike
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: 一个基于 `OneBot v11` 的 NoneBot2 QQ 点赞插件。
|
|
5
|
+
Author: ByteColtX
|
|
6
|
+
Author-email: ByteColtX <ByteColtX@gmail.com>
|
|
7
|
+
Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
|
|
8
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
|
|
9
|
+
Requires-Dist: nonebot2>=2.4.0,<3.0.0
|
|
10
|
+
Requires-Dist: nonebot-adapter-onebot>=2.4.0,<3.0.0
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Project-URL: Homepage, https://github.com/ByteColtX/nonebot-plugin-sublike
|
|
13
|
+
Project-URL: Issues, https://github.com/ByteColtX/nonebot-plugin-sublike/issues
|
|
14
|
+
Project-URL: Repository, https://github.com/ByteColtX/nonebot-plugin-sublike.git
|
|
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"></a>
|
|
20
|
+
|
|
21
|
+
## ✨ nonebot-plugin-sublike ✨
|
|
22
|
+
[](./LICENSE)
|
|
23
|
+
[](https://pypi.python.org/pypi/nonebot-plugin-sublike)
|
|
24
|
+
[](https://www.python.org)
|
|
25
|
+
[](https://github.com/astral-sh/uv)
|
|
26
|
+
<br/>
|
|
27
|
+
[](https://github.com/astral-sh/ruff)
|
|
28
|
+
[](https://results.pre-commit.ci/latest/github/ByteColtX/nonebot-plugin-sublike/master)
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
## 📖 介绍
|
|
33
|
+
|
|
34
|
+
一个基于 `OneBot v11` 的 NoneBot2 QQ 点赞插件。
|
|
35
|
+
|
|
36
|
+
当前已支持:
|
|
37
|
+
|
|
38
|
+
- 即时点赞自己
|
|
39
|
+
- 即时点赞群内其他人
|
|
40
|
+
- 订阅定时点赞
|
|
41
|
+
- 订阅续期、取消订阅、订阅状态查询
|
|
42
|
+
- 按运行时段轮询订阅用户,并在执行前加入随机延迟
|
|
43
|
+
|
|
44
|
+
此外还提供以下个性化配置能力:
|
|
45
|
+
|
|
46
|
+
- 自定义“赞我”“赞他”“订阅赞”等触发词
|
|
47
|
+
- 控制即时点赞、订阅点赞是否要求好友关系
|
|
48
|
+
- 控制是否允许即时点赞他人
|
|
49
|
+
- 配置订阅有效期、扫描间隔、运行时段和最大随机延迟
|
|
50
|
+
- 配置禁用插件命令的群号列表
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## 💿 安装
|
|
54
|
+
|
|
55
|
+
<details open>
|
|
56
|
+
<summary>使用 nb-cli 安装</summary>
|
|
57
|
+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
|
58
|
+
|
|
59
|
+
nb plugin install nonebot-plugin-sublike --upgrade
|
|
60
|
+
使用 **pypi** 源安装
|
|
61
|
+
|
|
62
|
+
nb plugin install nonebot-plugin-sublike --upgrade -i "https://pypi.org/simple"
|
|
63
|
+
使用**清华源**安装
|
|
64
|
+
|
|
65
|
+
nb plugin install nonebot-plugin-sublike --upgrade -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
</details>
|
|
69
|
+
|
|
70
|
+
<details>
|
|
71
|
+
<summary>使用包管理器安装</summary>
|
|
72
|
+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
|
|
73
|
+
|
|
74
|
+
<details open>
|
|
75
|
+
<summary>uv</summary>
|
|
76
|
+
|
|
77
|
+
uv add nonebot-plugin-sublike
|
|
78
|
+
安装仓库 master 分支
|
|
79
|
+
|
|
80
|
+
uv add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary>pdm</summary>
|
|
85
|
+
|
|
86
|
+
pdm add nonebot-plugin-sublike
|
|
87
|
+
安装仓库 master 分支
|
|
88
|
+
|
|
89
|
+
pdm add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
90
|
+
</details>
|
|
91
|
+
<details>
|
|
92
|
+
<summary>poetry</summary>
|
|
93
|
+
|
|
94
|
+
poetry add nonebot-plugin-sublike
|
|
95
|
+
安装仓库 master 分支
|
|
96
|
+
|
|
97
|
+
poetry add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
|
|
101
|
+
|
|
102
|
+
plugins = ["nonebot_plugin_sublike"]
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary>使用 nbr 安装(使用 uv 管理依赖可用)</summary>
|
|
108
|
+
|
|
109
|
+
[nbr](https://github.com/fllesser/nbr) 是一个基于 uv 的 nb-cli,可以方便地管理 nonebot2
|
|
110
|
+
|
|
111
|
+
nbr plugin install nonebot-plugin-sublike
|
|
112
|
+
使用 **pypi** 源安装
|
|
113
|
+
|
|
114
|
+
nbr plugin install nonebot-plugin-sublike -i "https://pypi.org/simple"
|
|
115
|
+
使用**清华源**安装
|
|
116
|
+
|
|
117
|
+
nbr plugin install nonebot-plugin-sublike -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
118
|
+
|
|
119
|
+
</details>
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## ⚙️ 配置
|
|
123
|
+
|
|
124
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置
|
|
125
|
+
|
|
126
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
127
|
+
| :---: | :---: | :---: | :--- |
|
|
128
|
+
| `sublike_cmd_me` | 否 | `["赞我", "草我"]` | 即时点赞自己的触发词 |
|
|
129
|
+
| `sublike_cmd_other` | 否 | `["赞ta","赞他"]` | 即时点赞他人的触发词 |
|
|
130
|
+
| `sublike_cmd_sub` | 否 | `["订阅赞", "天天赞我"]` | 订阅赞触发词 |
|
|
131
|
+
| `sublike_cmd_unsub` | 否 | `["取消订阅赞"]` | 取消订阅触发词 |
|
|
132
|
+
| `sublike_cmd_status` | 否 | `["查询订阅赞"]` | 订阅状态查询触发词 |
|
|
133
|
+
| `sublike_need_friend_me` | 否 | `false` | 即时点赞是否要求好友关系 |
|
|
134
|
+
| `sublike_need_friend_sub` | 否 | `true` | 订阅点赞是否要求好友关系 |
|
|
135
|
+
| `sublike_allow_other` | 否 | `true` | 是否允许即时点赞他人 |
|
|
136
|
+
| `sublike_sub_expire_days` | 否 | `7` | 订阅有效期天数,需在过期前再次触发订阅命令续期 |
|
|
137
|
+
| `sublike_sched_interval` | 否 | `60` | 定时扫描间隔,单位分钟 |
|
|
138
|
+
| `sublike_sched_start` | 否 | `8` | 定时任务开始小时 |
|
|
139
|
+
| `sublike_sched_end` | 否 | `0` | 定时任务结束小时,`0` 表示次日 `00:00` |
|
|
140
|
+
| `sublike_delay_max` | 否 | `2` | 单个订阅用户执行前的最大随机延迟,单位分钟 |
|
|
141
|
+
| `sublike_banned_groups` | 否 | `[]` | 禁用插件命令的群号列表 |
|
|
142
|
+
|
|
143
|
+
## 🎉 使用
|
|
144
|
+
### 指令表
|
|
145
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
146
|
+
| :---: | :---: | :---: | :---: | :--- |
|
|
147
|
+
| `赞我` | 群员 | 否 | 群聊 / 私聊 | 给发送者点赞直到当日上限 |
|
|
148
|
+
| `赞他 @用户` | 群员 | 否 | 群聊 | 给目标用户点赞直到当日上限 |
|
|
149
|
+
| `订阅赞` / `天天赞我` | 群员 | 否 | 群聊 / 私聊 | 创建或续期订阅赞 |
|
|
150
|
+
| `取消订阅赞` | 群员 | 否 | 群聊 / 私聊 | 取消自己的订阅赞 |
|
|
151
|
+
| `查询订阅赞` | 群员 | 否 | 群聊 / 私聊 | 普通用户查看自己的订阅状态,`SUPERUSERS` 可查看全部有效订阅 |
|
|
@@ -0,0 +1,135 @@
|
|
|
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"></a>
|
|
4
|
+
|
|
5
|
+
## ✨ nonebot-plugin-sublike ✨
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://pypi.python.org/pypi/nonebot-plugin-sublike)
|
|
8
|
+
[](https://www.python.org)
|
|
9
|
+
[](https://github.com/astral-sh/uv)
|
|
10
|
+
<br/>
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
[](https://results.pre-commit.ci/latest/github/ByteColtX/nonebot-plugin-sublike/master)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## 📖 介绍
|
|
17
|
+
|
|
18
|
+
一个基于 `OneBot v11` 的 NoneBot2 QQ 点赞插件。
|
|
19
|
+
|
|
20
|
+
当前已支持:
|
|
21
|
+
|
|
22
|
+
- 即时点赞自己
|
|
23
|
+
- 即时点赞群内其他人
|
|
24
|
+
- 订阅定时点赞
|
|
25
|
+
- 订阅续期、取消订阅、订阅状态查询
|
|
26
|
+
- 按运行时段轮询订阅用户,并在执行前加入随机延迟
|
|
27
|
+
|
|
28
|
+
此外还提供以下个性化配置能力:
|
|
29
|
+
|
|
30
|
+
- 自定义“赞我”“赞他”“订阅赞”等触发词
|
|
31
|
+
- 控制即时点赞、订阅点赞是否要求好友关系
|
|
32
|
+
- 控制是否允许即时点赞他人
|
|
33
|
+
- 配置订阅有效期、扫描间隔、运行时段和最大随机延迟
|
|
34
|
+
- 配置禁用插件命令的群号列表
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## 💿 安装
|
|
38
|
+
|
|
39
|
+
<details open>
|
|
40
|
+
<summary>使用 nb-cli 安装</summary>
|
|
41
|
+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
|
42
|
+
|
|
43
|
+
nb plugin install nonebot-plugin-sublike --upgrade
|
|
44
|
+
使用 **pypi** 源安装
|
|
45
|
+
|
|
46
|
+
nb plugin install nonebot-plugin-sublike --upgrade -i "https://pypi.org/simple"
|
|
47
|
+
使用**清华源**安装
|
|
48
|
+
|
|
49
|
+
nb plugin install nonebot-plugin-sublike --upgrade -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
</details>
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary>使用包管理器安装</summary>
|
|
56
|
+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
|
|
57
|
+
|
|
58
|
+
<details open>
|
|
59
|
+
<summary>uv</summary>
|
|
60
|
+
|
|
61
|
+
uv add nonebot-plugin-sublike
|
|
62
|
+
安装仓库 master 分支
|
|
63
|
+
|
|
64
|
+
uv add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary>pdm</summary>
|
|
69
|
+
|
|
70
|
+
pdm add nonebot-plugin-sublike
|
|
71
|
+
安装仓库 master 分支
|
|
72
|
+
|
|
73
|
+
pdm add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
74
|
+
</details>
|
|
75
|
+
<details>
|
|
76
|
+
<summary>poetry</summary>
|
|
77
|
+
|
|
78
|
+
poetry add nonebot-plugin-sublike
|
|
79
|
+
安装仓库 master 分支
|
|
80
|
+
|
|
81
|
+
poetry add git+https://github.com/ByteColtX/nonebot-plugin-sublike@master
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
|
|
85
|
+
|
|
86
|
+
plugins = ["nonebot_plugin_sublike"]
|
|
87
|
+
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary>使用 nbr 安装(使用 uv 管理依赖可用)</summary>
|
|
92
|
+
|
|
93
|
+
[nbr](https://github.com/fllesser/nbr) 是一个基于 uv 的 nb-cli,可以方便地管理 nonebot2
|
|
94
|
+
|
|
95
|
+
nbr plugin install nonebot-plugin-sublike
|
|
96
|
+
使用 **pypi** 源安装
|
|
97
|
+
|
|
98
|
+
nbr plugin install nonebot-plugin-sublike -i "https://pypi.org/simple"
|
|
99
|
+
使用**清华源**安装
|
|
100
|
+
|
|
101
|
+
nbr plugin install nonebot-plugin-sublike -i "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## ⚙️ 配置
|
|
107
|
+
|
|
108
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置
|
|
109
|
+
|
|
110
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
111
|
+
| :---: | :---: | :---: | :--- |
|
|
112
|
+
| `sublike_cmd_me` | 否 | `["赞我", "草我"]` | 即时点赞自己的触发词 |
|
|
113
|
+
| `sublike_cmd_other` | 否 | `["赞ta","赞他"]` | 即时点赞他人的触发词 |
|
|
114
|
+
| `sublike_cmd_sub` | 否 | `["订阅赞", "天天赞我"]` | 订阅赞触发词 |
|
|
115
|
+
| `sublike_cmd_unsub` | 否 | `["取消订阅赞"]` | 取消订阅触发词 |
|
|
116
|
+
| `sublike_cmd_status` | 否 | `["查询订阅赞"]` | 订阅状态查询触发词 |
|
|
117
|
+
| `sublike_need_friend_me` | 否 | `false` | 即时点赞是否要求好友关系 |
|
|
118
|
+
| `sublike_need_friend_sub` | 否 | `true` | 订阅点赞是否要求好友关系 |
|
|
119
|
+
| `sublike_allow_other` | 否 | `true` | 是否允许即时点赞他人 |
|
|
120
|
+
| `sublike_sub_expire_days` | 否 | `7` | 订阅有效期天数,需在过期前再次触发订阅命令续期 |
|
|
121
|
+
| `sublike_sched_interval` | 否 | `60` | 定时扫描间隔,单位分钟 |
|
|
122
|
+
| `sublike_sched_start` | 否 | `8` | 定时任务开始小时 |
|
|
123
|
+
| `sublike_sched_end` | 否 | `0` | 定时任务结束小时,`0` 表示次日 `00:00` |
|
|
124
|
+
| `sublike_delay_max` | 否 | `2` | 单个订阅用户执行前的最大随机延迟,单位分钟 |
|
|
125
|
+
| `sublike_banned_groups` | 否 | `[]` | 禁用插件命令的群号列表 |
|
|
126
|
+
|
|
127
|
+
## 🎉 使用
|
|
128
|
+
### 指令表
|
|
129
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
130
|
+
| :---: | :---: | :---: | :---: | :--- |
|
|
131
|
+
| `赞我` | 群员 | 否 | 群聊 / 私聊 | 给发送者点赞直到当日上限 |
|
|
132
|
+
| `赞他 @用户` | 群员 | 否 | 群聊 | 给目标用户点赞直到当日上限 |
|
|
133
|
+
| `订阅赞` / `天天赞我` | 群员 | 否 | 群聊 / 私聊 | 创建或续期订阅赞 |
|
|
134
|
+
| `取消订阅赞` | 群员 | 否 | 群聊 / 私聊 | 取消自己的订阅赞 |
|
|
135
|
+
| `查询订阅赞` | 群员 | 否 | 群聊 / 私聊 | 普通用户查看自己的订阅状态,`SUPERUSERS` 可查看全部有效订阅 |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nonebot-plugin-sublike"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "一个基于 `OneBot v11` 的 NoneBot2 QQ 点赞插件。"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [{ name = "ByteColtX", email = "ByteColtX@gmail.com" }]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"nonebot-plugin-apscheduler>=0.5.0,<1.0.0", # 定时任务
|
|
10
|
+
"nonebot-plugin-localstore>=0.7.4,<1.0.0", # 存储文件
|
|
11
|
+
"nonebot2>=2.4.0,<3.0.0",
|
|
12
|
+
"nonebot-adapter-onebot>=2.4.0,<3.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/ByteColtX/nonebot-plugin-sublike"
|
|
17
|
+
Issues = "https://github.com/ByteColtX/nonebot-plugin-sublike/issues"
|
|
18
|
+
Repository = "https://github.com/ByteColtX/nonebot-plugin-sublike.git"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"bump-my-version>=1.2.7",
|
|
23
|
+
"ruff>=0.15.2,<1.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.10.0,<0.11.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
[tool.bumpversion]
|
|
32
|
+
current_version = "0.1.0"
|
|
33
|
+
commit = true
|
|
34
|
+
message = "release: bump vesion from {current_version} to {new_version}"
|
|
35
|
+
tag = true
|
|
36
|
+
|
|
37
|
+
[[tool.bumpversion.files]]
|
|
38
|
+
filename = "uv.lock"
|
|
39
|
+
search = "name = \"nonebot-plugin-sublike\"\nversion = \"{current_version}\""
|
|
40
|
+
replace = "name = \"nonebot-plugin-sublike\"\nversion = \"{new_version}\""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
[tool.nonebot]
|
|
44
|
+
plugins = ["nonebot_plugin_sublike"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 88
|
|
48
|
+
|
|
49
|
+
[tool.ruff.format]
|
|
50
|
+
line-ending = "lf"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = [
|
|
54
|
+
"F", # Pyflakes
|
|
55
|
+
"W", # pycodestyle warnings
|
|
56
|
+
"E", # pycodestyle errors
|
|
57
|
+
"I", # isort
|
|
58
|
+
"UP", # pyupgrade
|
|
59
|
+
"RUF", # Ruff-specific rules
|
|
60
|
+
]
|
|
61
|
+
ignore = [
|
|
62
|
+
"E402", # module-import-not-at-top-of-file
|
|
63
|
+
"RUF001", # ambiguous-unicode-character-string
|
|
64
|
+
"RUF002", # ambiguous-unicode-character-docstring
|
|
65
|
+
"RUF003", # ambiguous-unicode-character-comment
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.pyupgrade]
|
|
69
|
+
keep-runtime-typing = true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from nonebot import require
|
|
2
|
+
from nonebot.plugin import PluginMetadata
|
|
3
|
+
|
|
4
|
+
require("nonebot_plugin_apscheduler")
|
|
5
|
+
require("nonebot_plugin_localstore")
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .matcher import like_me
|
|
9
|
+
from .scheduler import subscription_scan_job
|
|
10
|
+
|
|
11
|
+
__plugin_meta__ = PluginMetadata(
|
|
12
|
+
name="QQ点赞",
|
|
13
|
+
description="QQ点赞、订阅赞",
|
|
14
|
+
usage="赞我|赞他|订阅赞|取消订阅赞|订阅列表查询",
|
|
15
|
+
type="application",
|
|
16
|
+
homepage="https://github.com/ByteColtX/nonebot-plugin-sublike",
|
|
17
|
+
config=Config,
|
|
18
|
+
supported_adapters={"~onebot.v11"},
|
|
19
|
+
extra={"author": "ByteColtX <umk@live.com>"},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = ["__plugin_meta__", "like_me", "subscription_scan_job"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""插件配置。"""
|
|
2
|
+
|
|
3
|
+
from nonebot import get_plugin_config
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config(BaseModel):
|
|
8
|
+
"""插件配置模型。"""
|
|
9
|
+
|
|
10
|
+
sublike_cmd_me: tuple[str, ...] = ("赞我", "草我", "超我")
|
|
11
|
+
sublike_cmd_sub: tuple[str, ...] = ("订阅赞", "天天赞我")
|
|
12
|
+
sublike_cmd_unsub: tuple[str, ...] = ("取消订阅赞", "订阅赞取消")
|
|
13
|
+
sublike_cmd_status: tuple[str, ...] = (
|
|
14
|
+
"订阅赞查看",
|
|
15
|
+
"查看订阅赞",
|
|
16
|
+
"订阅赞状态",
|
|
17
|
+
"订阅赞查询",
|
|
18
|
+
"查询订阅赞",
|
|
19
|
+
)
|
|
20
|
+
sublike_cmd_other: tuple[str, ...] = (
|
|
21
|
+
"赞ta",
|
|
22
|
+
"赞TA",
|
|
23
|
+
"赞他",
|
|
24
|
+
"赞她",
|
|
25
|
+
"草ta",
|
|
26
|
+
"草TA",
|
|
27
|
+
"草他",
|
|
28
|
+
"草她",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
sublike_need_friend_me: bool = False
|
|
32
|
+
sublike_need_friend_sub: bool = True
|
|
33
|
+
sublike_allow_other: bool = True
|
|
34
|
+
|
|
35
|
+
sublike_sub_expire_days: int = Field(default=7, ge=1)
|
|
36
|
+
sublike_sched_interval: int = Field(default=60, ge=1)
|
|
37
|
+
sublike_sched_start: int = Field(default=8, ge=0, le=23)
|
|
38
|
+
sublike_sched_end: int = Field(default=0, ge=0, le=23)
|
|
39
|
+
sublike_delay_max: int = Field(default=2, ge=0)
|
|
40
|
+
|
|
41
|
+
sublike_banned_groups: tuple[int, ...] = ()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
plugin_config = get_plugin_config(Config)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""消息触发器。"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from nonebot import on_message
|
|
6
|
+
from nonebot.adapters.onebot.v11 import (
|
|
7
|
+
Bot,
|
|
8
|
+
GroupMessageEvent,
|
|
9
|
+
Message,
|
|
10
|
+
MessageEvent,
|
|
11
|
+
MessageSegment,
|
|
12
|
+
)
|
|
13
|
+
from nonebot.rule import Rule
|
|
14
|
+
|
|
15
|
+
from .config import plugin_config
|
|
16
|
+
from .models import (
|
|
17
|
+
LikeResult,
|
|
18
|
+
LikeSource,
|
|
19
|
+
LikeStatus,
|
|
20
|
+
SubscriptionResult,
|
|
21
|
+
SubscriptionStatus,
|
|
22
|
+
)
|
|
23
|
+
from .service import (
|
|
24
|
+
handle_instant_like,
|
|
25
|
+
handle_subscribe,
|
|
26
|
+
handle_subscription_status,
|
|
27
|
+
handle_unsubscribe,
|
|
28
|
+
is_superuser,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
QQ_RE = re.compile(r"\b[1-9]\d{5,11}\b")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_banned_group(event: MessageEvent) -> bool:
|
|
35
|
+
"""判断当前消息是否来自被禁用的群。"""
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
isinstance(event, GroupMessageEvent)
|
|
39
|
+
and event.group_id in plugin_config.sublike_banned_groups
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_like_me(event: MessageEvent) -> bool:
|
|
44
|
+
"""判断消息是否为“赞我”命令。"""
|
|
45
|
+
|
|
46
|
+
if _is_banned_group(event):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
plain_text = event.get_plaintext().strip()
|
|
50
|
+
return plain_text in plugin_config.sublike_cmd_me
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_like_other(event: MessageEvent) -> bool:
|
|
54
|
+
"""判断消息是否为“赞他人”命令。"""
|
|
55
|
+
|
|
56
|
+
if not plugin_config.sublike_allow_other:
|
|
57
|
+
return False
|
|
58
|
+
if not isinstance(event, GroupMessageEvent):
|
|
59
|
+
return False
|
|
60
|
+
if _is_banned_group(event):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
plain_text = event.get_plaintext().strip()
|
|
64
|
+
return any(
|
|
65
|
+
plain_text.startswith(keyword) for keyword in plugin_config.sublike_cmd_other
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_subscribe(event: MessageEvent) -> bool:
|
|
70
|
+
"""判断消息是否为订阅命令。"""
|
|
71
|
+
|
|
72
|
+
if _is_banned_group(event):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
plain_text = event.get_plaintext().strip()
|
|
76
|
+
return plain_text in plugin_config.sublike_cmd_sub
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_unsubscribe(event: MessageEvent) -> bool:
|
|
80
|
+
"""判断消息是否为取消订阅命令。"""
|
|
81
|
+
|
|
82
|
+
if _is_banned_group(event):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
plain_text = event.get_plaintext().strip()
|
|
86
|
+
return plain_text in plugin_config.sublike_cmd_unsub
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_subscription_status(event: MessageEvent) -> bool:
|
|
90
|
+
"""判断消息是否为订阅状态查询命令。"""
|
|
91
|
+
|
|
92
|
+
if _is_banned_group(event):
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
plain_text = event.get_plaintext().strip()
|
|
96
|
+
return plain_text in plugin_config.sublike_cmd_status
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_target_user_id(event: GroupMessageEvent) -> int | None:
|
|
100
|
+
"""从群消息中提取被点赞的目标 QQ 号。"""
|
|
101
|
+
|
|
102
|
+
for segment in event.get_message():
|
|
103
|
+
if segment.type != "at":
|
|
104
|
+
continue
|
|
105
|
+
qq = segment.data.get("qq")
|
|
106
|
+
if isinstance(qq, str) and qq.isdigit():
|
|
107
|
+
return int(qq)
|
|
108
|
+
|
|
109
|
+
plain_text = event.get_plaintext().strip()
|
|
110
|
+
match = QQ_RE.search(plain_text)
|
|
111
|
+
if match is None:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return int(match.group(0))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_like_me_message(result: LikeResult) -> str:
|
|
118
|
+
"""生成“赞我”回复文案。"""
|
|
119
|
+
|
|
120
|
+
if result.status == LikeStatus.NOT_FRIEND:
|
|
121
|
+
return "⚠️ 需要先加好友才能点赞"
|
|
122
|
+
if result.status == LikeStatus.SUCCESS:
|
|
123
|
+
return f"👍 已经给你点了 {result.total} 个赞"
|
|
124
|
+
if result.status == LikeStatus.LIMIT_REACHED:
|
|
125
|
+
return "🌟 今天赞不了你更多了喵~"
|
|
126
|
+
return "💥 点赞失败了喵~"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_like_other_message(
|
|
130
|
+
target_user_id: int,
|
|
131
|
+
result: LikeResult,
|
|
132
|
+
) -> Message | str:
|
|
133
|
+
"""生成“赞他”回复文案。"""
|
|
134
|
+
|
|
135
|
+
if result.status == LikeStatus.NOT_FRIEND:
|
|
136
|
+
return Message(
|
|
137
|
+
[
|
|
138
|
+
MessageSegment.text("⚠️ 请先让 "),
|
|
139
|
+
MessageSegment.at(target_user_id),
|
|
140
|
+
MessageSegment.text(" 添加机器人为好友后再点赞"),
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if result.status == LikeStatus.SUCCESS:
|
|
145
|
+
return Message(
|
|
146
|
+
[
|
|
147
|
+
MessageSegment.text("👍 已经给 "),
|
|
148
|
+
MessageSegment.at(target_user_id),
|
|
149
|
+
MessageSegment.text(f" 点了 {result.total} 个赞"),
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if result.status == LikeStatus.LIMIT_REACHED:
|
|
154
|
+
return Message(
|
|
155
|
+
[
|
|
156
|
+
MessageSegment.text("🌟 今天赞不了 "),
|
|
157
|
+
MessageSegment.at(target_user_id),
|
|
158
|
+
MessageSegment.text(" 更多了喵~"),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return "💥 点赞失败了喵~"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_subscribe_message(result: SubscriptionResult) -> str:
|
|
166
|
+
"""生成订阅命令回复文案。"""
|
|
167
|
+
|
|
168
|
+
if result.status == SubscriptionStatus.RENEWED:
|
|
169
|
+
if result.require_friend and result.is_friend is False:
|
|
170
|
+
return "🔁 订阅赞已续期,但当前你还不是机器人好友,定时点赞可能不会生效"
|
|
171
|
+
return "🔁 订阅赞已续期"
|
|
172
|
+
|
|
173
|
+
if result.status == SubscriptionStatus.SUBSCRIBED:
|
|
174
|
+
if result.require_friend and result.is_friend is False:
|
|
175
|
+
return "👍 订阅赞成功,但当前你还不是机器人好友,定时点赞可能不会生效"
|
|
176
|
+
return "👍 订阅赞成功"
|
|
177
|
+
|
|
178
|
+
return "💥 订阅处理失败"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_unsubscribe_message(result: SubscriptionResult) -> str:
|
|
182
|
+
"""生成取消订阅回复文案。"""
|
|
183
|
+
|
|
184
|
+
if result.status == SubscriptionStatus.UNSUBSCRIBED:
|
|
185
|
+
return "👎 已取消订阅赞"
|
|
186
|
+
return "💢 你当前没有订阅赞"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_status_message(result: SubscriptionResult) -> str:
|
|
190
|
+
"""生成订阅状态回复文案。"""
|
|
191
|
+
|
|
192
|
+
if result.status == SubscriptionStatus.EMPTY:
|
|
193
|
+
if result.is_superuser_view:
|
|
194
|
+
return "📭 当前没有有效订阅"
|
|
195
|
+
return "📭 你当前没有有效订阅"
|
|
196
|
+
|
|
197
|
+
if result.status == SubscriptionStatus.STATUS_LIST:
|
|
198
|
+
lines = ["📋 当前有效订阅:"]
|
|
199
|
+
for record in result.records:
|
|
200
|
+
lines.append(f"{record.user_id} 到期于 {record.expires_at:%Y-%m-%d}")
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
if result.status == SubscriptionStatus.STATUS_SINGLE and result.record is not None:
|
|
204
|
+
lines = [
|
|
205
|
+
"📌 你的订阅状态:",
|
|
206
|
+
f"QQ:{result.record.user_id}",
|
|
207
|
+
f"到期时间:{result.record.expires_at:%Y-%m-%d}",
|
|
208
|
+
]
|
|
209
|
+
if result.record.last_like_at is not None:
|
|
210
|
+
lines.append(f"最近点赞:{result.record.last_like_at:%Y-%m-%d}")
|
|
211
|
+
else:
|
|
212
|
+
lines.append("最近点赞:暂无")
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
return "💥 查询订阅状态失败"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
like_me = on_message(rule=Rule(is_like_me), priority=5, block=True)
|
|
219
|
+
like_other = on_message(rule=Rule(is_like_other), priority=5, block=True)
|
|
220
|
+
like_subscribe = on_message(rule=Rule(is_subscribe), priority=5, block=True)
|
|
221
|
+
like_unsubscribe = on_message(rule=Rule(is_unsubscribe), priority=5, block=True)
|
|
222
|
+
like_status = on_message(
|
|
223
|
+
rule=Rule(is_subscription_status),
|
|
224
|
+
priority=5,
|
|
225
|
+
block=True,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@like_me.handle()
|
|
230
|
+
async def handle_like_me(bot: Bot, event: MessageEvent):
|
|
231
|
+
"""处理“赞我”命令。"""
|
|
232
|
+
|
|
233
|
+
result = await handle_instant_like(bot, event.user_id)
|
|
234
|
+
await like_me.finish(build_like_me_message(result))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@like_other.handle()
|
|
238
|
+
async def handle_like_other(bot: Bot, event: GroupMessageEvent):
|
|
239
|
+
"""处理“赞他人”命令。"""
|
|
240
|
+
|
|
241
|
+
target_user_id = extract_target_user_id(event)
|
|
242
|
+
if target_user_id is None:
|
|
243
|
+
await like_other.finish("🤡 请提供有效的 QQ 号或 @目标用户")
|
|
244
|
+
|
|
245
|
+
result = await handle_instant_like(
|
|
246
|
+
bot,
|
|
247
|
+
target_user_id,
|
|
248
|
+
source=LikeSource.INSTANT,
|
|
249
|
+
)
|
|
250
|
+
reply = build_like_other_message(target_user_id, result)
|
|
251
|
+
await like_other.finish(reply)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@like_subscribe.handle()
|
|
255
|
+
async def handle_like_subscribe(bot: Bot, event: MessageEvent):
|
|
256
|
+
"""处理订阅命令。"""
|
|
257
|
+
|
|
258
|
+
result = await handle_subscribe(bot, event.user_id)
|
|
259
|
+
await like_subscribe.finish(build_subscribe_message(result))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@like_unsubscribe.handle()
|
|
263
|
+
async def handle_like_unsubscribe(event: MessageEvent):
|
|
264
|
+
"""处理取消订阅命令。"""
|
|
265
|
+
|
|
266
|
+
result = handle_unsubscribe(event.user_id)
|
|
267
|
+
await like_unsubscribe.finish(build_unsubscribe_message(result))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@like_status.handle()
|
|
271
|
+
async def handle_like_status(event: MessageEvent):
|
|
272
|
+
"""处理订阅状态查询命令。"""
|
|
273
|
+
|
|
274
|
+
result = handle_subscription_status(
|
|
275
|
+
event.user_id,
|
|
276
|
+
is_superuser(event.user_id),
|
|
277
|
+
)
|
|
278
|
+
await like_status.finish(build_status_message(result))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""数据模型。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LikeSource(str, Enum):
|
|
10
|
+
"""点赞来源。"""
|
|
11
|
+
|
|
12
|
+
INSTANT = "instant"
|
|
13
|
+
SUBSCRIPTION = "subscription"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LikeStatus(str, Enum):
|
|
17
|
+
"""点赞结果状态。"""
|
|
18
|
+
|
|
19
|
+
SUCCESS = "success"
|
|
20
|
+
LIMIT_REACHED = "limit_reached"
|
|
21
|
+
NOT_FRIEND = "not_friend"
|
|
22
|
+
FAILED = "failed"
|
|
23
|
+
SKIPPED = "skipped"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SubscriptionStatus(str, Enum):
|
|
27
|
+
"""订阅操作状态。"""
|
|
28
|
+
|
|
29
|
+
SUBSCRIBED = "subscribed"
|
|
30
|
+
RENEWED = "renewed"
|
|
31
|
+
UNSUBSCRIBED = "unsubscribed"
|
|
32
|
+
NOT_SUBSCRIBED = "not_subscribed"
|
|
33
|
+
STATUS_SINGLE = "status_single"
|
|
34
|
+
STATUS_LIST = "status_list"
|
|
35
|
+
EMPTY = "empty"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SubscriptionRecord(BaseModel):
|
|
39
|
+
"""订阅记录。"""
|
|
40
|
+
|
|
41
|
+
user_id: int
|
|
42
|
+
created_at: datetime
|
|
43
|
+
last_trigger_at: datetime
|
|
44
|
+
expires_at: datetime
|
|
45
|
+
last_like_at: datetime | None = None
|
|
46
|
+
last_like_date: date | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UserLikeStats(BaseModel):
|
|
50
|
+
"""用户累计点赞统计。"""
|
|
51
|
+
|
|
52
|
+
user_id: int
|
|
53
|
+
total_like_days: int = Field(default=0, ge=0)
|
|
54
|
+
total_like_count: int = Field(default=0, ge=0)
|
|
55
|
+
last_like_date: date | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LikeResult(BaseModel):
|
|
59
|
+
"""单次点赞流程结果。"""
|
|
60
|
+
|
|
61
|
+
user_id: int
|
|
62
|
+
status: LikeStatus = LikeStatus.FAILED
|
|
63
|
+
total: int = Field(default=0, ge=0)
|
|
64
|
+
source: LikeSource = LikeSource.INSTANT
|
|
65
|
+
is_friend: bool | None = None
|
|
66
|
+
hit_limit: bool = False
|
|
67
|
+
success: bool = False
|
|
68
|
+
detail: str = ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SubscriptionResult(BaseModel):
|
|
72
|
+
"""订阅操作结果。"""
|
|
73
|
+
|
|
74
|
+
user_id: int
|
|
75
|
+
status: SubscriptionStatus
|
|
76
|
+
is_superuser_view: bool = False
|
|
77
|
+
require_friend: bool = False
|
|
78
|
+
is_friend: bool | None = None
|
|
79
|
+
record: SubscriptionRecord | None = None
|
|
80
|
+
records: list[SubscriptionRecord] = Field(default_factory=list)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""定时任务入口。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from nonebot import get_bots, logger, require
|
|
6
|
+
from nonebot.adapters.onebot.v11 import Bot
|
|
7
|
+
|
|
8
|
+
require("nonebot_plugin_apscheduler")
|
|
9
|
+
from nonebot_plugin_apscheduler import scheduler
|
|
10
|
+
|
|
11
|
+
from .config import plugin_config
|
|
12
|
+
from .service import run_subscription_scan
|
|
13
|
+
from .utils import in_active_window
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_onebot_bot() -> Bot | None:
|
|
17
|
+
"""获取可用的 OneBot v11 Bot。"""
|
|
18
|
+
|
|
19
|
+
for bot in get_bots().values():
|
|
20
|
+
if isinstance(bot, Bot):
|
|
21
|
+
return bot
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@scheduler.scheduled_job(
|
|
26
|
+
"interval",
|
|
27
|
+
minutes=plugin_config.sublike_sched_interval,
|
|
28
|
+
id="nonebot_plugin_sublike_subscription_scan",
|
|
29
|
+
)
|
|
30
|
+
async def subscription_scan_job() -> None:
|
|
31
|
+
"""定时扫描订阅用户。"""
|
|
32
|
+
|
|
33
|
+
now = datetime.now()
|
|
34
|
+
if not in_active_window(
|
|
35
|
+
now,
|
|
36
|
+
plugin_config.sublike_sched_start,
|
|
37
|
+
plugin_config.sublike_sched_end,
|
|
38
|
+
):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
bot = _get_onebot_bot()
|
|
42
|
+
if bot is None:
|
|
43
|
+
logger.warning("nonebot_plugin_sublike 未找到可用的 OneBot v11 Bot")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
await run_subscription_scan(bot)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""点赞业务逻辑。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from nonebot import get_driver, logger
|
|
7
|
+
from nonebot.adapters.onebot.v11 import Bot
|
|
8
|
+
|
|
9
|
+
from .config import plugin_config
|
|
10
|
+
from .models import (
|
|
11
|
+
LikeResult,
|
|
12
|
+
LikeSource,
|
|
13
|
+
LikeStatus,
|
|
14
|
+
SubscriptionRecord,
|
|
15
|
+
SubscriptionResult,
|
|
16
|
+
SubscriptionStatus,
|
|
17
|
+
UserLikeStats,
|
|
18
|
+
)
|
|
19
|
+
from .store import (
|
|
20
|
+
get_subscription,
|
|
21
|
+
get_user_stats,
|
|
22
|
+
load_subscriptions,
|
|
23
|
+
purge_expired_subscriptions,
|
|
24
|
+
remove_subscription,
|
|
25
|
+
upsert_subscription,
|
|
26
|
+
upsert_user_stats,
|
|
27
|
+
)
|
|
28
|
+
from .utils import get_random_delay_seconds, in_active_window, is_friend
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def check_friend(
|
|
32
|
+
bot: Bot,
|
|
33
|
+
user_id: int,
|
|
34
|
+
*,
|
|
35
|
+
require_friend: bool,
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""按配置判断是否需要好友关系。"""
|
|
38
|
+
|
|
39
|
+
if not require_friend:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
return await is_friend(bot, user_id)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_user_like_stats(user_id: int, total: int, liked_at: datetime) -> None:
|
|
46
|
+
"""更新用户累计点赞统计。"""
|
|
47
|
+
|
|
48
|
+
stats = get_user_stats(user_id)
|
|
49
|
+
if stats is None:
|
|
50
|
+
stats = UserLikeStats(user_id=user_id)
|
|
51
|
+
|
|
52
|
+
liked_date = liked_at.date()
|
|
53
|
+
if stats.last_like_date != liked_date:
|
|
54
|
+
stats.total_like_days += 1
|
|
55
|
+
|
|
56
|
+
stats.total_like_count += total
|
|
57
|
+
stats.last_like_date = liked_date
|
|
58
|
+
upsert_user_stats(stats)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_limit_response(response: object) -> bool:
|
|
62
|
+
"""判断接口返回是否表示已到点赞上限。"""
|
|
63
|
+
|
|
64
|
+
if not isinstance(response, dict):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return response.get("ok") is False or response.get("times") == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_limit_exception(exception: Exception) -> bool:
|
|
71
|
+
"""判断异常是否表示已到点赞上限。"""
|
|
72
|
+
|
|
73
|
+
text = repr(exception)
|
|
74
|
+
limit_markers = (
|
|
75
|
+
"已达上限",
|
|
76
|
+
"不能再赞",
|
|
77
|
+
"点赞失败 今日同一好友点赞数已达上限",
|
|
78
|
+
"limit",
|
|
79
|
+
)
|
|
80
|
+
return any(marker in text for marker in limit_markers)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def execute_like(bot: Bot, user_id: int, *, source: LikeSource) -> LikeResult:
|
|
84
|
+
"""执行点赞请求,直到接口拒绝继续点赞为止。"""
|
|
85
|
+
|
|
86
|
+
result = LikeResult(
|
|
87
|
+
user_id=user_id,
|
|
88
|
+
source=source,
|
|
89
|
+
status=LikeStatus.FAILED,
|
|
90
|
+
)
|
|
91
|
+
while True:
|
|
92
|
+
try:
|
|
93
|
+
response = await bot.send_like(user_id=user_id, times=10)
|
|
94
|
+
except Exception as exception:
|
|
95
|
+
if result.total > 0:
|
|
96
|
+
result.success = True
|
|
97
|
+
result.status = LikeStatus.SUCCESS
|
|
98
|
+
result.hit_limit = True
|
|
99
|
+
result.detail = repr(exception)
|
|
100
|
+
logger.info(
|
|
101
|
+
f"用户 {user_id} 点赞已到上限,本次累计 {result.total} 赞:"
|
|
102
|
+
f"{exception!r}"
|
|
103
|
+
)
|
|
104
|
+
elif _is_limit_exception(exception):
|
|
105
|
+
result.status = LikeStatus.LIMIT_REACHED
|
|
106
|
+
result.hit_limit = True
|
|
107
|
+
result.detail = repr(exception)
|
|
108
|
+
logger.info(f"用户 {user_id} 今日点赞已达上限:{exception!r}")
|
|
109
|
+
else:
|
|
110
|
+
result.status = LikeStatus.FAILED
|
|
111
|
+
result.detail = repr(exception)
|
|
112
|
+
logger.warning(f"用户 {user_id} 点赞失败:{exception!r}")
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
result.total += 10
|
|
116
|
+
|
|
117
|
+
if _is_limit_response(response):
|
|
118
|
+
result.hit_limit = True
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if result.total > 0:
|
|
122
|
+
result.success = True
|
|
123
|
+
result.status = LikeStatus.SUCCESS
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def handle_instant_like(
|
|
129
|
+
bot: Bot,
|
|
130
|
+
user_id: int,
|
|
131
|
+
*,
|
|
132
|
+
source: LikeSource = LikeSource.INSTANT,
|
|
133
|
+
) -> LikeResult:
|
|
134
|
+
"""处理即时点赞流程。"""
|
|
135
|
+
|
|
136
|
+
result = LikeResult(user_id=user_id, source=source)
|
|
137
|
+
result.is_friend = await check_friend(
|
|
138
|
+
bot,
|
|
139
|
+
user_id,
|
|
140
|
+
require_friend=plugin_config.sublike_need_friend_me,
|
|
141
|
+
)
|
|
142
|
+
if not result.is_friend:
|
|
143
|
+
result.status = LikeStatus.NOT_FRIEND
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
like_result = await execute_like(bot, user_id, source=source)
|
|
147
|
+
like_result.is_friend = result.is_friend
|
|
148
|
+
|
|
149
|
+
if like_result.success:
|
|
150
|
+
update_user_like_stats(user_id, like_result.total, datetime.now())
|
|
151
|
+
|
|
152
|
+
return like_result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_superusers() -> set[str]:
|
|
156
|
+
"""获取超级用户列表。"""
|
|
157
|
+
|
|
158
|
+
return set(get_driver().config.superusers)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def handle_subscribe(bot: Bot, user_id: int) -> SubscriptionResult:
|
|
162
|
+
"""创建或续期订阅。"""
|
|
163
|
+
|
|
164
|
+
now = datetime.now()
|
|
165
|
+
purge_expired_subscriptions(now)
|
|
166
|
+
|
|
167
|
+
current = get_subscription(user_id)
|
|
168
|
+
expires_at = now + timedelta(days=plugin_config.sublike_sub_expire_days)
|
|
169
|
+
is_renew = current is not None and current.expires_at > now
|
|
170
|
+
|
|
171
|
+
if current is None or current.expires_at <= now:
|
|
172
|
+
record = SubscriptionRecord(
|
|
173
|
+
user_id=user_id,
|
|
174
|
+
created_at=now,
|
|
175
|
+
last_trigger_at=now,
|
|
176
|
+
expires_at=expires_at,
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
record = current.model_copy(
|
|
180
|
+
update={
|
|
181
|
+
"last_trigger_at": now,
|
|
182
|
+
"expires_at": expires_at,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
upsert_subscription(record)
|
|
187
|
+
|
|
188
|
+
require_friend = plugin_config.sublike_need_friend_sub
|
|
189
|
+
friend_state: bool | None = None
|
|
190
|
+
if require_friend:
|
|
191
|
+
friend_state = await is_friend(bot, user_id)
|
|
192
|
+
|
|
193
|
+
like_result = await handle_subscription_like(
|
|
194
|
+
bot,
|
|
195
|
+
record,
|
|
196
|
+
skip_delay=True,
|
|
197
|
+
friend_state=friend_state,
|
|
198
|
+
)
|
|
199
|
+
if like_result.success:
|
|
200
|
+
record = get_subscription(user_id) or record
|
|
201
|
+
elif like_result.status == LikeStatus.FAILED:
|
|
202
|
+
logger.warning(f"用户 {user_id} 订阅后立即点赞失败:{like_result.detail}")
|
|
203
|
+
|
|
204
|
+
return SubscriptionResult(
|
|
205
|
+
user_id=user_id,
|
|
206
|
+
status=(
|
|
207
|
+
SubscriptionStatus.RENEWED if is_renew else SubscriptionStatus.SUBSCRIBED
|
|
208
|
+
),
|
|
209
|
+
require_friend=require_friend,
|
|
210
|
+
is_friend=friend_state,
|
|
211
|
+
record=record,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def handle_unsubscribe(user_id: int) -> SubscriptionResult:
|
|
216
|
+
"""取消订阅。"""
|
|
217
|
+
|
|
218
|
+
removed = remove_subscription(user_id)
|
|
219
|
+
if removed:
|
|
220
|
+
return SubscriptionResult(
|
|
221
|
+
user_id=user_id,
|
|
222
|
+
status=SubscriptionStatus.UNSUBSCRIBED,
|
|
223
|
+
)
|
|
224
|
+
return SubscriptionResult(
|
|
225
|
+
user_id=user_id,
|
|
226
|
+
status=SubscriptionStatus.NOT_SUBSCRIBED,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def handle_subscription_status(
|
|
231
|
+
user_id: int,
|
|
232
|
+
is_superuser: bool,
|
|
233
|
+
) -> SubscriptionResult:
|
|
234
|
+
"""查看订阅状态。"""
|
|
235
|
+
|
|
236
|
+
now = datetime.now()
|
|
237
|
+
purge_expired_subscriptions(now)
|
|
238
|
+
|
|
239
|
+
if is_superuser:
|
|
240
|
+
records = load_subscriptions()
|
|
241
|
+
if not records:
|
|
242
|
+
return SubscriptionResult(
|
|
243
|
+
user_id=user_id,
|
|
244
|
+
status=SubscriptionStatus.EMPTY,
|
|
245
|
+
is_superuser_view=True,
|
|
246
|
+
)
|
|
247
|
+
return SubscriptionResult(
|
|
248
|
+
user_id=user_id,
|
|
249
|
+
status=SubscriptionStatus.STATUS_LIST,
|
|
250
|
+
is_superuser_view=True,
|
|
251
|
+
records=records,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
record = get_subscription(user_id)
|
|
255
|
+
if record is None or record.expires_at <= now:
|
|
256
|
+
return SubscriptionResult(
|
|
257
|
+
user_id=user_id,
|
|
258
|
+
status=SubscriptionStatus.EMPTY,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return SubscriptionResult(
|
|
262
|
+
user_id=user_id,
|
|
263
|
+
status=SubscriptionStatus.STATUS_SINGLE,
|
|
264
|
+
record=record,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def is_superuser(user_id: int) -> bool:
|
|
269
|
+
"""判断当前用户是否为超级用户。"""
|
|
270
|
+
|
|
271
|
+
return str(user_id) in _get_superusers()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def handle_subscription_like(bot: Bot, record: SubscriptionRecord) -> LikeResult:
|
|
275
|
+
"""执行单个订阅用户的定时点赞。"""
|
|
276
|
+
return await _handle_subscription_like(bot, record)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def _handle_subscription_like(
|
|
280
|
+
bot: Bot,
|
|
281
|
+
record: SubscriptionRecord,
|
|
282
|
+
*,
|
|
283
|
+
skip_delay: bool = False,
|
|
284
|
+
friend_state: bool | None = None,
|
|
285
|
+
) -> LikeResult:
|
|
286
|
+
"""执行单个订阅用户的点赞。"""
|
|
287
|
+
|
|
288
|
+
result = LikeResult(
|
|
289
|
+
user_id=record.user_id,
|
|
290
|
+
source=LikeSource.SUBSCRIPTION,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if plugin_config.sublike_need_friend_sub:
|
|
294
|
+
if friend_state is None:
|
|
295
|
+
result.is_friend = await check_friend(
|
|
296
|
+
bot,
|
|
297
|
+
record.user_id,
|
|
298
|
+
require_friend=plugin_config.sublike_need_friend_sub,
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
result.is_friend = friend_state
|
|
302
|
+
if not result.is_friend:
|
|
303
|
+
result.status = LikeStatus.NOT_FRIEND
|
|
304
|
+
result.detail = "当前不是机器人好友,跳过订阅点赞"
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
delay_seconds = (
|
|
308
|
+
0 if skip_delay else get_random_delay_seconds(plugin_config.sublike_delay_max)
|
|
309
|
+
)
|
|
310
|
+
if delay_seconds > 0:
|
|
311
|
+
await asyncio.sleep(delay_seconds)
|
|
312
|
+
|
|
313
|
+
result = await execute_like(
|
|
314
|
+
bot,
|
|
315
|
+
record.user_id,
|
|
316
|
+
source=LikeSource.SUBSCRIPTION,
|
|
317
|
+
)
|
|
318
|
+
if plugin_config.sublike_need_friend_sub:
|
|
319
|
+
result.is_friend = True
|
|
320
|
+
|
|
321
|
+
if result.success:
|
|
322
|
+
now = datetime.now()
|
|
323
|
+
update_user_like_stats(record.user_id, result.total, now)
|
|
324
|
+
updated_record = record.model_copy(
|
|
325
|
+
update={
|
|
326
|
+
"last_like_at": now,
|
|
327
|
+
"last_like_date": now.date(),
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
upsert_subscription(updated_record)
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def run_subscription_scan(bot: Bot) -> None:
|
|
336
|
+
"""执行一次订阅扫描。"""
|
|
337
|
+
|
|
338
|
+
now = datetime.now()
|
|
339
|
+
if not in_active_window(
|
|
340
|
+
now,
|
|
341
|
+
plugin_config.sublike_sched_start,
|
|
342
|
+
plugin_config.sublike_sched_end,
|
|
343
|
+
):
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
purge_expired_subscriptions(now)
|
|
347
|
+
records = load_subscriptions()
|
|
348
|
+
for record in records:
|
|
349
|
+
if record.last_like_date == now.date():
|
|
350
|
+
continue
|
|
351
|
+
result = await handle_subscription_like(bot, record)
|
|
352
|
+
if result.status == LikeStatus.NOT_FRIEND:
|
|
353
|
+
logger.info(f"用户 {record.user_id} 不是好友,跳过订阅点赞")
|
|
354
|
+
elif result.status == LikeStatus.FAILED:
|
|
355
|
+
logger.warning(f"用户 {record.user_id} 订阅点赞失败:{result.detail}")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""数据存储。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from nonebot import require
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
require("nonebot_plugin_localstore")
|
|
14
|
+
import nonebot_plugin_localstore as store
|
|
15
|
+
|
|
16
|
+
from .models import SubscriptionRecord, UserLikeStats
|
|
17
|
+
|
|
18
|
+
SUBSCRIPTIONS_FILE = store.get_plugin_data_file("subscriptions.json")
|
|
19
|
+
USER_STATS_FILE = store.get_plugin_data_file("user_stats.json")
|
|
20
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_records(
|
|
24
|
+
file_path: Path,
|
|
25
|
+
model_type: type[SubscriptionRecord],
|
|
26
|
+
) -> list[SubscriptionRecord]:
|
|
27
|
+
"""读取订阅记录列表。"""
|
|
28
|
+
|
|
29
|
+
return _load_model_list(file_path, model_type)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_stats(
|
|
33
|
+
file_path: Path,
|
|
34
|
+
model_type: type[UserLikeStats],
|
|
35
|
+
) -> list[UserLikeStats]:
|
|
36
|
+
"""读取用户统计列表。"""
|
|
37
|
+
|
|
38
|
+
return _load_model_list(file_path, model_type)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_model_list(
|
|
42
|
+
file_path: Path,
|
|
43
|
+
model_type: type[ModelT],
|
|
44
|
+
) -> list[ModelT]:
|
|
45
|
+
"""从 JSON 文件中读取模型列表。"""
|
|
46
|
+
|
|
47
|
+
if not file_path.exists():
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
raw_data = json.loads(file_path.read_text(encoding="utf-8"))
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
if not isinstance(raw_data, list):
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
records: list[ModelT] = []
|
|
59
|
+
for item in raw_data:
|
|
60
|
+
try:
|
|
61
|
+
records.append(model_type.model_validate(item))
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
return records
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _save_model_list(
|
|
69
|
+
file_path: Path,
|
|
70
|
+
records: list[SubscriptionRecord] | list[UserLikeStats],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""将模型列表写入 JSON 文件。"""
|
|
73
|
+
|
|
74
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
payload = [record.model_dump(mode="json") for record in records]
|
|
76
|
+
file_path.write_text(
|
|
77
|
+
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_subscriptions() -> list[SubscriptionRecord]:
|
|
83
|
+
"""读取全部订阅记录。"""
|
|
84
|
+
|
|
85
|
+
records = _load_records(SUBSCRIPTIONS_FILE, SubscriptionRecord)
|
|
86
|
+
return sorted(records, key=lambda record: record.user_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def save_subscriptions(records: list[SubscriptionRecord]) -> None:
|
|
90
|
+
"""保存全部订阅记录。"""
|
|
91
|
+
|
|
92
|
+
ordered_records = sorted(records, key=lambda record: record.user_id)
|
|
93
|
+
_save_model_list(SUBSCRIPTIONS_FILE, ordered_records)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_subscription(user_id: int) -> SubscriptionRecord | None:
|
|
97
|
+
"""按 QQ 号获取订阅记录。"""
|
|
98
|
+
|
|
99
|
+
for record in load_subscriptions():
|
|
100
|
+
if record.user_id == user_id:
|
|
101
|
+
return record
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def upsert_subscription(record: SubscriptionRecord) -> None:
|
|
106
|
+
"""新增或更新订阅记录。"""
|
|
107
|
+
|
|
108
|
+
records = [item for item in load_subscriptions() if item.user_id != record.user_id]
|
|
109
|
+
records.append(record)
|
|
110
|
+
save_subscriptions(records)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def remove_subscription(user_id: int) -> bool:
|
|
114
|
+
"""删除订阅记录。"""
|
|
115
|
+
|
|
116
|
+
records = load_subscriptions()
|
|
117
|
+
new_records = [record for record in records if record.user_id != user_id]
|
|
118
|
+
if len(new_records) == len(records):
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
save_subscriptions(new_records)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def purge_expired_subscriptions(now: datetime) -> int:
|
|
126
|
+
"""清理已过期的订阅记录。"""
|
|
127
|
+
|
|
128
|
+
records = load_subscriptions()
|
|
129
|
+
valid_records = [record for record in records if record.expires_at > now]
|
|
130
|
+
removed_count = len(records) - len(valid_records)
|
|
131
|
+
if removed_count > 0:
|
|
132
|
+
save_subscriptions(valid_records)
|
|
133
|
+
return removed_count
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_user_stats() -> list[UserLikeStats]:
|
|
137
|
+
"""读取全部用户统计。"""
|
|
138
|
+
|
|
139
|
+
records = _load_stats(USER_STATS_FILE, UserLikeStats)
|
|
140
|
+
return sorted(records, key=lambda record: record.user_id)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def save_user_stats(records: list[UserLikeStats]) -> None:
|
|
144
|
+
"""保存全部用户统计。"""
|
|
145
|
+
|
|
146
|
+
ordered_records = sorted(records, key=lambda record: record.user_id)
|
|
147
|
+
_save_model_list(USER_STATS_FILE, ordered_records)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_user_stats(user_id: int) -> UserLikeStats | None:
|
|
151
|
+
"""按 QQ 号获取用户统计。"""
|
|
152
|
+
|
|
153
|
+
for record in load_user_stats():
|
|
154
|
+
if record.user_id == user_id:
|
|
155
|
+
return record
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def upsert_user_stats(record: UserLikeStats) -> None:
|
|
160
|
+
"""新增或更新用户统计。"""
|
|
161
|
+
|
|
162
|
+
records = [item for item in load_user_stats() if item.user_id != record.user_id]
|
|
163
|
+
records.append(record)
|
|
164
|
+
save_user_stats(records)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""工具函数。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from random import randint
|
|
5
|
+
|
|
6
|
+
from nonebot.adapters.onebot.v11 import Bot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def is_friend(bot: Bot, user_id: int) -> bool:
|
|
10
|
+
"""判断目标用户是否在机器人好友列表中。"""
|
|
11
|
+
|
|
12
|
+
friend_list = await bot.get_friend_list()
|
|
13
|
+
return any(friend.get("user_id") == user_id for friend in friend_list)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def in_active_window(now: datetime, start_hour: int, end_hour: int) -> bool:
|
|
17
|
+
"""判断当前时间是否处于定时任务运行时段。"""
|
|
18
|
+
|
|
19
|
+
current_hour = now.hour
|
|
20
|
+
if start_hour == end_hour:
|
|
21
|
+
return True
|
|
22
|
+
if start_hour < end_hour:
|
|
23
|
+
return start_hour <= current_hour < end_hour
|
|
24
|
+
return current_hour >= start_hour or current_hour < end_hour
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_random_delay_seconds(max_minutes: int) -> int:
|
|
28
|
+
"""获取随机延迟秒数。"""
|
|
29
|
+
|
|
30
|
+
if max_minutes <= 0:
|
|
31
|
+
return 0
|
|
32
|
+
return randint(0, max_minutes * 60)
|