Compare commits

..

502 Commits

Author SHA1 Message Date
github-actions[bot]
3234871b53 🔖 Release 2.0.0-rc.1 2022-10-02 08:05:19 +00:00
Ju4tCode
03543f01f2 🔖 bump version 2.0.0rc1 (#1300) 2022-10-02 15:49:31 +08:00
github-actions[bot]
ba5c0303c7 📝 Update changelog 2022-09-29 09:21:16 +00:00
Ju4tCode
e56fdd04ad 🍻 publish adapter GitHub (#1297) 2022-09-29 17:20:05 +08:00
github-actions[bot]
9f10bb70db 📝 Update changelog 2022-09-29 08:57:20 +00:00
Ju4tCode
71aad502d1 🐛 Fix: 内置规则和权限没有捕获错误 (#1291) 2022-09-29 16:56:06 +08:00
github-actions[bot]
ab85b8651e 📝 Update changelog 2022-09-29 08:43:25 +00:00
NewYearPrism
4fe8929441 🍻 publish plugin 文字识别 (#1294) 2022-09-29 16:42:06 +08:00
github-actions[bot]
5c303710f6 📝 Update changelog 2022-09-29 08:17:33 +00:00
RandomEnch
68d2ada94b 🍻 publish plugin 在线编曲 (#1292) 2022-09-29 16:16:07 +08:00
github-actions[bot]
75470fe157 📝 Update changelog 2022-09-25 15:24:53 +00:00
koking0
47b3fc516a 🍻 publish plugin 图灵机器人 (#1288) 2022-09-25 23:23:49 +08:00
github-actions[bot]
84c24b014f 📝 Update changelog 2022-09-23 12:26:33 +00:00
lgc2333
756cde6525 🍻 publish plugin PicStatus (#1286) 2022-09-23 20:25:22 +08:00
github-actions[bot]
57ef19af94 📝 Update changelog 2022-09-21 08:58:18 +00:00
Kaguya233qwq
5927b517e2 🍻 publish plugin 阿里云盘福利码自动兑换 (#1282) 2022-09-21 16:57:01 +08:00
github-actions[bot]
132205bfcc 📝 Update changelog 2022-09-21 08:49:44 +00:00
dpm12345
b31dfa9ab0 🍻 publish plugin gal角色语音生成 (#1280) 2022-09-21 16:48:17 +08:00
github-actions[bot]
b249802c38 📝 Update changelog 2022-09-19 09:56:45 +00:00
Todysheep
9df705aaa7 🍻 publish plugin 漂流瓶 (#1278) 2022-09-19 17:55:38 +08:00
github-actions[bot]
a0df535f0c 📝 Update changelog 2022-09-18 14:36:17 +00:00
AkiraXie
31022a653d Feature: SUPERUSER 权限匹配任意超管事件 (#1275)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-18 22:33:36 +08:00
github-actions[bot]
ba77443dde 📝 Update changelog 2022-09-18 14:30:09 +00:00
XZhouQD
984f743097 🍻 publish plugin BWIKI助手移植版 (#1273) 2022-09-18 22:29:03 +08:00
github-actions[bot]
638a9c94af 📝 Update changelog 2022-09-16 11:52:43 +00:00
su226
92f1d5a4d7 🍻 publish bot IdhagnBot (#1266) 2022-09-16 19:51:35 +08:00
github-actions[bot]
248af2ae1a 📝 Update changelog 2022-09-15 13:58:15 +00:00
littlebutt
4c37be7312 🍻 publish plugin nonebot物联网插件 (#1264) 2022-09-15 21:56:57 +08:00
github-actions[bot]
2cb8eafa81 📝 Update changelog 2022-09-13 02:47:25 +00:00
CMHopeSunshine
05bff5ec17 🍻 publish bot LittlePaimon (#1255) 2022-09-13 10:46:18 +08:00
github-actions[bot]
13245cb58f 📝 Update changelog 2022-09-12 13:27:14 +00:00
AbCooly
37bc7326b5 🍻 publish plugin 狼人杀插件 (#1251) 2022-09-12 21:25:50 +08:00
github-actions[bot]
f6d189d8c5 📝 Update changelog 2022-09-12 04:30:37 +00:00
bridgeL
600ef7031f 🍻 publish plugin ayaka - 文字游戏开发辅助插件 (#1253) 2022-09-12 12:29:30 +08:00
github-actions[bot]
7bedf7c8d0 📝 Update changelog 2022-09-11 12:59:20 +00:00
ppxxxg22
f62ee5893c 🍻 publish plugin 图像超分辨率重建 (#1249) 2022-09-11 20:57:55 +08:00
github-actions[bot]
71234e9a68 📝 Update changelog 2022-09-10 12:56:12 +00:00
Akirami
3bbca0fa70 Feature: 改进 CommandGroupMatcherGroup 的结构 (#1240) 2022-09-10 20:54:49 +08:00
github-actions[bot]
20f144ba93 📝 Update changelog 2022-09-09 10:53:37 +00:00
Akirami
4c8bc9f0cb 🍻 Fix: 修正 GenshinUID 的发布类型 (#1243) 2022-09-09 18:52:12 +08:00
github-actions[bot]
064509f26b 📝 Update changelog 2022-09-09 03:54:19 +00:00
Ju4tCode
8c42490a7e 🔇 Feature: 调整日志输出格式与等级 (#1233) 2022-09-09 11:52:57 +08:00
github-actions[bot]
179d7105c9 📝 Update changelog 2022-09-09 02:16:34 +00:00
KarisAya
1c14e638c8 🍻 publish plugin Minecraft Server 聊天同步 (#1244) 2022-09-09 10:15:26 +08:00
github-actions[bot]
c6eef06b55 📝 Update changelog 2022-09-08 02:38:39 +00:00
Akirami
beef564a22 🔥 remove unused imports (#1236) 2022-09-08 10:37:16 +08:00
github-actions[bot]
672f2ceecc 📝 Update changelog 2022-09-07 02:25:49 +00:00
Sclock
28142402d7 🍻 publish plugin 查询ETH合并日期 (#1231) 2022-09-07 10:24:39 +08:00
github-actions[bot]
b886329fb8 📝 Update changelog 2022-09-07 02:00:34 +00:00
Ju4tCode
a0b186aff3 ♻️ improve dependent structure (#1227) 2022-09-07 09:59:05 +08:00
pre-commit-ci[bot]
595c64e760 ⬆️ auto update by pre-commit hooks (#1229)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-06 09:54:57 +08:00
github-actions[bot]
5114749073 📝 Update changelog 2022-09-05 06:23:20 +00:00
84227871
af2d7b5797 🍻 publish bot GenshinUID (#1225) 2022-09-05 14:21:53 +08:00
github-actions[bot]
56943c0908 📝 Update changelog 2022-09-05 05:52:33 +00:00
SDIJF1521
45478deb95 🍻 publish bot 小白机器人 (#1223) 2022-09-05 13:51:16 +08:00
github-actions[bot]
d281ec5bf9 📝 Update changelog 2022-09-05 02:02:23 +00:00
17TheWord
291a7cbb8b 🍻 publish plugin 星际战甲事件查询 (#1219) 2022-09-05 10:01:20 +08:00
github-actions[bot]
41259546bd 📝 Update changelog 2022-09-04 11:31:42 +00:00
Ljzd-PRO
f96038241f ✏️ 更新插件米游社辅助工具 tag (#1221) 2022-09-04 19:30:32 +08:00
github-actions[bot]
b051320d78 📝 Update changelog 2022-09-04 03:18:37 +00:00
Ljzd-PRO
d12efac9f4 🍻 publish plugin 米游社辅助工具 (#1217) 2022-09-04 11:17:20 +08:00
github-actions[bot]
f87a38a30a 📝 Update changelog 2022-09-03 02:27:15 +00:00
monsterxcn
bf016b3f69 🍻 publish plugin 原神每日材料查询 (#1215) 2022-09-03 10:26:07 +08:00
github-actions[bot]
373f5255f1 📝 Update changelog 2022-09-02 05:41:40 +00:00
Ju4tCode
5a35015195 📝 update documentation 2022-09-02 13:40:24 +08:00
Melodyknit
d3a2f1dc08 🍻 publish adapter Console (#1212) 2022-09-02 13:40:24 +08:00
github-actions[bot]
f1aec4eb10 📝 Update changelog 2022-09-01 02:42:53 +00:00
Ju4tCode
cd30be21ba 🐛 fix nested user permission update (#1208) 2022-09-01 10:41:43 +08:00
17TheWord
f150a9ee89 🍻 publish plugin MC_QQ_MCRcon (#1210) 2022-09-01 10:41:08 +08:00
github-actions[bot]
e68281f60f 📝 Update changelog 2022-09-01 02:05:08 +00:00
monsterxcn
32be64485a 🍻 publish plugin 原神角色展柜查询 (#1207) 2022-09-01 10:03:55 +08:00
github-actions[bot]
c76f492305 📝 Update changelog 2022-08-31 07:46:34 +00:00
s52047qwas
29b0351644 🍻 publish plugin 修仙模拟器 (#1195) 2022-08-31 15:45:27 +08:00
github-actions[bot]
ef3350fd9c 📝 Update changelog 2022-08-31 06:57:46 +00:00
Raidenneox
459699de5c 🍻 publish plugin 赛博浅草寺 (#1205) 2022-08-31 14:56:25 +08:00
github-actions[bot]
31c3eb8fd6 📝 Update changelog 2022-08-31 02:08:36 +00:00
Lan
1cfdee2645 Featue: load_plugin 支持 pathlib.Path (#1194)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-08-31 10:07:14 +08:00
github-actions[bot]
4e76518a58 📝 Update changelog 2022-08-31 02:05:28 +00:00
GC-ZF
b53e029df1 🍻 publish plugin 不背单词 (#1203) 2022-08-31 10:04:01 +08:00
github-actions[bot]
c1faf68806 📝 Update changelog 2022-08-30 11:39:33 +00:00
CofinCup
fe64b904ff 🍻 publish plugin 自识别todo (#1192) 2022-08-30 19:38:24 +08:00
github-actions[bot]
a621ade449 📝 Update changelog 2022-08-30 01:55:21 +00:00
Ju4tCode
3fda978064 Feature: 新增事件类型过滤 rule (#1183) 2022-08-30 09:54:09 +08:00
github-actions[bot]
60ab93164c 📝 Update changelog 2022-08-28 12:12:28 +00:00
sena-nana
2b22d5abda 🍻 publish plugin 雨课堂自动签到 (#1188) 2022-08-28 20:11:22 +08:00
github-actions[bot]
e3a4834383 📝 Update changelog 2022-08-27 14:30:10 +00:00
sena-nana
21087036af 🍻 publish plugin 反馈及通知 (#1186) 2022-08-27 22:29:05 +08:00
github-actions[bot]
94d336ef4d 📝 Update changelog 2022-08-27 14:16:48 +00:00
sena-nana
07707213a5 🍻 publish plugin MagiaDice骰娘及TRPGLOG (#1184) 2022-08-27 22:15:41 +08:00
github-actions[bot]
7579878fb4 📝 Update changelog 2022-08-27 13:51:18 +00:00
Nranphy
6599b6420e 🍻 publish plugin 面麻小助手 (#1190) 2022-08-27 21:50:04 +08:00
github-actions[bot]
135c6e8168 📝 Update changelog 2022-08-26 02:16:15 +00:00
X-Skirt-X
743e7363ea 🍻 publish plugin 话痨排行榜 (#1181) 2022-08-26 10:15:07 +08:00
github-actions[bot]
a101428c81 📝 Update changelog 2022-08-25 02:27:25 +00:00
YunJin
0d2b1f693e ✏️ Plugin: 修改插件多功能简易群管信息 (#1180) 2022-08-25 10:26:06 +08:00
github-actions[bot]
e5a53dfd5c 📝 Update changelog 2022-08-25 02:00:45 +00:00
KarisAya
915c2b3e43 🍻 publish plugin 保存群聊闪照 (#1178) 2022-08-25 09:59:40 +08:00
github-actions[bot]
767b6a9913 📝 Update changelog 2022-08-24 01:55:17 +00:00
Ju4tCode
3f8af04803 add rich text support for shell command (#1171) 2022-08-24 09:54:08 +08:00
github-actions[bot]
00af815b8a 📝 Update changelog 2022-08-23 07:01:42 +00:00
InariInDream
24df594b97 🍻 publish plugin 课表查询 (#1167) 2022-08-23 15:00:34 +08:00
github-actions[bot]
d6567f9288 📝 Update changelog 2022-08-23 06:35:43 +00:00
yzyyz1387
4eb158245e 🍻 publish plugin 业余无线电助手 (#1172) 2022-08-23 14:34:27 +08:00
github-actions[bot]
ef35266d3e 📝 Update changelog 2022-08-23 04:04:55 +00:00
he0119
1d1beb100a 🍻 publish plugin NoneBot 树形帮助插件 (#1176) 2022-08-23 12:03:41 +08:00
github-actions[bot]
06ab6093b7 📝 Update changelog 2022-08-23 03:54:04 +00:00
yzyyz1387
c1ce7fb940 🍻 publish plugin 工作性价比 (#1174) 2022-08-23 11:53:01 +08:00
github-actions[bot]
6e03ddbf12 📝 Update changelog 2022-08-23 03:48:13 +00:00
KarisAya
40c8787828 🍻 publish plugin 娶群友 (#1169) 2022-08-23 11:47:03 +08:00
github-actions[bot]
be459e0bbb 📝 Update changelog 2022-08-22 10:12:31 +00:00
ssttkkl
1056828f90 🍻 publish plugin PixivBot (#1164) 2022-08-22 18:11:28 +08:00
github-actions[bot]
ef18e8943d 📝 Update changelog 2022-08-22 06:40:04 +00:00
Mix
92ff1df419 🐛 修复当消息与不支持的类型相加时抛出的异常类型错误 (#1166) 2022-08-22 14:39:00 +08:00
github-actions[bot]
be5ac88a18 📝 Update changelog 2022-08-21 03:25:26 +00:00
Yiyuiii
fac647370a 🍻 publish plugin 日韩中 VITS 模型原神拟声 (#1161) 2022-08-21 11:24:11 +08:00
github-actions[bot]
05a3891903 📝 Update changelog 2022-08-20 02:19:01 +00:00
Ju4tCode
4deae8f00c 🔥 remove deprecated State param (#1160) 2022-08-20 10:17:52 +08:00
github-actions[bot]
0f70e975b0 📝 Update changelog 2022-08-19 01:10:13 +00:00
SkyDynamic
982680be91 🍻 publish plugin 每日人品 (#1155) 2022-08-19 09:09:10 +08:00
github-actions[bot]
96b0a863e6 📝 Update changelog 2022-08-18 01:43:06 +00:00
CrazyBoyM
d64bb37c6d 🍻 publish plugin nonebot-plugin-drawer (#1145) 2022-08-18 09:41:48 +08:00
github-actions[bot]
51d7f1783d 📝 Update changelog 2022-08-16 02:08:08 +00:00
YunJin
64c18379c9 ✏️ change plugin admin hello info (#1159) 2022-08-16 10:06:52 +08:00
GC_XiaoZhang
2735f0cba9 ✏️ change plugin fireN info (#1158) 2022-08-16 10:06:02 +08:00
github-actions[bot]
660dbaf3b8 📝 Update changelog 2022-08-16 02:04:40 +00:00
Ju4tCode
898c29d7ee 💥 remove deprecated nonebot.plugins toml table (#1151)
Feature: 移除过时的 `nonebot.plugins` toml 配置
2022-08-16 10:03:37 +08:00
github-actions[bot]
cdc507bab9 📝 Update changelog 2022-08-15 13:34:28 +00:00
YunJin
f32bcdc1fc ✏️ update plugin 多功能简易群管 (#1154) 2022-08-15 21:33:13 +08:00
github-actions[bot]
013602da21 📝 Update changelog 2022-08-14 11:42:11 +00:00
Ju4tCode
4974c596ec 💥 remove Python 3.7 support (#1148) 2022-08-14 19:41:00 +08:00
github-actions[bot]
0620bec51f 📝 Update changelog 2022-08-14 09:11:29 +00:00
KarisAya
8870e6a26e 🍻 publish plugin 小游戏合集 (#1149) 2022-08-14 17:10:21 +08:00
github-actions[bot]
0e3ed0e7ab 📝 Update changelog 2022-08-12 07:02:00 +00:00
HuYihe2008
6cc3b68447 🍻 publish plugin 简易群管(带入群欢迎) (#1141) 2022-08-12 15:00:52 +08:00
github-actions[bot]
549a37b172 📝 Update changelog 2022-08-11 05:52:38 +00:00
ZombieFly
16394ad68b 🍻 publish plugin wiki条目搜索、获取简介 (#1132) 2022-08-11 13:51:20 +08:00
github-actions[bot]
57e580c255 📝 Update changelog 2022-08-11 05:51:05 +00:00
Ankhyty
6c23d89494 🍻 publish plugin bangumi搜索 (#1136) 2022-08-11 13:50:01 +08:00
github-actions[bot]
675e70f579 📝 Update changelog 2022-08-09 11:01:23 +00:00
bingqiu456
7a098b96f8 🍻 publish plugin 疫情小助手-频道版 (#1130) 2022-08-09 19:00:15 +08:00
github-actions[bot]
c9794bf91d 📝 Update changelog 2022-08-08 13:08:47 +00:00
Ju4tCode
1766d4da69 💥 remove deprecated export (#1125) 2022-08-08 21:07:36 +08:00
github-actions[bot]
6583bc8c61 📝 Update changelog 2022-08-08 04:19:01 +00:00
17TheWord
179f16346a 🍻 publish plugin MC_QQ通信 (#1126) 2022-08-08 12:17:47 +08:00
github-actions[bot]
badb0c9ff4 📝 Update changelog 2022-08-08 02:26:00 +00:00
lgc2333
ee0ea85e40 🍻 publish plugin BAWiki (#1128) 2022-08-08 10:24:48 +08:00
github-actions[bot]
e5e69c2726 🔖 Release 2.0.0-beta.5 2022-08-04 06:27:40 +00:00
github-actions[bot]
7c7ea613e9 📝 Update changelog 2022-08-04 06:18:34 +00:00
Ju4tCode
bb1b94e5e3 🔖 bump version 2.0.0-beta.5 (#1122) 2022-08-04 14:14:50 +08:00
github-actions[bot]
8420add975 📝 Update changelog 2022-08-04 05:40:36 +00:00
Ju4tCode
2192e8cb6d 🐛 fix parent detect error after require (#1121) 2022-08-04 13:39:20 +08:00
github-actions[bot]
48ccef2f06 📝 Update changelog 2022-08-02 02:31:11 +00:00
AkiraXie
c6bc24efc2 🐛 run_postprecessors handle matcher.state now (#1119) 2022-08-02 10:29:48 +08:00
github-actions[bot]
63f8d78d20 📝 Update changelog 2022-08-01 06:19:28 +00:00
ArgonarioD
db36c262db 🍻 publish plugin 「能不能好好说话?」缩写翻译 (#1117) 2022-08-01 14:18:15 +08:00
github-actions[bot]
732a13b692 📝 Update changelog 2022-08-01 02:53:25 +00:00
Ju4tCode
71bf1d1147 🐛 fix import error if setuptools not installed (#1116)
Fix: 修复 setuptools 未安装导致 ImportError
2022-08-01 10:52:15 +08:00
github-actions[bot]
6e98ac031c 📝 Update changelog 2022-07-31 02:33:58 +00:00
syrinka
9a49354ddd 🍻 publish plugin 推送钩子 (#1114) 2022-07-31 10:32:36 +08:00
github-actions[bot]
455752bd92 📝 Update changelog 2022-07-28 04:42:49 +00:00
yuyuziYYZ
2a51b07229 🍻 publish bot SkadiBot (#1112) 2022-07-28 12:41:42 +08:00
github-actions[bot]
732b5b0b1b 📝 Update changelog 2022-07-25 02:01:16 +00:00
ziru-w
a0dcc7753c 🍻 publish plugin 易命令 (#1110) 2022-07-25 09:59:58 +08:00
github-actions[bot]
12942f2d50 📝 Update changelog 2022-07-23 02:25:34 +00:00
bingqiu456
192d094f54 🍻 publish plugin 群昵称时间 (#1108) 2022-07-23 10:24:17 +08:00
github-actions[bot]
bb02d50837 📝 Update changelog 2022-07-22 06:23:48 +00:00
那个小白白白
bc8c65d0d8 Bot: 修改剑网三 bot 信息 (#1107) 2022-07-22 14:22:36 +08:00
github-actions[bot]
9447b1f462 📝 Update changelog 2022-07-20 02:23:17 +00:00
Dobiichi-Origami
c03b0c73cb Feature: on_x 支持 expire_time 参数 (#1106)
Co-authored-by: Dobiichi-Origami <454470535@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-07-20 10:21:31 +08:00
github-actions[bot]
19f4c01ad3 📝 Update changelog 2022-07-15 02:12:35 +00:00
synodriver
9bd07b9ced add block driver startup/shutdown sync support (#1104)
Feature: 正向驱动器 startup/shutdown hook 支持同步函数
2022-07-15 10:11:19 +08:00
github-actions[bot]
fe5cf5624c 📝 Update changelog 2022-07-14 03:31:37 +00:00
Shine-Light
a14c38300e 🍻 publish bot 真宵Bot (#1102) 2022-07-14 11:30:34 +08:00
github-actions[bot]
9e908d5b3f 📝 Update changelog 2022-07-12 07:22:59 +00:00
ziru-w
f1ab95489c 🍻 publish plugin 处理好友添加和群邀请 (#1098) 2022-07-12 15:21:30 +08:00
github-actions[bot]
3c42e26e27 📝 Update changelog 2022-07-12 06:59:52 +00:00
zheuziihau
c248b8c354 🍻 publish plugin 明日方舟寻访记录分析 (#1096) 2022-07-12 14:58:45 +08:00
github-actions[bot]
0ecea50778 📝 Update changelog 2022-07-11 08:05:27 +00:00
ziru-w
33d4d01d51 🍻 publish plugin b站视频每日推送 (#1094) 2022-07-11 16:04:06 +08:00
github-actions[bot]
1667440c64 📝 Update changelog 2022-07-10 06:38:53 +00:00
KarisAya
141527238c 🍻 publish plugin 自动回复(文i)插件 (#1089) 2022-07-10 14:37:54 +08:00
github-actions[bot]
e2d0453741 📝 Update changelog 2022-07-10 02:48:38 +00:00
10-24
0849df1c76 🍻 publish plugin ACC计算工具 (#1092) 2022-07-10 10:47:35 +08:00
github-actions[bot]
c4d45c087a 📝 Update changelog 2022-07-08 04:26:23 +00:00
Ju4tCode
be15cfabcc 📝 Docs: 添加 nonemoji 并更新开发指南 (#1088)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-08 12:24:48 +08:00
github-actions[bot]
dddbeb389f 📝 Update changelog 2022-07-06 03:02:29 +00:00
yaowan233
bdfaf4840f 🍻 publish plugin OSU查分插件 (#1081) 2022-07-06 11:00:35 +08:00
GC-ZF
b37b1380a3 🍻 publish plugin 战地1、5战绩查询工具 (#1086) 2022-07-06 10:59:34 +08:00
github-actions[bot]
d8ed5c2e80 📝 Update changelog 2022-07-06 02:38:40 +00:00
GC-ZF
4bc391c066 🍻 publish plugin 一起燚xN吧 (#1084) 2022-07-06 10:37:18 +08:00
pre-commit-ci[bot]
5aa6138bf3 ⬆️ auto update by pre-commit hooks (#1080)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-05 10:17:39 +08:00
github-actions[bot]
fc78b9c547 📝 Update changelog 2022-07-05 02:17:00 +00:00
Ju4tCode
118874080d ✏️ fix docs event message type error (#1079) 2022-07-05 10:15:55 +08:00
github-actions[bot]
cf2137a1a9 📝 Update changelog 2022-07-02 02:21:11 +00:00
StarHeart
14b145b58d 📝 Docs: 修复旧 Vuepress 文档缓存问题 (#1077)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-02 10:19:51 +08:00
github-actions[bot]
1aba737cbd 📝 Update changelog 2022-07-01 02:58:18 +00:00
Ju4tCode
fe38b1f17f 📝 Docs: 更新 Readme 贡献图片 (#1074) 2022-07-01 10:57:05 +08:00
github-actions[bot]
776651284f 📝 Update changelog 2022-07-01 02:39:14 +00:00
CMHopeSunshine
068cc3a7ea 🍻 publish plugin 米游币商品自动兑换 (#1075) 2022-07-01 10:38:16 +08:00
github-actions[bot]
0e7e88cfa2 📝 Update changelog 2022-06-30 06:31:31 +00:00
StarHeart
c120f9be70 📝 unregister sw loaded from vuepress (#1073) 2022-06-30 14:30:15 +08:00
github-actions[bot]
563436b38e 📝 Update changelog 2022-06-30 06:29:47 +00:00
shinianj
386be6cbb6 🍻 publish plugin 赛马 (#1068) 2022-06-30 14:28:42 +08:00
github-actions[bot]
b5e29533d8 📝 Update changelog 2022-06-30 04:20:11 +00:00
MingxuanGame
beb19adad5 📝 fix docs call Permission wrong (#1072) 2022-06-30 12:19:10 +08:00
github-actions[bot]
e2289c78b0 📝 Update changelog 2022-06-29 02:56:19 +00:00
hamo-reid
d3f261eb34 🍻 publish plugin PicMenu (#1070) 2022-06-29 10:55:16 +08:00
github-actions[bot]
d54c2e6bf4 📝 Update changelog 2022-06-28 02:26:03 +00:00
Mai-icy
6fdebc4912 🍻 publish plugin 面包店小游戏 (#1063) 2022-06-28 10:25:02 +08:00
github-actions[bot]
f1ffac5ca7 📝 Update changelog 2022-06-25 08:20:06 +00:00
A-kirami
12716ee79a 🍻 publish plugin 黑白名单 (#1060) 2022-06-25 16:19:08 +08:00
github-actions[bot]
42413281bb 📝 Update changelog 2022-06-25 04:34:24 +00:00
github-actions[bot]
a67eda4c80 📝 Update changelog 2022-06-24 02:52:05 +00:00
Akirami
9dbea871b8 ✏️ fix type T_RunPostProcessor incorrect description (#1057)
Bug: 修复 typing 中 T_RunPostProcessor 类型的注释描述不正确
2022-06-24 10:51:06 +08:00
github-actions[bot]
4181f4ca77 📝 Update changelog 2022-06-24 02:49:36 +00:00
Special-Week
fe4a33d19b 🍻 publish plugin BitTorrent (#1058) 2022-06-24 10:48:29 +08:00
github-actions[bot]
58d8815f39 🔖 Release 2.0.0-beta.4 2022-06-20 11:40:59 +00:00
Ju4tCode
b80083fed5 🔖 bump version 2.0.0-beta.4 (#1056) 2022-06-20 19:29:56 +08:00
github-actions[bot]
4ba17d900a 📝 Update changelog 2022-06-20 07:53:30 +00:00
Ju4tCode
f11970132c Fix: 修复 MessageSegment 在有额外数据时报错 (#1055) 2022-06-20 15:52:12 +08:00
github-actions[bot]
c91c9380a7 📝 Update changelog 2022-06-20 07:51:00 +00:00
Ju4tCode
06ee47edcd Feature: 添加插件元信息定义 (#1046) 2022-06-20 15:49:53 +08:00
github-actions[bot]
a82ce00a4b 📝 Update changelog 2022-06-19 01:25:55 +00:00
AquamarineCyan
e0902eeb58 🍻 publish plugin 历史上的今天 (#1048) 2022-06-19 09:24:36 +08:00
github-actions[bot]
b754ac2fbc 📝 Update changelog 2022-06-18 07:59:15 +00:00
Special-Week
1ae0a654bf 🍻 publish plugin 智能回复 (#1053) 2022-06-18 15:58:10 +08:00
github-actions[bot]
b85348f648 📝 Update changelog 2022-06-18 06:49:01 +00:00
Ju4tCode
7b06469a30 🐛 fix env var not override dotenv file (#1052) 2022-06-18 14:47:42 +08:00
github-actions[bot]
a62a49d477 📝 Update changelog 2022-06-18 03:13:55 +00:00
Special-Week
e2f96a0b5c 🍻 publish plugin nonebot_plugin_setu4 (#1050) 2022-06-18 11:12:40 +08:00
github-actions[bot]
28c22b7511 📝 Update changelog 2022-06-16 03:43:18 +00:00
nikissXI
c027e4d2ce 🍻 publish bot nya_bot (#1044) 2022-06-16 11:42:09 +08:00
github-actions[bot]
e38bb2b530 📝 Update changelog 2022-06-06 02:26:03 +00:00
18870
19a9a3c3c5 🍻 publish plugin 命令重启机器人 (#1037) 2022-06-06 10:24:55 +08:00
github-actions[bot]
4c8f5059db 📝 Update changelog 2022-06-04 11:34:00 +00:00
ZMXC01
9ab1acf1e7 🍻 publish plugin 青年大学习自动提交 (#1035) 2022-06-04 19:32:59 +08:00
github-actions[bot]
175acd38eb 📝 Update changelog 2022-06-03 13:00:11 +00:00
BlueGlassBlock
4241eb538c 🎨 Feature: 日志记录自动检测终端是否支持彩色 (#1034)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-06-03 20:59:04 +08:00
github-actions[bot]
c55b32b9f9 📝 Update changelog 2022-06-03 02:42:41 +00:00
bingqiu456
2082e5f5c2 🍻 publish plugin 疫情小助手 (#1032) 2022-06-03 10:41:18 +08:00
SEAFHMC
8485a356e7 🍻 publish plugin 谁艾特我了 (#1030) 2022-06-02 14:48:41 +08:00
dependabot[bot]
f6f70fb435 ⬆️ Bump httpx from 0.22.0 to 0.23.0 (#1029)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-02 11:21:28 +08:00
github-actions[bot]
c3ce4f76d1 📝 Update changelog 2022-06-01 16:37:04 +00:00
benx1n
db90a9ebab 🍻 publish plugin Hikari-战舰世界水表查询 (#1024) 2022-06-02 00:35:51 +08:00
github-actions[bot]
3962a98743 📝 Update changelog 2022-06-01 10:36:51 +00:00
Ju4tCode
3ce23bc593 🍻 publish adapter OneBot V12 (#1026)
Co-authored-by: yanyongyu <yanyongyu@users.noreply.github.com>
2022-06-01 18:35:40 +08:00
github-actions[bot]
3238a82042 📝 Update changelog 2022-05-31 04:36:51 +00:00
axStar
ed1f920088 🍻 publish plugin Warframe时间查询 (#1022) 2022-05-31 12:35:52 +08:00
github-actions[bot]
c34b3439fa 📝 Update changelog 2022-05-30 12:41:17 +00:00
Ju4tCode
bd2225c43c 🍻 publish plugin imagetools (#1020) (#1021)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-05-30 20:40:10 +08:00
github-actions[bot]
381234725e 📝 Update changelog 2022-05-29 02:57:28 +00:00
NumberSir
a5ee6ac401 🍻 publish plugin 明日方舟工具箱 (#1018) 2022-05-29 10:56:14 +08:00
github-actions[bot]
79fd12768d 📝 Update changelog 2022-05-27 09:29:27 +00:00
ASTWY
c0ab86f91b 🍻 publish plugin B站视频伪分享卡片 (#1013) 2022-05-27 17:28:20 +08:00
Ju4tCode
ae21bfdd0e 👷 fix python ci cache path error (#1012)
* 👷 fix python ci cache path error

* 🐛 fix path error

* ✏️ fix typo
2022-05-27 11:25:51 +08:00
github-actions[bot]
6cfa21b15c 📝 Update changelog 2022-05-26 08:36:55 +00:00
Ju4tCode
fa3ed2b58c improve plugin system (#1011) 2022-05-26 16:35:47 +08:00
github-actions[bot]
579839f2a4 📝 Update changelog 2022-05-24 11:34:00 +00:00
shoucandanghehe
daa95d02ac 🍻 publish plugin TETRIS Stats (#1008) 2022-05-24 19:32:52 +08:00
github-actions[bot]
33c261c802 📝 Update changelog 2022-05-24 04:37:58 +00:00
Jigsaw
558e073cfa 📝 Add style guides for document (#1005) 2022-05-24 12:36:59 +08:00
github-actions[bot]
c52637a3b8 📝 Update changelog 2022-05-24 02:43:21 +00:00
kexue-z
e3d1f572ed 🍻 publish plugin 签到插件 (#1006) 2022-05-24 10:42:16 +08:00
github-actions[bot]
af86b96974 📝 Update changelog 2022-05-22 17:16:30 +00:00
snowyfirefly
ddc42f7be8 🍻 publish bot LiteyukiBot-轻雪机器人 (#1002) 2022-05-23 01:15:18 +08:00
github-actions[bot]
41d50641ad 📝 Update changelog 2022-05-22 11:43:31 +00:00
Ju4tCode
6feed0610b 🐛 fix union validation error (#1001) 2022-05-22 19:42:30 +08:00
github-actions[bot]
fe43cc92a5 📝 Update changelog 2022-05-21 14:04:12 +00:00
Ju4tCode
f6fb3b3970 📝 Docs: 更新 require 样例 (#996) 2022-05-21 22:03:19 +08:00
github-actions[bot]
d8ea7f1e6f 📝 Update changelog 2022-05-21 11:42:46 +00:00
Mix
dd55650f0a 📝 Update QQ Channel icon in README.md (#997)
* 📝 Update QQ Channel icon

* 📝  Minimize QQ Channel icon SVG
2022-05-21 19:41:50 +08:00
github-actions[bot]
20f414d0de 📝 Update changelog 2022-05-21 03:18:43 +00:00
AkiraXie
5924f1e7ac 📝 Docs: 调整跨插件访问文档 (#993)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-05-21 11:17:27 +08:00
github-actions[bot]
2fbd44eef9 📝 Update changelog 2022-05-21 03:15:19 +00:00
kexue-z
0adf4d6934 🍻 publish plugin 数据库连接插件 (#994) 2022-05-21 11:14:19 +08:00
github-actions[bot]
2c91a4c7f1 📝 Update changelog 2022-05-21 02:54:46 +00:00
NumberSir
625c72ab0d 🍻 publish plugin 百度翻译 (#991) 2022-05-21 10:53:47 +08:00
github-actions[bot]
449a2c5f96 📝 Update changelog 2022-05-21 01:15:31 +00:00
AkashiCoin
9e64f3f8ab 🍻 publish plugin MockingBird语音 2022-05-21 09:14:27 +08:00
github-actions[bot]
9b45b77894 🔖 Release 2.0.0-beta.3 2022-05-20 10:21:32 +00:00
Ju4tCode
e890453870 🔖 bump version 2.0.0-beta.3 (#990) 2022-05-20 18:13:50 +08:00
github-actions[bot]
abcea78fcc 📝 Update changelog 2022-05-20 09:35:28 +00:00
Ju4tCode
80594cffb6 🔊 add export deprecation warning (#983) 2022-05-20 17:34:15 +08:00
github-actions[bot]
6d4c5cbc2d 📝 Update changelog 2022-05-20 00:45:25 +00:00
Ju4tCode
d295e9ef6b 🍻 publish plugin imageutils (#985)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-05-20 08:44:16 +08:00
Ju4tCode
2ad46bf97a 🍻 publish bot 屑岛风Bot (#987)
Co-authored-by: kexue-z <kexue-z@users.noreply.github.com>
2022-05-20 08:43:36 +08:00
Ju4tCode
70ddc634f6 Feat: 添加 devcontainer 支持 (#981) 2022-05-19 11:55:56 +08:00
github-actions[bot]
540629aa7c 📝 Update changelog 2022-05-18 23:38:35 +00:00
黯星座
4f4369c712 ✏️ Fix typo in scheduler document (#982) 2022-05-19 07:37:23 +08:00
github-actions[bot]
c6633bc9af 📝 Update changelog 2022-05-18 08:54:30 +00:00
Ju4tCode
1c099b4d13 🍻 publish plugin 摸鱼日历 (#980)
Co-authored-by: A-kirami <A-kirami@users.noreply.github.com>
2022-05-18 16:53:13 +08:00
github-actions[bot]
abe1e29fd9 📝 Update changelog 2022-05-15 05:51:01 +00:00
Ju4tCode
e8b9963ef3 🐛 Fix: 商店搜索失效 (#978) 2022-05-15 13:49:56 +08:00
github-actions[bot]
90f7c153cb 📝 Update changelog 2022-05-15 03:23:24 +00:00
Ju4tCode
983a5930c6 🍻 publish plugin 走迷宫 (#977)
Co-authored-by: EtherLeaF <EtherLeaF@users.noreply.github.com>
2022-05-15 11:22:16 +08:00
github-actions[bot]
ff65f10da9 📝 Update changelog 2022-05-15 02:36:29 +00:00
Ju4tCode
1710d009bb 🍻 publish plugin 语录娱乐 (#973)
Co-authored-by: bingqiu456 <bingqiu456@users.noreply.github.com>
2022-05-15 10:35:20 +08:00
github-actions[bot]
049d988574 📝 Update changelog 2022-05-14 15:16:43 +00:00
Ju4tCode
97fa0b4fe9 🍻 publish plugin 国内新冠疫情数据查询 (#975)
Co-authored-by: nicklly <nicklly@users.noreply.github.com>
2022-05-14 23:15:27 +08:00
github-actions[bot]
cd42385a43 📝 Update changelog 2022-05-14 13:08:07 +00:00
Ju4tCode
56f99b7f0b Feat: 支持 WebSocket 连接同时获取 str 或 bytes (#962) 2022-05-14 21:06:57 +08:00
github-actions[bot]
91c5056c97 📝 Update changelog 2022-05-14 08:55:45 +00:00
Ju4tCode
5e970a291f 🐛 fix di default param eq override (#971) 2022-05-14 16:54:41 +08:00
github-actions[bot]
42a49a20aa 📝 Update changelog 2022-05-13 15:05:36 +00:00
Ju4tCode
a4a329cf87 🍻 publish plugin nonebot_plugin_eventdone (#966)
Co-authored-by: PadorFelice <PadorFelice@users.noreply.github.com>
2022-05-13 23:04:19 +08:00
github-actions[bot]
dc074f35d5 📝 Update changelog 2022-05-13 07:36:59 +00:00
Ju4tCode
591107870e 🍻 publish plugin 幻影坦克图片合成 (#968)
Co-authored-by: RafuiiChan <RafuiiChan@users.noreply.github.com>
2022-05-13 15:35:53 +08:00
github-actions[bot]
94b19b4833 📝 Update changelog 2022-05-13 02:34:52 +00:00
Ju4tCode
1a91371410 🍻 publish plugin 合成字符画(GIF) (#964)
Co-authored-by: RafuiiChan <RafuiiChan@users.noreply.github.com>
2022-05-13 10:33:48 +08:00
github-actions[bot]
a77664297d 📝 Update changelog 2022-05-07 05:17:59 +00:00
StarHeart
b889d2352e 📝 Docs: 添加 QQ 频道链接 (#961)
* 📝 add qq channel badge

* 📝 update qq channel link

* Update README.md

Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-05-07 13:16:43 +08:00
github-actions[bot]
17e09267e0 📝 Update changelog 2022-05-05 02:20:00 +00:00
Ju4tCode
bbf734b2d1 🍻 publish bot ShigureBot (#959)
Co-authored-by: lgc2333 <lgc2333@users.noreply.github.com>
2022-05-05 10:18:41 +08:00
github-actions[bot]
7ab9e85dc0 📝 Update changelog 2022-05-02 11:51:05 +00:00
Ju4tCode
71bfb42fe0 🍻 publish plugin 国际象棋 (#957)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-05-02 19:49:50 +08:00
github-actions[bot]
aeaea54ac1 📝 Update changelog 2022-05-02 05:12:14 +00:00
Ju4tCode
49437daf10 🍻 publish bot Inkar Suki (#955)
Co-authored-by: HornCopper <HornCopper@users.noreply.github.com>
2022-05-02 13:10:56 +08:00
github-actions[bot]
a93401e3b4 📝 Update changelog 2022-05-02 03:01:43 +00:00
MeetWq
e87861983b 🍻 rename nonebot-plugin-chess (#953)
Plugin: nonebot-plugin-chess 改名为 nonebot-plugin-boardgame
2022-05-02 11:00:27 +08:00
github-actions[bot]
2d81d54d93 📝 Update changelog 2022-05-01 02:30:47 +00:00
Ju4tCode
e145d99335 🍻 publish plugin NoneBot2 文档搜索 (#952)
Co-authored-by: MingxuanGame <MingxuanGame@users.noreply.github.com>
2022-05-01 10:29:40 +08:00
github-actions[bot]
7e3a58a0e8 📝 Update changelog 2022-04-30 08:05:57 +00:00
MingxuanGame
11b6e1ba98 📝 Docs: 添加 nonebug 单元测试文档 (#929)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: StarHeart <starheart233@gmail.com>
2022-04-30 16:04:41 +08:00
github-actions[bot]
34186830ab 📝 Update changelog 2022-04-30 06:45:08 +00:00
Ju4tCode
b98be416e4 🍻 publish plugin 中国象棋 (#949)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-04-30 14:44:04 +08:00
github-actions[bot]
aaae928026 📝 Update changelog 2022-04-30 06:41:36 +00:00
@刘作鱼
1e43b4df10 📝 Add deployment document using PM2 (#853)
* Update deployment.md

重新提交 PM2 部署文档

* ♻️ 📝 Refactor PM2 deplotyment document

* ✏️ Fix typo in PM2 deployment document

* Update deployment.md

fix 表述歧义

* 🚚 copy docs to next version

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-04-30 14:40:25 +08:00
github-actions[bot]
505b4d46d0 📝 Update changelog 2022-04-30 02:00:37 +00:00
Mix
95331bbb22 🐛 Fix MessageTemplate improper behavior when no format spec (#947)
* 🧪 Add a test to figure out bug in #938

* ♻️ 🐛 Refactor rich message template formatting, fix #938
2022-04-30 09:59:23 +08:00
github-actions[bot]
f028575f2f 📝 Update changelog 2022-04-28 10:16:51 +00:00
Ju4tCode
252e3de459 🍻 publish plugin B站视频封面提取 (#946)
Co-authored-by: A-kirami <A-kirami@users.noreply.github.com>
2022-04-28 18:15:34 +08:00
github-actions[bot]
6449b1e9fd 📝 Update changelog 2022-04-28 09:59:12 +00:00
Ju4tCode
5334f11902 🍻 publish plugin 一言 (#944)
Co-authored-by: A-kirami <A-kirami@users.noreply.github.com>
2022-04-28 17:57:50 +08:00
github-actions[bot]
76ffcf14e8 📝 Update changelog 2022-04-28 09:49:41 +00:00
Ju4tCode
c8f25db6f6 🍻 publish plugin 答案之书 (#942)
Co-authored-by: A-kirami <A-kirami@users.noreply.github.com>
2022-04-28 17:48:22 +08:00
github-actions[bot]
4845ca10a4 📝 Update changelog 2022-04-28 09:43:10 +00:00
Ju4tCode
eec27a267a 🍻 publish plugin 支付宝到账语音 (#940)
Co-authored-by: A-kirami <A-kirami@users.noreply.github.com>
2022-04-28 17:41:13 +08:00
github-actions[bot]
3870f0084d 📝 Update changelog 2022-04-24 10:08:06 +00:00
kexue
06b36ec278 📝 Docs: 更新 GitHub Action 部署文档 (#937)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-04-24 18:06:57 +08:00
github-actions[bot]
dcfa25c486 📝 Update changelog 2022-04-20 09:03:15 +00:00
Ju4tCode
14953f5161 🍻 publish plugin nonebot-plugin-dida (#934)
Co-authored-by: TDK1969 <TDK1969@users.noreply.github.com>
2022-04-20 17:02:06 +08:00
github-actions[bot]
16f69b045b 📝 Update changelog 2022-04-20 06:44:40 +00:00
MeetWq
533e99418c Feat: 添加 CommandStart 依赖注入参数 (#915)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-04-20 14:43:29 +08:00
github-actions[bot]
f989710cd6 📝 Update changelog 2022-04-18 10:24:27 +00:00
Ju4tCode
e1534f2205 🍻 publish plugin 随机唐可可 (#931)
Co-authored-by: KafCoppelia <KafCoppelia@users.noreply.github.com>
2022-04-18 18:23:13 +08:00
github-actions[bot]
532aee5e71 📝 Update changelog 2022-04-16 15:47:46 +00:00
Nacho
fb047a4987 🍻 change ncm info (#924) 2022-04-16 23:46:37 +08:00
github-actions[bot]
af799aa846 📝 Update changelog 2022-04-16 02:21:11 +00:00
Ju4tCode
91f4daa722 📝 add custom rule guide (#914)
Co-authored-by: StarHeart <starheart233@gmail.com>
2022-04-16 10:20:01 +08:00
Ju4tCode
42fa47263a 🍻 publish plugin splatoon2新闻 (#917)
Co-authored-by: DrinkOolongTea <DrinkOolongTea@users.noreply.github.com>
2022-04-16 10:18:56 +08:00
dependabot[bot]
39fd544651 ⬆️ Bump codecov/codecov-action from 2 to 3 (#911)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-06 20:18:44 +08:00
github-actions[bot]
e5bb30e2b5 📝 Update changelog 2022-04-05 14:21:41 +00:00
Ju4tCode
a6d8f18cf0 🍻 publish plugin nonebot_plugin_draw (#910)
Co-authored-by: bingganhe123 <bingganhe123@users.noreply.github.com>
2022-04-05 22:20:27 +08:00
github-actions[bot]
47d843ddca 📝 Update changelog 2022-04-05 13:54:00 +00:00
Jigsaw
74542d30e0 📝 remove outdated plugins (#902) 2022-04-05 21:52:42 +08:00
pre-commit-ci[bot]
e12445be2f ⬆️ auto update by pre-commit hooks (#908)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-04-05 11:05:37 +08:00
github-actions[bot]
d8eb7d311b 📝 Update changelog 2022-04-05 02:53:11 +00:00
Ju4tCode
7ac14bab03 🍻 publish plugin 扫雷游戏 (#907)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-04-05 10:52:04 +08:00
github-actions[bot]
e2621b4448 📝 Update changelog 2022-04-04 02:36:23 +00:00
Ju4tCode
2f3324ce0c 🐛 Fix: Bot Hook 没有捕获跳过异常 (#905) 2022-04-04 10:35:14 +08:00
github-actions[bot]
494b9c625d 📝 Update changelog 2022-04-01 08:30:51 +00:00
Akirami
f20cf785ce 🏷️ fix some matcher's redundant optional (#904)
Fix: 修复部分事件响应器参数类型中冗余的 Optional
2022-04-01 16:29:44 +08:00
github-actions[bot]
82803ff90f 📝 Update changelog 2022-03-29 02:33:41 +00:00
Ju4tCode
d38b5602a6 🍻 publish plugin 汉兜 Handle (#899)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-03-29 10:32:52 +08:00
github-actions[bot]
adb5fd8ca0 📝 Update changelog 2022-03-28 07:36:06 +00:00
Ju4tCode
977e1de077 🍻 publish plugin 多适配器帮助函数 (#897)
Co-authored-by: iyume <iyume@users.noreply.github.com>
2022-03-28 15:35:01 +08:00
github-actions[bot]
537866db95 📝 Update changelog 2022-03-25 13:19:16 +00:00
Ju4tCode
9ffd78dda3 🍻 publish plugin 语句抽象化 (#894)
Co-authored-by: CherryCherries <CherryCherries@users.noreply.github.com>
2022-03-25 21:18:28 +08:00
github-actions[bot]
599da5158e 📝 Update changelog 2022-03-25 10:25:27 +00:00
hemengyang
2b64e8266c 👷 CI: 修复发布机器人的意外错误 (#892)
* 🐛 fix permission error

* 👷 remove unnecessary push trigger
2022-03-25 18:24:35 +08:00
github-actions[bot]
8ccf10954a 📝 Update changelog 2022-03-25 03:44:05 +00:00
Ju4tCode
09b9a626e6 🍻 publish plugin 快速搜索 (#889)
Co-authored-by: KoishiStudio <KoishiStudio@users.noreply.github.com>
2022-03-25 11:43:06 +08:00
github-actions[bot]
1d221fddab 📝 Update changelog 2022-03-25 03:11:00 +00:00
Ju4tCode
524ed419c2 🍻 publish plugin wordle猜单词 (#891)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-03-25 11:10:10 +08:00
github-actions[bot]
536e75f994 📝 Update changelog 2022-03-25 03:03:58 +00:00
Ju4tCode
130c2ed5c0 🍻 publish plugin MediaWiki查询 (#886)
Co-authored-by: KoishiStudio <KoishiStudio@users.noreply.github.com>
2022-03-25 11:02:54 +08:00
github-actions[bot]
3c8e705bb0 📝 Update changelog 2022-03-24 02:47:44 +00:00
Ju4tCode
87e5e15b52 🍻 publish plugin HikariSearch (#884)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-03-24 10:46:45 +08:00
github-actions[bot]
a2abc5a714 📝 Update changelog 2022-03-23 02:43:18 +00:00
Ju4tCode
de434b3072 🍻 publish plugin 第二个leetcode查询插件 (#882)
Co-authored-by: Nranphy <Nranphy@users.noreply.github.com>
2022-03-23 10:42:15 +08:00
github-actions[bot]
614f005373 📝 Update changelog 2022-03-21 15:05:52 +00:00
MeetWq
36efa3f441 📝 remove or replace some invalid plugins (#879)
* 替代SimpleMusic插件

* 移除失效插件
2022-03-21 23:04:48 +08:00
github-actions[bot]
78a90ef7aa 📝 Update changelog 2022-03-20 11:43:57 +00:00
Ju4tCode
2e5df56d38 🍻 publish plugin 成分姬 (#878)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-03-20 19:42:55 +08:00
github-actions[bot]
a230e98052 📝 Update changelog 2022-03-20 11:41:34 +00:00
Ju4tCode
45e2e6c280 🐛 fix event maybe converted when checking type (#876)
Fix: 修复 event 类型检查会对类型进行自动转换
2022-03-20 19:40:43 +08:00
github-actions[bot]
fcdb05a7e2 📝 Update changelog 2022-03-18 14:49:11 +00:00
Ju4tCode
8c6d5a2d1f 🍻 publish plugin Arcaea查分插件 (#875)
Co-authored-by: SEAFHMC <SEAFHMC@users.noreply.github.com>
2022-03-18 22:48:09 +08:00
github-actions[bot]
8735b61a8d 📝 Update changelog 2022-03-17 13:12:30 +00:00
Ju4tCode
02de6fd266 add rule permission reflected operation support (#872)
Feature: 添加 Rule, Permission 反向位运算支持
2022-03-17 21:11:37 +08:00
github-actions[bot]
06f8dde33c 📝 Update changelog 2022-03-17 02:57:02 +00:00
Ju4tCode
0fe3e4fb16 🍻 publish plugin QQ自动同意好友申请 (#871)
Co-authored-by: ZakiuC <ZakiuC@users.noreply.github.com>
2022-03-17 10:56:17 +08:00
github-actions[bot]
56c6f6a471 📝 Update changelog 2022-03-11 15:41:47 +00:00
Ju4tCode
58e69f7884 🍻 publish plugin 21点游戏插件 (#865)
Co-authored-by: yaowan233 <yaowan233@users.noreply.github.com>
2022-03-11 23:40:54 +08:00
github-actions[bot]
4ebbf7638c 📝 Update changelog 2022-03-11 08:43:18 +00:00
Ju4tCode
8d7507c8f2 🍻 publish plugin 色图生成 (#863)
Co-authored-by: monsterxcn <monsterxcn@users.noreply.github.com>
2022-03-11 16:42:29 +08:00
github-actions[bot]
c1c720756b 📝 Update changelog 2022-03-11 07:48:09 +00:00
Ju4tCode
a1be18f7f4 📝 fix abs link in register adapter (#861)
Docs: 修复适配器文档内商店链接
2022-03-11 15:47:22 +08:00
github-actions[bot]
e2fcfa902e 📝 Update changelog 2022-03-11 04:13:18 +00:00
StarHeart
b54f4c8d4c 📝 Docs: tips for finding adapters' document link (#860)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-03-11 12:12:25 +08:00
github-actions[bot]
4cf07ca2e0 📝 Update changelog 2022-03-10 09:21:58 +00:00
Ju4tCode
5b3dd8f020 🍻 Plugin: bilibili通知插件 (#859)
Co-authored-by: TDK1969 <TDK1969@users.noreply.github.com>
2022-03-10 17:20:46 +08:00
Ju4tCode
9bb291e95b ⬆️ upgrade fastapi to 0.75 (#857) 2022-03-09 11:59:35 +08:00
github-actions[bot]
492c0947c3 📝 Update changelog 2022-03-05 15:36:03 +00:00
Ju4tCode
547d50ad76 🍻 Plugin: 订阅推送管理 (#855)
Co-authored-by: mwbimh <mwbimh@users.noreply.github.com>
2022-03-05 23:35:14 +08:00
github-actions[bot]
f7600a8a62 📝 Update changelog 2022-03-03 09:44:57 +00:00
Ju4tCode
9bd380a3bb 🍻 Plugin: 动漫新闻 (#852)
Co-authored-by: 5656565566 <5656565566@users.noreply.github.com>
2022-03-03 17:43:53 +08:00
github-actions[bot]
8600687f7d 📝 Update changelog 2022-03-02 09:48:28 +00:00
Ju4tCode
1d98ea1961 Plugin: 游戏王卡查 (#846)
Co-authored-by: anlen123 <anlen123@users.noreply.github.com>
2022-03-02 17:47:18 +08:00
dependabot[bot]
998db949da ⬆️ Bump actions/checkout from 2 to 3 (#844)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-02 13:46:22 +08:00
github-actions[bot]
dea1b8c6fa 📝 Update changelog 2022-03-01 04:28:59 +00:00
Ju4tCode
d348f544b1 🔀 Merge pull request #843
Plugin: 二维码识别与发送
2022-03-01 12:28:08 +08:00
kexue-z
b1559eee42 🍻 publish plugin 二维码识别与发送 2022-03-01 04:02:57 +00:00
github-actions[bot]
edc7183c22 📝 Update changelog 2022-02-28 03:56:30 +00:00
Ju4tCode
ea539345cc 🔀 Merge pull request #841
Plugin: mockingbird
2022-02-28 11:55:41 +08:00
Diaosi1111
03d60dd0be 🍻 publish plugin mockingbird 2022-02-28 02:56:03 +00:00
Ju4tCode
48003a779f ⬆️ update fastapi and tomlkit (#837)
Co-authored-by: iyume <iyumelive@gmail.com>
2022-02-27 18:10:20 +08:00
github-actions[bot]
2203b82b09 📝 Update changelog 2022-02-27 07:36:59 +00:00
Ju4tCode
9fc2f7c02e 🔀 Merge pull request #839
Plugin: QQ自动续火花
2022-02-27 15:36:09 +08:00
25252www
6ee295bfd7 🍻 publish plugin QQ自动续火花 2022-02-27 07:09:39 +00:00
mobyw
6b067f0865 🔥 Plugin: 移除 nonebot-general-rss (#836) 2022-02-25 23:18:26 +08:00
Ju4tCode
339c25638b 👷 fix ci error when no changlog performed (#835) 2022-02-25 12:58:38 +08:00
Ju4tCode
e6cd3e57f5 🔀 Merge pull request #834
Plugin: update nonebot-bison
2022-02-25 12:10:19 +08:00
felinae98
69fcda5658 update nonebot-bison
支持beta
2022-02-24 22:55:30 +08:00
github-actions[bot]
88f8614cfc 📝 Update changelog 2022-02-23 07:38:38 +00:00
Ju4tCode
273b302ef2 🔀 Merge pull request #832
Plugin: 每日一句
2022-02-23 15:37:44 +08:00
MelodyYuuka
e86bab74d3 🍻 publish plugin 每日一句 2022-02-23 06:56:46 +00:00
github-actions[bot]
b1df360900 📝 Update changelog 2022-02-22 15:11:23 +00:00
CherryGS
2c271da965 📝 Add note for fastapi_reload option on Windows @CherryGS (#830)
* 添加 `fastapi_reload` 在 win 的额外影响

* 🚨 auto fix by pre-commit hooks

* Update choose-driver.md

* 🚨 auto fix by pre-commit hooks

* 调整格式

* 🚨 auto fix by pre-commit hooks

* Update website/versioned_docs/version-2.0.0-beta.2/tutorial/choose-driver.md

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>

* Update website/versioned_docs/version-2.0.0-beta.2/tutorial/choose-driver.md

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>

* Update website/versioned_docs/version-2.0.0-beta.2/tutorial/choose-driver.md

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>

* Update website/versioned_docs/version-2.0.0-beta.2/tutorial/choose-driver.md

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>

* Update website/versioned_docs/version-2.0.0-beta.2/tutorial/choose-driver.md

Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>

* Update choose-driver.md

* Update choose-driver.md

* 📝 update reload warning

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-02-22 23:10:18 +08:00
github-actions[bot]
5db9c1e232 📝 Update changelog 2022-02-20 19:17:42 +00:00
Ju4tCode
b05ce41b1d 🔀 Merge pull request #829
Plugin: 原神抽卡记录分析
2022-02-21 03:16:57 +08:00
monsterxcn
bda27c78b9 🍻 publish plugin 原神抽卡记录分析 2022-02-20 19:09:12 +00:00
github-actions[bot]
6f0e27ee6d 📝 Update changelog 2022-02-20 15:18:56 +00:00
Ju4tCode
2c89409667 🔀 Merge pull request #825
Plugin: YetAnotherPicSearch
2022-02-20 23:18:01 +08:00
Ju4tCode
5adc5ce1cd 🔀 Merge pull request #826
Chore(deps): bump dependencies
2022-02-20 23:16:28 +08:00
StarHeartHunt
cad2f90b8a ⬆️ bump dependencies 2022-02-20 14:58:53 +08:00
NekoAria
b3d246cfb1 🍻 publish plugin YetAnotherPicSearch 2022-02-19 15:52:45 +00:00
github-actions[bot]
a8a6eb8c93 📝 Update changelog 2022-02-19 10:45:12 +00:00
Ju4tCode
86a73011b1 🔀 Merge pull request #822
CI: 添加更新日志忽略 label 选项
2022-02-19 18:44:23 +08:00
yanyongyu
9fb089bf08 👷 exclude label for changelog 2022-02-19 16:56:41 +08:00
github-actions[bot]
72d993921f 📝 Update changelog 2022-02-19 05:33:28 +00:00
Ju4tCode
db764f7e9e 🔀 Merge pull request #820
CI: 添加 dependabot 配置文件
2022-02-19 13:32:46 +08:00
Mix
1767f7a388 🔧 Add dependabot config file to update GitHub Actions 2022-02-19 11:51:07 +08:00
github-actions[bot]
fc45c67d97 📝 Update changelog 2022-02-19 03:22:02 +00:00
Ju4tCode
87885bd878 🔀 Merge pull request #819
Docs: 修复 ci/cd action 中错误的版本号
2022-02-19 11:21:20 +08:00
Bubbleioa
d75a04b31a fix bobheadxi/deployments version
在 [github action](https://github.com/Bubbleioa/ioa-bot/runs/5250225969) 上出错 
Error: Unable to resolve action `bobheadxi/deployments@v0.6`, unable to find version `v0.6`

最新版本为 v0.6.2
2022-02-19 00:40:01 +08:00
github-actions[bot]
12313204e1 📝 Update changelog 2022-02-18 08:00:02 +00:00
Ju4tCode
4573235583 🔀 Merge pull request #816
Docs: 添加 netlify 标签
2022-02-18 15:59:21 +08:00
yanyongyu
4293bdf21f 🎨 add dark theme support 2022-02-18 15:46:59 +08:00
yanyongyu
baae3e48de 🍻 add netlify badge 2022-02-18 14:53:24 +08:00
github-actions[bot]
24df95ae4a 📝 Update changelog 2022-02-18 06:08:33 +00:00
Ju4tCode
6b50a57348 🔀 Merge pull request #815
Fix: 修复 on_fullmatch 返回类型错误
2022-02-18 14:07:41 +08:00
yanyongyu
6920ec3a11 🏷️ fix fullmatch return type error 2022-02-18 11:12:19 +08:00
github-actions[bot]
6586f28f6a 📝 Update changelog 2022-02-18 03:05:38 +00:00
Ju4tCode
192c8da09c 🔀 Merge pull request #797
Feature: 新增文本完整匹配规则
2022-02-18 11:04:49 +08:00
Mix
1fba27d9b8 Fix failed full match test 2022-02-17 23:50:00 +08:00
Mix
0f0dc0a818 improve full match performance with frozenset 2022-02-17 23:49:47 +08:00
github-actions[bot]
3c3a250180 📝 Update changelog 2022-02-17 08:45:14 +00:00
Ju4tCode
03d33f3bdc 🔀 Merge pull request #814
CI: 分离 pr 预览 action
2022-02-17 16:44:31 +08:00
yanyongyu
1147d67f1a 👷 separate website ci for pr checking 2022-02-17 16:30:01 +08:00
github-actions[bot]
98e5956d44 📝 Update changelog 2022-02-17 08:00:39 +00:00
Ju4tCode
999a6a0e10 🔀 Merge pull request #813
Docs: 减小更新日志 toc 最大显示等级
2022-02-17 15:59:43 +08:00
github-actions[bot]
ff2675b527 📝 Update changelog 2022-02-17 07:56:39 +00:00
Ju4tCode
daa026cfd7 🔀 Merge pull request #812
Fix: 修复 DataclassEncoder 嵌套 encode 的问题
2022-02-17 15:55:56 +08:00
yanyongyu
d4d3962177 🎨 reduce changelog toc level 2022-02-17 15:44:49 +08:00
yanyongyu
1d6a333b49 update dataclass encoder tests 2022-02-17 15:35:39 +08:00
pre-commit-ci[bot]
9c0e05c615 🚨 auto fix by pre-commit hooks 2022-02-17 07:12:29 +00:00
github-actions[bot]
258bdbe403 📝 Update changelog 2022-02-17 07:11:16 +00:00
AkiraXie
c48ddaf0a2 🐛 fix DataclassEncoder bug and add test case 2022-02-17 15:06:26 +08:00
github-actions[bot]
9dd989c627 📝 Update changelog 2022-02-16 15:19:06 +00:00
Ju4tCode
3d84844a58 🔀 Merge pull request #810
Plugin: 60s读世界小插件
2022-02-16 23:18:20 +08:00
bingganhe123
d63d434e0f 🍻 publish plugin 60s读世界小插件 2022-02-16 15:03:00 +00:00
github-actions[bot]
5216a5b8f2 📝 Update changelog 2022-02-16 12:29:53 +00:00
hemengyang
4107affb9b Docs: 修改议题模板中的错误链接 (#807) 2022-02-16 20:29:04 +08:00
github-actions[bot]
b898303e4d 📝 Update changelog 2022-02-16 12:25:05 +00:00
Mix
43aebd9c93 Docs: 修改消息模板文档中错误的样例 (#806)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-02-16 20:24:00 +08:00
github-actions[bot]
fcda5c37d7 📝 Update changelog 2022-02-16 11:45:11 +00:00
Lan
1412385e51 Fix: 修改错误的插件 PyPI 项目名称 (#804)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-02-16 19:44:29 +08:00
github-actions[bot]
00fab9143e 📝 Update changelog 2022-02-16 11:36:28 +00:00
Ju4tCode
5e17d4c2f9 🔀 Merge pull request #798
Docs: 更新贡献指南
2022-02-16 19:35:42 +08:00
github-actions[bot]
d7de928f22 📝 Update changelog 2022-02-16 10:00:23 +00:00
Ju4tCode
83ff7a3a6c 🔀 Merge pull request #805
CI: 减少 action 冗余运行
2022-02-16 17:59:33 +08:00
yanyongyu
6f9c9eb740 🔧 change latest changes to chinese 2022-02-16 17:51:17 +08:00
yanyongyu
6272dfd46a 🔥 remove unused condition 2022-02-16 17:04:12 +08:00
yanyongyu
ab78769822 🐛 fix concurrency missing matrix 2022-02-16 14:41:39 +08:00
yanyongyu
004a308765 👷 disable auto push 2022-02-16 14:28:44 +08:00
yanyongyu
98e0ec27ee ✏️ fix changelog typo 2022-02-16 14:19:26 +08:00
yanyongyu
a491d842db 👷 reduce ci redundant run 2022-02-16 14:17:33 +08:00
github-actions[bot]
722fc6c6e1 📝 Update changelog 2022-02-16 04:12:35 +00:00
Ju4tCode
a5fffe2a4f 🔀 Merge pull request #803
Plugin: pixiv.net p站查询图片
2022-02-16 12:12:10 +08:00
github-actions[bot]
5f1f84327d 📝 Update changelog 2022-02-16 04:05:34 +00:00
Ju4tCode
3d8ac3e789 🔀 Merge pull request #802
CI: fix ci permission error
2022-02-16 12:05:13 +08:00
anlen123
c05eea2b67 🍻 publish plugin pixiv.net p站查询图片 2022-02-16 04:03:22 +00:00
yanyongyu
a6299bec8f 💚 fix ci permission error 2022-02-16 11:49:45 +08:00
Ju4tCode
513c14ee78 🔀 Merge pull request #799
CI: 添加更新日志自动更新 action
2022-02-16 11:30:56 +08:00
yanyongyu
987e44e1d0 👷 update ci config 2022-02-16 11:10:54 +08:00
jigsaw
e7937e5a06 📝 update contributing guide 2022-02-16 10:33:21 +08:00
yanyongyu
962c71ea4e 👷 update changelog ci 2022-02-15 23:39:04 +08:00
Ju4tCode
04e9a50bc1 📝 update contributing guide
Co-authored-by: Mix <32300164+mnixry@users.noreply.github.com>
2022-02-15 23:03:56 +08:00
yanyongyu
4bd1b92e9f 📝 update contributing guide 2022-02-15 21:10:07 +08:00
pre-commit-ci[bot]
f737bb899c 🚨 auto fix by pre-commit hooks 2022-02-15 00:27:43 +00:00
Akirami
9f12404338 add full match Matcher 2022-02-15 08:20:29 +08:00
214 changed files with 11179 additions and 5691 deletions

15
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
# ** [Optional] Uncomment this section to install additional packages. **
# USER root
#
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
USER codespace
# [Required] Poetry
RUN curl -sSL https://install.python-poetry.org | python - -y
RUN poetry config virtualenvs.in-project true

View File

@@ -0,0 +1,98 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux
{
"name": "GitHub Codespaces (Default)",
"build": {
"dockerfile": "Dockerfile"
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go",
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
"python.linting.enabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"python.analysis.diagnosticMode": "workspace",
"python.analysis.typeCheckingMode": "basic",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"lldb.executable": "/usr/bin/lldb",
"files.exclude": {
"**/__pycache__": true
},
"files.watcherExclude": {
"**/target/**": true,
"**/__pycache__": true
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"GitHub.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
},
"remoteUser": "codespace",
"overrideCommand": false,
"mounts": [
"source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
],
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined",
"--privileged",
"--init"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// "oryx build" will automatically install your dependencies and attempt to build your project
"postCreateCommand": "poetry install && poetry run pre-commit install && yarn install"
}

View File

@@ -4,11 +4,11 @@ contact_links:
url: https://discussions.nonebot.dev/
about: Ask questions about nonebot
- name: Plugin Publish
url: https://v2.nonebot.dev/store.html
url: https://v2.nonebot.dev/store
about: Publish your plugin to nonebot homepage and nb-cli
- name: Adapter Publish
url: https://v2.nonebot.dev/store.html
url: https://v2.nonebot.dev/store
about: Publish your adapter to nonebot homepage and nb-cli
- name: Bot Publish
url: https://v2.nonebot.dev/store.html
url: https://v2.nonebot.dev/store
about: Publish your bot to nonebot homepage and nb-cli

View File

@@ -5,22 +5,27 @@ inputs:
python-version:
description: Python version
required: false
default: "3.9"
default: "3.10"
runs:
using: "composite"
steps:
- uses: actions/setup-python@v2
- id: python
uses: actions/setup-python@v2
with:
python-version: ${{ inputs.python-version }}
architecture: "x64"
- uses: Gr1N/setup-poetry@v7
- id: poetry-cache
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
shell: bash
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
path: ${{ steps.poetry-cache.outputs.dir }}
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
- run: poetry install -E all
shell: bash

6
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily

View File

@@ -1,37 +1,34 @@
header: |
### Documentation
See: https://v2.nonebot.dev
template: |
### 💫 Changes
$CHANGES
template: $CHANGES
category-template: "### $TITLE"
name-template: "Release v$RESOLVED_VERSION 🌈"
tag-template: "v$RESOLVED_VERSION"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
change-template: "- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))"
change-title-escapes: '\<&'
exclude-labels:
- "dependencies"
- "skip-changelog"
categories:
- title: "💥 Breaking Changes"
- title: "💥 破坏性变更"
labels:
- "Breaking"
- title: "🚀 Features"
- title: "🚀 新功能"
labels:
- "feature"
- "enhancement"
- title: "🐛 Bug Fixes"
- title: "🐛 Bug 修复"
labels:
- "fix"
- "bugfix"
- "bug"
- title: "📝 Documentation"
- title: "📝 文档"
labels:
- "documentation"
- title: "🍻 Plugin Publish"
- title: "💫 杂项"
- title: "🍻 插件发布"
label: "Plugin"
- title: "🍻 Bot Publish"
- title: "🍻 机器人发布"
label: "Bot"
- title: "🍻 Adapter Publish"
- title: "🍻 适配器发布"
label: "Adapter"
version-resolver:
major:

View File

@@ -4,16 +4,18 @@ on:
push:
branches:
- master
- dev
pull_request:
jobs:
test:
name: Test Coverage
runs-on: ${{ matrix.os }}
concurrency:
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
cancel-in-progress: true
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10"]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
env:
@@ -21,7 +23,7 @@ jobs:
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup Python environment
uses: ./.github/actions/setup-python
@@ -34,7 +36,7 @@ jobs:
poetry run pytest -n auto --cov-report xml
- name: Upload coverage report
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
with:
env_vars: OS,PYTHON_VERSION
files: ./tests/coverage.xml

View File

@@ -1,12 +1,9 @@
name: NoneBot2 Publish Bot
on:
push:
branches:
- master
issues:
types: [opened, reopened, edited]
pull_request:
pull_request_target:
types: [closed]
jobs:
@@ -15,12 +12,12 @@ jobs:
name: nonebot2 publish bot
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
- name: NoneBot2 Publish Bot
uses: nonebot/nonebot2-publish-bot@main
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
with:
token: ${{ secrets.GH_TOKEN }}
config: >

View File

@@ -14,24 +14,46 @@ jobs:
update-release-draft:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
concurrency:
group: pull-request-changelog
cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- uses: release-drafter/release-drafter@v5
id: release-drafter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# TODO
# - name: Update Changelog
# run: |
# echo ${{ steps.release-drafter.outputs.body }}
- name: Update Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
changelog_file: website/src/pages/changelog.md
latest_changes_position: '# 更新日志\n\n'
latest_changes_title: "## 最近更新"
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
changelog_body: ${{ steps.release-drafter.outputs.body }}
commit_and_push: false
- name: Commit and Push
run: |
yarn prettier
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add .
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
git push
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup Python Environment
uses: ./.github/actions/setup-python

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: master
token: ${{ secrets.GH_TOKEN }}
@@ -22,18 +22,26 @@ jobs:
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Archive Files
run: yarn archive $(poetry version -s)
- run: echo "TAG_NAME=v$(poetry version -s)" >> $GITHUB_ENV
# TODO
- name: Archive Changelog
run: cat CHANGELOG.md
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
changelog_file: website/src/pages/changelog.md
archive_regex: '(?<=## )最近更新(?=\n)'
archive_title: ${{ env.TAG_NAME }}
commit_and_push: false
- name: Archive Files
run: |
yarn archive $(poetry version -s)
yarn prettier
- name: Push Tag
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add .
git commit -m ":bookmark: Release $(poetry version -s)"
git tag v$(poetry version -s)
git tag ${{ env.TAG_NAME }}
git push && git push --tags

View File

@@ -4,21 +4,16 @@ on:
push:
branches:
- master
- dev
pull_request_target:
jobs:
publish:
runs-on: ubuntu-latest
concurrency:
group: website-deploy-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v2
if: github.event_name == 'push'
- uses: actions/checkout@v2
if: github.event_name != 'push'
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/checkout@v3
- name: Setup Python Environment
uses: ./.github/actions/setup-python
@@ -35,27 +30,15 @@ jobs:
- name: Get Branch Name
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
- name: Get Deploy Name
if: github.event_name == 'push'
run: |
echo "DEPLOY_NAME=${{ env.BRANCH_NAME }}" >> $GITHUB_ENV
echo "PRODUCTION=${{ env.BRANCH_NAME == 'master' }}" >> $GITHUB_ENV
- name: Get Deploy Name
if: github.event_name != 'push'
run: |
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
echo "PRODUCTION=false" >> $GITHUB_ENV
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1
with:
publish-dir: "./website/build"
production-deploy: ${{ env.PRODUCTION }}
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
deploy-message: "Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}"
enable-commit-comment: false
alias: ${{ env.DEPLOY_NAME }}
alias: ${{ env.BRANCH_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}

45
.github/workflows/website-preview.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Site Deploy(Preview)
on:
pull_request_target:
jobs:
preview:
runs-on: ubuntu-latest
concurrency:
group: pull-request-preview-${{ github.event.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Python Environment
uses: ./.github/actions/setup-python
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build API Doc
uses: ./.github/actions/build-api-doc
- name: Build Doc
run: yarn build
- name: Get Deploy Name
run: |
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1
with:
publish-dir: "./website/build"
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
enable-commit-comment: false
alias: ${{ env.DEPLOY_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}

4
.markdownlint.yaml Normal file
View File

@@ -0,0 +1,4 @@
MD013: false
MD024: # 重复标题
siblings_only: true
MD033: false # 允许 html

View File

@@ -1,22 +1,32 @@
default_install_hook_types: [pre-commit, prepare-commit-msg]
ci:
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
autofix_prs: true
autoupdate_branch: dev
autoupdate_schedule: weekly
autoupdate_branch: master
autoupdate_schedule: monthly
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
stages: [commit]
- repo: https://github.com/psf/black
rev: 22.1.0
rev: 22.8.0
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.5.1
rev: v3.0.0-alpha.0
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown]
types_or: [javascript, jsx, ts, tsx, markdown, yaml]
stages: [commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.3
hooks:
- id: nonemoji
stages: [prepare-commit-msg]

View File

@@ -22,34 +22,61 @@ NoneBot2 还未进入正式版,欢迎在 Issue 中提议要加入哪些新功
NoneBot 使用 [poetry](https://python-poetry.org/) 管理项目依赖,由于 pre-commit 也经其管理,所以在此一并说明。
下面的命令能在已安装 poetry 和 npm 的情况下帮你快速配置开发环境。
下面的命令能在已安装 poetry 和 yarn 的情况下帮你快速配置开发环境。
```sh
```bash
# 安装 python 依赖
poetry install
# 安装 pre-commit git hook
pre-commit install
npm -g i gitmoji-cli
gitmoji -i
```
### 使用 GitHub CodespacesDev Container
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
### Commit 规范
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。
NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。
或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commitnonemoji 已默认作为项目开发依赖安装。
```bash
nonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...]
```
### 工作流概述
`master` 分支为 NoneBot 的开发分支,在任何情况下都请不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文,以便自动生成更新日志。
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。
### 撰写文档
NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server如下所示
```sh
```bash
yarn install
yarn start
```
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你“Hello, World !”。`
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件。
### 参与开发
NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
@@ -63,9 +90,3 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。
同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。
## Git 工作流
`dev` 分支为 NoneBot 的开发分支,如无特殊情况请将更改提交到该分支。
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `dev` 分支发起 Pull Request注意遵循先前提到的 commit message 规范创建 commit。

View File

@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://pypi.python.org/pypi/nonebot2">
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
</a>
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python">
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="python">
<a href="https://codecov.io/gh/nonebot/nonebot2">
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
</a>
@@ -29,27 +29,36 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/>
</a>
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" />
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
</a>
<br />
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="ding">
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://core.telegram.org/bots/api">
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram">
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
</a>
<a href="https://open.feishu.cn/document/home/index">
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAk1BMVEX///8zMzNJSUlSUlJcXFxtbW0zMzNLS0szMzMzMzNBQUGVlZUzMzM1NTU0NDQzMzMzMzM0NDQ0NDQ0NDQ3NzdDQ0M0NDQ2NjY4ODg9PT0zMzM0NDQ5OTk7OzszMzM0NDQ3NzczMzM0NDQ0NDQ0NDQ0NDQ3Nzc2NjY4ODg2NjY7Ozs0NDQ6Ojo6Ojo3Nzc4ODgzMzNGdMWJAAAAMHRSTlMD6h0TDgr8GNf0KQbvhLT45KKTmm4jwHVJLdLFQzbcjFTgzsq7rl58T2kyqD46Y1riMDRhAAAFr0lEQVR42uzZWXKiUACF4YMyqKAQhyjOc7STmLP/1bVlLukESIJ3sLGKbwFU/Q8HuIBKpVKpVCqVSqVSqVQqlUqlUvmNM10Mcfda/U6TPdw3e9lb8ayLO+bPniYu+amjNcPd8U7PFhML0RE5uCvnaY/5SVt0WFvckcu0vxjiYmDxbu5cl2mn9UVHRMa4B2LaP3RYKD1vL6adccRFLSLL/izxxbRz7UXHimdLlFdq2mlvnztYRznZh96cP3G/dkxQRrOnR5c/c5eiQ+S2UTbe/sHir9zD1w6+okz8aXvMItyRqE46ApSHmHYRYdLRoPCMcrAP3TkLC6fpDp5QAn/EtItqij3UG/zgQZH5aWc7ZqJjzA9jKFGf9ppXC3I6uMB/Mzh2mpQQ/Mnp4BSy1Kctx4pFx5qfhA4kqE87pCyrldfBDm6sLqat2mGnttXHDfkvYtryooHo2PCrFm5lcNw1qWr1XUeEm7BH3QYVRJNGcOmoietNmNKDWeKFnCo6b3Wc1drW/NsOLpFRqmmT4xgfPFw42Q7XhkFi2kq2DtKcR2Y8wpTacRdQ3aZYB59ggiOmrS6sFevgDNr9GW6pzRAZdsQsC3rV3x4i6uQha8+sB2h9am9c6rVBDj9ixr5k007rIs+CGV65pl3wXjRi2hrKYjFtM/rI02JaW3XaPYtGtZHHY9qL0lN7QuO2yBMzpenLTvtkZNos+AY1ZcpObtoLtWmrj6TNlCOuJqZ9M3PkaDBlIBHCmwpHyHpjSgMS2ryhcIqsmsWULmR0eTPhK7LsMdMOKHdJM+nw8E+8ZoYDOT3eRDDDuz6HNt7VeszaQtYDJch+38WRZ51TDO+0Y54hylzy0XHib2JI83c0zIoLd1hAeUus1jenQe2HQ79Dg6wB3i1d/uoNpS2JrulgHWqcRxqySjoObrFjfUlLVrVrOtiGMmdCA+ZJx8hlEa9QZ2+oXcNLOkIWEUAHe22sYxqykGdoUV//5w6eoKlkTI3Gdbx7CVmQB10lDWqzSTpemyyoAW28ubYO++oOLqBPbUUtJknHrMnCRihdyaOTdAQsLHSgtSTS2BEHLK4DvQYWFW2lOtiHZi3Fko6fXCjgNVooV0nHl7tMBP1aAaXtJDvYgwGxdMmzLzu1JUyYNSU7IAwiiZ8OJrxKlTzI38QnMORFoqSn8Fh9gikvIa/UVejgDMZMQ9mOOa8WwKCRyysslF6hn2HSwZX4+ew1KGEPCSZKhoqHMw9mLd1rO8aUMYZpexbRV/2AsYBxy7/t3NtuglAQheFR6wEPVEQtaq1WxQNqnfd/urY08QJFYHZS15D9vcHckMzOz/QWA9/3jqHrbmbr1bT10a90ncQcoiclgKY/Vq81q6P2JJqfI+NHPqdDSMRzsEtIXmYGcQcQk2fwKgHxTCIVJGMWwTu6sWGxPSFx+QpkOfz3QcYEJWQhtGsbR5aKCIrHInjXNsSDeITFZ6ELYZEMAnltY8AyawKz4KJAr21IBzkRmB6LOIRGOEhIaHYsciA0uxIshwa/DLQIzrAEy2HswBIBwck9yNOvbWT4YgHEU4zbEiyHsQsXhnmKccmxp2cbxvb8CyDbMBXwD4hsw1BQguUw9s4Mk20YOTFQtmHiDJVtGJhjZRtyEVi2ITbhnLBOMd5qOvqXwz9RFy3bkJpU0LINeTCsJdvIztHVZhsJo77SbOPG6FNltpFQqMxsE7hmS+9ymJxE7XKYUGupyzZS1Kbaso00tbWybONBTadyObyjPlaVbTycRFO28Uh9oyjbEJ/E2JImnVDXy1y6zpHvW5E2npJsI5unI9vIwVe3HKYZaMg2clkoyDby6Wl5mcv0Bp9t5DVEzzZyG4JnG/kdsLONArbQ2UYRlwZwtlHIsoGbbRSdRNtymGbf0LYcpgleQbMNwdUCbcthmrP2j++VjqdSy7Isy7Isy4LxDTcBnqEPd5jdAAAAAElFTkSuQmCC" alt="feishu">
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
</a>
<a href="https://docs.github.com/en/developers/apps">
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
</a>
<a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC+lBMVEUAAAApRHRAbvYyVI4dMlsNGjcvTH5anN0/a5xdo+Vdpvpms/dMfvdcoPZjr/hJfu5krvA9Z+8/a+M9adxQjNdYmdVAbdU4XtRRjcxFd8wyVbM+aZowUJwpRJlZneRIevU5YORWl+1fqOZVle9ZnOpEdOk/aelepus/a+NQiuVKgeQ6YeRQi905X91EdtxAbthZnNlRjdk5YdpQjuhLgehhquc7Y+c6Y8k3XctWlcs0V8tJfMMzVsM9aMJVksdHebVKgLQyVJknP5REdKD///9irv9Ccf5EdP9GfP9jsf7+/f5SkP9mtf9GeP/+/vxJfv5Ukv9Ng/9boP9Gef1ls/9Ulf9Wl//+//5KgP9Nhv9dpP9Oiv9Tk/5Abv9Lg/9Fdv9NiP7///5Qj/7+/fxfpv9Nh/xhq/9QjP9Wm/79//1co/9ir/5fqf5bov9Qi//8/f1XmP1Wl/1ksv/7/v38/vv9/vlgqv9Znf9IfPzZ5fdeqP9nt/5anv5Xmf9Uk/1anvxpuf73/P34+/tot/9hrf1anvpMhvpSjv1pt/9ZnP1aofxTkfxNivza5/dOhPf+//1Ri/z+//vz+fvw9/tIf/v6/fpdofrI2PlYmfnF3Pe0y/Pm7/jW4/e0zvRapPxXlfpTkvpGevri7ffS3vVMiP5XnvxXnPtQivn3/Ph0r/dgpPZ1qfXi6/S9z/NYmf/z+v5Yn/7L2vlfmflUk/hXi/dRgvSw0fGnu+1dqP3t9Pthq/vA1vjc6vfU4vd4sfdepPdsn/dam/fU5fawzfRjjfRhi/NJhP0/a/y80fhTjfhrrfeXvvZuk/WyyPR9rvSqz/OcwfOmv/OSuPO0yfKguPCWr/BPi+9MheRKgv1gsPzq9PpgpvpVjvrN2/jG1vdonvbA0PSrx/OtxPCkxPDp8PvN4ftnuPuCufloqPiex/a3z/WPuvWQqvR2mfRplvReivSOqvOKufKGufKCrPKBoPJzlfFjr+88Ze6ftux8nOxxq/SGr/CFr/Cvx+3R19+QAAAARHRSTlMAEP4ZEwUEvC3S/v7+/Pv08PDXvJqampqSgEtJLS3f/Pz4+O/n5+bl4tTU1Lq6ure2trGdnZ2dlI6Afl5eXldMS0lJLRAR2gcAAAewSURBVFjDlZcFfBJRHMdvdnd3d3cn1qk3h4Et7EQ965h3KKgTQUGdOgYMHII6a5vO2d3d3d3d3X4+/t974A51xvfDu7vfP348HsdxR6VAulaN6lUu8SHVgQOpPpSoXK9Rq3TUf5CmSdWcB/oFcCBn1SZp/rG9aP6c/X5LzvxF/6W9Vqq2KZKq1t8sgvKT9pQt8gf9qb9QOVLWjux+K8oVSrE9bYNU7f6BVA3SpjD93O3+kdy//Ripcw34QefOnaViwM8iV+rf9Ffo7Kdr166du87vSgQGZGcIJocq/OIQlAv1EbAHDL/0hXzOhFxBP61f7vbJdJ0PFfN1RBB0MIPAUO7Alazvz+l0WY4+Pbthw4az66QG63Do6dEsuh+W9QO+//moVRet071L8phomUzBGPcGBwe3hxdmn5FVyGS0yZP0TodpHzy/kGQBygYjonUvbtpFraDwGUjYZ2JRVNSKK26+0EXjWNnkZciH26OjPy3gOZ5nVVCq+p0BzfK8wC/4FB2MLfL5+4sU7wkER7/dhN6aYxia0zKmvT0l7DUxHE1zHMcoVLJNb6ODUbB4EYpQHddcPHpXK5OpWNHp8CxbtmzmPqnBvpnLAIeTQdPT3j16EUer+yaQpT8i8pqAVi9h4cu1kZGR69bt7y9h/7p1EFz7cmEC+iTiwkgczUKmkC8ECkIs54wiI4j2j5EXQwj9QyCBNgCRIZbIj3ZRYET+nCVkP4TwKqTJHoJTXlbFscZzlv6WXr1CkpGKXhb0PozVynoj1SiTHV3lmvYC1JZnCQzNOG9a1BY1aAlKqbBYLEm8jWYSnpGypmBQTQ0sUp9iVCqr/bl60SK1UqlchIIzYCABB/BCIRR/breqVMwp9SIlUA2u39mVCPUWBlZQHz8DiT591t5euPD2WiUR6rXfiAKhnBHvhXVkrs9AGWX2dFTLGX0AZfxCTiaTHYwChTjl1LL8HZ+YccvJa52nfCL+IHyT3Lb4kVi1pAqOBPqMjPfSYHBmZBQSI6Nm0TTDXicCFCsy9HWfiDqGDPTxIzEFqTqDMVF6BTIYHEXUKhDMrMGEKddstIr2qajQYzT6sIeIrENVCsVc1VuRQeiU0FAYU4gBEkhfY5CBT4TCDJABkZWoCx0xxGB26BSspqyiFSowwAKUSNvoWUQQA1p/NRTLC9R5El+jZ5BBR9Qzffr0VbTICrNiQSC9yskKrN9g+hkoVOljp2N5njrUBdExlhj07UI4qGVZ7ePpREw/KLJa5xxfqu9sBRjMjCXJQ5SvaI3ehg0mELlz94oVu3ei3IQJE2J3eu0rdt/AAvAZoEMwp2L7Iias0aOlAYO+hHmrV89De5KVqAndicEaUhlLremOCJuHDeZ0D+v+V4jBPFK5hjrfA+E3CAvr8RfCwvwGWJ6nLmgQYTF6Gr4cZACCELN6Z+Kbr1/eJO5cHaOBBAyNpkeYZg86kWbGgAAuUBXDw8OXh4fH6BlkoNGgY43mxumkbR6HySiIomBKcHi2JZ2+odHELIdSzR5YbevMmHBMRar2EEyMXgXxOcvD4Viza88mnuNYhmNpDMMwIr9pzy4NJMOXIwMGDHBfbaog3k+LQzNgHscNmTZkSNwut0mgaQH6OK0NtmDG8yb3rrghY8YMiZuD5jozbhoSQwpShaeNAaYt3obWYIt58TQkzKe3JYisKCoUNKegwUFI8J7257agSi8SIAtT6UqPRxjc+De2axocLzYbzObjSVtX8Dyj1TICv2Jr0nEzsHLMmPFxZvS75d2GMSDGl4bbxypyuXy8wXBLUMloe6JBvtJgGB8BjitdiUfmuO/dc885kuhaaRg/fiVUwUi00zKVcAuqoLEKRVGN5UCE4bhJRdPiFrPZJZcQESEPwOUybxFpWmU6bsCZxuiyXnKEfIQ8wjWTZxSs6WRExNwRfiCByn4IOSRPwr+kit/qikChkvjmNc/Qob2HznWd5G1WgXbc2TF36A9GkI0fqLrj4AWrjT/pmju0d++heShEhqy9EUt3CwKso9F75OHSHTuWLp3bW8LcpSj28IjXCCsoCLuv4GjWDBSm5sCBSG6/zMNJDldjh2fBggVbEwdicOXAz1sh5HHwrBbON3bTdpKoCc1kCrg00xMHzICzwanD0VrTkoESlpi06L/dZuMUMoXjSaaNKIgmQMgzFti48corj1aEk88KJwpjXDJWwhIjSysUVi0HJ4bn1ZWNG1EwT/ItTpnhhCVuO8sKCjDgjNuHSwAD9M8lsJzdvcQXKyO502ueaTQwaNDmza/dl50+g9EStsMMwMB52f168+bho4dDKFNzSkKBQX4mH37w6MT69etPHCaacBiHHj04PHnQoNGkrkDgjWb6NslMJrSRhjZPHhQYSp+WCiCoWHJu2LBhl2BIDSASGMoY9MvNdjFfKUA2RCRHpQbFfne7nrHDqA4dOozCG/Qie6mAQURG6P+VoPS4DED7wONAkT4ohUeeAtm6/QPZCqSlUqJF+U6dOnVDA23Q/ldRvsUfH/vyZuv0R7LlDaL+TOsamSdNmjTuBwEic43W1N/JkDfH1EmIqT78xznyZvjXh+9m6XNM/Ikc6Zulof6DdIUb1s1Y6n3m+/czvy+VsW7Dwik9/n8HzjZEy9x05tIAAAAASUVORK5CYII=" alt="QQ频道">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk">
</a>
</a>
<br />
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
<img src="https://img.shields.io/badge/qq%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat">
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat Group">
</a>
<a href="https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-5492ff?style=flat-square" alt="QQ Channel">
</a>
<a href="https://t.me/botuniverse">
<img src="https://img.shields.io/badge/telegram-botuniverse-blue?style=flat-square" alt="Telegram Channel">
@@ -86,17 +95,28 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
- 生而可靠100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
- [OneBot 协议](https://onebot.dev/) (QQ 等)
- [钉钉](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p)
- [Telegram](https://core.telegram.org/bots/api)
- [飞书](https://open.feishu.cn/document/home/index)
- [QQ 频道](https://bot.q.qq.com/wiki/)
- 坚实后盾:支持多种 web 框架,可自定义替换
- [FastAPI](https://fastapi.tiangolo.com/)
- [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask)
- [aiohttp](https://docs.aiohttp.org/en/stable/)
- [httpx](https://www.python-httpx.org/)
- [websockets](https://websockets.readthedocs.io/en/stable/)
| 协议名称 | 状态 | 注释 |
| :---------------------------------------------------: | :--: | :----------------------------------------------------------------: |
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
| Console | ✅ | 控制台交互 |
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
| 驱动框架 | 类型 |
| :--------------------------------------------------------: | :----: |
| [FastAPI](https://fastapi.tiangolo.com/) | 服务端 |
| [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask) | 服务端 |
| [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 |
| [httpx](https://www.python-httpx.org/) | 客户端 |
| [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 |
更多:[概览](https://v2.nonebot.dev/docs/)
@@ -180,5 +200,5 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
感谢以下开发者对 NoneBot2 作出的贡献:
<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2" />
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" />
</a>

View File

@@ -11,10 +11,12 @@
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
@@ -24,9 +26,10 @@
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
- `export` => {ref}``export` <nonebot.plugin.export.export>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
- `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter:
@@ -37,19 +40,21 @@ FrontMatter:
import importlib
from typing import Any, Dict, Type, Optional
import pkg_resources
import loguru
from nonebot.log import logger
from nonebot.adapters import Bot
from nonebot.utils import escape_tag
from nonebot.config import Env, Config
from nonebot.log import logger, default_filter
from nonebot.drivers import Driver, ReverseDriver, combine_driver
try:
import pkg_resources
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
__version__ = _dist.version
VERSION = _dist.parsed_version
except pkg_resources.DistributionNotFound: # pragma: no cover
except Exception: # pragma: no cover
__version__ = None
VERSION = None
@@ -169,8 +174,7 @@ def get_bots() -> Dict[str, Bot]:
bots = nonebot.get_bots()
```
"""
driver = get_driver()
return driver.bots
return get_driver().bots
def _resolve_dot_notation(
@@ -204,6 +208,15 @@ def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
return combine_driver(DriverClass, *mixins)
def _log_patcher(record: "loguru.Record"):
record["name"] = (
plugin.name
if (module_name := record["name"])
and (plugin := get_plugin_by_module_name(module_name))
else (module_name and module_name.split(".")[0])
)
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
@@ -230,7 +243,9 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
_env_file=_env_file or f".env.{env.environment}",
)
default_filter.level = config.log_level
logger.configure(
extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher
)
logger.opt(colors=True).info(
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
)
@@ -238,7 +253,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
)
DriverClass: Type[Driver] = _resolve_combine_expr(config.driver)
DriverClass = _resolve_combine_expr(config.driver)
_driver = DriverClass(env, config)
@@ -259,7 +274,7 @@ def run(*args: Any, **kwargs: Any) -> None:
from nonebot.plugin import on as on
from nonebot.plugin import export as export
from nonebot.plugin import on_type as on_type
from nonebot.plugin import require as require
from nonebot.plugin import on_regex as on_regex
from nonebot.plugin import on_notice as on_notice
@@ -273,6 +288,7 @@ from nonebot.plugin import on_endswith as on_endswith
from nonebot.plugin import CommandGroup as CommandGroup
from nonebot.plugin import MatcherGroup as MatcherGroup
from nonebot.plugin import load_plugins as load_plugins
from nonebot.plugin import on_fullmatch as on_fullmatch
from nonebot.plugin import on_metaevent as on_metaevent
from nonebot.plugin import on_startswith as on_startswith
from nonebot.plugin import load_from_json as load_from_json
@@ -282,5 +298,7 @@ from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
__autodoc__ = {"internal": False}

View File

@@ -25,7 +25,6 @@ from pydantic.env_settings import (
)
from nonebot.log import logger
from nonebot.utils import escape_tag
class CustomEnvSettings(EnvSettingsSource):
@@ -56,7 +55,7 @@ class CustomEnvSettings(EnvSettingsSource):
if env_path.is_file():
env_file_vars = read_env_file(
env_path,
encoding=env_file_encoding,
encoding=env_file_encoding, # type: ignore
case_sensitive=settings.__config__.case_sensitive,
)
env_vars = {**env_file_vars, **env_vars}
@@ -83,15 +82,16 @@ class CustomEnvSettings(EnvSettingsSource):
d[field.alias] = env_val
if env_file_vars:
for env_name, env_val in env_file_vars.items():
if (env_val is None or len(env_val) == 0) and env_name in env_vars:
for env_name in env_file_vars.keys():
env_val = env_vars[env_name]
if env_val and (val_striped := env_val.strip()):
try:
if env_val:
env_val = settings.__config__.json_loads(env_val.strip())
env_val = settings.__config__.json_loads(val_striped)
except ValueError as e:
logger.opt(colors=True, exception=e).trace(
f"Error while parsing JSON for {escape_tag(env_name)}. Assumed as string."
logger.trace(
"Error while parsing JSON for "
f"{env_name!r}={val_striped!r}. "
"Assumed as string."
)
d[env_name] = env_val

View File

@@ -4,7 +4,7 @@ FrontMatter:
sidebar_position: 9
description: nonebot.consts 模块
"""
from typing_extensions import Literal
from typing import Literal
# used by Matcher
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
@@ -28,6 +28,8 @@ RAW_CMD_KEY: Literal["raw_command"] = "raw_command"
"""命令文本存储 key"""
CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
"""命令参数存储 key"""
CMD_START_KEY: Literal["command_start"] = "command_start"
"""命令开头存储 key"""
SHELL_ARGS: Literal["_args"] = "_args"
"""shell 命令 parse 后参数字典存储 key"""

View File

@@ -6,15 +6,31 @@ FrontMatter:
"""
import abc
import asyncio
import inspect
from typing import Any, Dict, List, Type, Generic, TypeVar, Callable, Optional
from dataclasses import field, dataclass
from typing import (
Any,
Dict,
List,
Type,
Tuple,
Generic,
TypeVar,
Callable,
Iterable,
Optional,
Awaitable,
cast,
)
from pydantic import BaseConfig
from pydantic.schema import get_annotation_from_field_info
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
from nonebot.log import logger
from nonebot.exception import TypeMisMatch
from nonebot.typing import _DependentCallable
from nonebot.exception import SkippedException
from nonebot.utils import run_sync, is_coroutine_callable
from .utils import check_field_type, get_typed_signature
@@ -31,25 +47,29 @@ class Param(abc.ABC, FieldInfo):
@classmethod
def _check_param(
cls, dependent: "Dependent", name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
) -> Optional["Param"]:
return None
return
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
) -> Optional["Param"]:
return None
return
@abc.abstractmethod
async def _solve(self, **kwargs: Any) -> Any:
raise NotImplementedError
async def _check(self, **kwargs: Any) -> None:
return
class CustomConfig(BaseConfig):
arbitrary_types_allowed = True
@dataclass(frozen=True)
class Dependent(Generic[R]):
"""依赖注入容器
@@ -61,89 +81,57 @@ class Dependent(Generic[R]):
allow_types: 允许的参数类型
"""
def __init__(
self,
*,
call: Callable[..., Any],
pre_checkers: Optional[List[Param]] = None,
params: Optional[List[ModelField]] = None,
parameterless: Optional[List[Param]] = None,
allow_types: Optional[List[Type[Param]]] = None,
) -> None:
self.call = call
self.pre_checkers = pre_checkers or []
self.params = params or []
self.parameterless = parameterless or []
self.allow_types = allow_types or []
call: _DependentCallable[R]
params: Tuple[ModelField] = field(default_factory=tuple)
parameterless: Tuple[Param] = field(default_factory=tuple)
def __repr__(self) -> str:
if inspect.isfunction(self.call) or inspect.isclass(self.call):
call_str = self.call.__name__
else:
call_str = repr(self.call)
return (
f"<Dependent call={self.call}, params={self.params},"
f" parameterless={self.parameterless}>"
f"Dependent(call={call_str}"
+ (f", parameterless={self.parameterless}" if self.parameterless else "")
+ ")"
)
def __str__(self) -> str:
return self.__repr__()
async def __call__(self, **kwargs: Any) -> R:
# do pre-check
await self.check(**kwargs)
# solve param values
values = await self.solve(**kwargs)
# call function
if is_coroutine_callable(self.call):
return await self.call(**values)
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
else:
return await run_sync(self.call)(**values)
return await run_sync(cast(Callable[..., R], self.call))(**values)
def parse_param(self, name: str, param: inspect.Parameter) -> Param:
for allow_type in self.allow_types:
field_info = allow_type._check_param(self, name, param)
if field_info:
return field_info
else:
raise ValueError(
f"Unknown parameter {name} for function {self.call} with type {param.annotation}"
)
@staticmethod
def parse_params(
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
) -> Tuple[ModelField]:
fields: List[ModelField] = []
params = get_typed_signature(call).parameters.values()
def parse_parameterless(self, value: Any) -> Param:
for allow_type in self.allow_types:
field_info = allow_type._check_parameterless(self, value)
if field_info:
return field_info
else:
raise ValueError(
f"Unknown parameterless {value} for function {self.call} with type {type(value)}"
)
def prepend_parameterless(self, value: Any) -> None:
self.parameterless.insert(0, self.parse_parameterless(value))
def append_parameterless(self, value: Any) -> None:
self.parameterless.append(self.parse_parameterless(value))
@classmethod
def parse(
cls: Type[T],
*,
call: Callable[..., Any],
parameterless: Optional[List[Any]] = None,
allow_types: Optional[List[Type[Param]]] = None,
) -> T:
signature = get_typed_signature(call)
params = signature.parameters
dependent = cls(
call=call,
allow_types=allow_types,
)
for param_name, param in params.items():
for param in params:
default_value = Required
if param.default != param.empty:
default_value = param.default
if isinstance(default_value, Param):
field_info = default_value
default_value = field_info.default
else:
field_info = dependent.parse_param(param_name, param)
for allow_type in allow_types:
if field_info := allow_type._check_param(param, allow_types):
break
else:
raise ValueError(
f"Unknown parameter {param.name} for function {call} with type {param.annotation}"
)
default_value = field_info.default
annotation: Any = Any
@@ -151,11 +139,12 @@ class Dependent(Generic[R]):
if param.annotation != param.empty:
annotation = param.annotation
annotation = get_annotation_from_field_info(
annotation, field_info, param_name
annotation, field_info, param.name
)
dependent.params.append(
fields.append(
ModelField(
name=param_name,
name=param.name,
type_=annotation,
class_validators=None,
model_config=CustomConfig,
@@ -165,49 +154,72 @@ class Dependent(Generic[R]):
)
)
parameterless_params = [
dependent.parse_parameterless(param) for param in (parameterless or [])
]
dependent.parameterless.extend(parameterless_params)
return tuple(fields)
logger.trace(
f"Parsed dependent with call={call}, "
f"params={[param.field_info for param in dependent.params]}, "
f"parameterless={dependent.parameterless}"
@staticmethod
def parse_parameterless(
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
) -> Tuple[Param, ...]:
parameterless_params: List[Param] = []
for value in parameterless:
for allow_type in allow_types:
if param := allow_type._check_parameterless(value, allow_types):
break
else:
raise ValueError(f"Unknown parameterless {value}")
parameterless_params.append(param)
return tuple(parameterless_params)
@classmethod
def parse(
cls,
*,
call: _DependentCallable[R],
parameterless: Optional[Iterable[Any]] = None,
allow_types: Iterable[Type[Param]],
) -> "Dependent[R]":
allow_types = tuple(allow_types)
params = cls.parse_params(call, allow_types)
parameterless_params = (
tuple()
if parameterless is None
else cls.parse_parameterless(tuple(parameterless), allow_types)
)
return dependent
return cls(call, params, parameterless_params)
async def solve(
self,
**params: Any,
) -> Dict[str, Any]:
values: Dict[str, Any] = {}
async def check(self, **params: Any) -> None:
try:
await asyncio.gather(
*(param._check(**params) for param in self.parameterless)
)
await asyncio.gather(
*(
cast(Param, param.field_info)._check(**params)
for param in self.params
)
)
except SkippedException as e:
logger.trace(f"{self} skipped due to {e}")
raise
for checker in self.pre_checkers:
await checker._solve(**params)
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
value = await cast(Param, field.field_info)._solve(**params)
if value is Undefined:
value = field.get_default()
return check_field_type(field, value)
async def solve(self, **params: Any) -> Dict[str, Any]:
# solve parameterless
for param in self.parameterless:
await param._solve(**params)
for field in self.params:
field_info = field.field_info
assert isinstance(field_info, Param), "Params must be subclasses of Param"
value = await field_info._solve(**params)
if value == Undefined:
value = field.get_default()
try:
values[field.name] = check_field_type(field, value)
except TypeMisMatch:
logger.debug(
f"{field_info} "
f"type {type(value)} not match depends {self.call} "
f"annotation {field._type_display()}, ignored"
# solve param values
values = await asyncio.gather(
*(self._solve_field(field, params) for field in self.params)
)
raise
return values
return {field.name: value for field, value in zip(self.params, values)}
__autodoc__ = {"CustomConfig": False}

View File

@@ -28,8 +28,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
)
for param in signature.parameters.values()
]
typed_signature = inspect.Signature(typed_params)
return typed_signature
return inspect.Signature(typed_params)
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:

View File

@@ -1,15 +1,15 @@
import signal
import asyncio
import threading
from typing import Set, Callable, Awaitable
from typing import Set, Union, Callable, Awaitable, cast
from nonebot.log import logger
from nonebot.drivers import Driver
from nonebot.typing import overrides
from nonebot.config import Env, Config
from nonebot.utils import run_sync, is_coroutine_callable
STARTUP_FUNC = Callable[[], Awaitable[None]]
SHUTDOWN_FUNC = Callable[[], Awaitable[None]]
HOOK_FUNC = Union[Callable[[], None], Callable[[], Awaitable[None]]]
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
@@ -19,8 +19,8 @@ HANDLED_SIGNALS = (
class BlockDriver(Driver):
def __init__(self, env: Env, config: Config):
super().__init__(env, config)
self.startup_funcs: Set[STARTUP_FUNC] = set()
self.shutdown_funcs: Set[SHUTDOWN_FUNC] = set()
self.startup_funcs: Set[HOOK_FUNC] = set()
self.shutdown_funcs: Set[HOOK_FUNC] = set()
self.should_exit: asyncio.Event = asyncio.Event()
self.force_exit: bool = False
@@ -37,7 +37,7 @@ class BlockDriver(Driver):
return logger
@overrides(Driver)
def on_startup(self, func: STARTUP_FUNC) -> STARTUP_FUNC:
def on_startup(self, func: HOOK_FUNC) -> HOOK_FUNC:
"""
注册一个启动时执行的函数
"""
@@ -45,7 +45,7 @@ class BlockDriver(Driver):
return func
@overrides(Driver)
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC:
def on_shutdown(self, func: HOOK_FUNC) -> HOOK_FUNC:
"""
注册一个停止时执行的函数
"""
@@ -69,7 +69,12 @@ class BlockDriver(Driver):
async def startup(self):
# run startup
cors = [startup() for startup in self.startup_funcs]
cors = [
cast(Callable[..., Awaitable[None]], startup)()
if is_coroutine_callable(startup)
else run_sync(startup)()
for startup in self.startup_funcs
]
if cors:
try:
await asyncio.gather(*cors)
@@ -89,7 +94,12 @@ class BlockDriver(Driver):
logger.info("Waiting for application shutdown.")
# run shutdown
cors = [shutdown() for shutdown in self.shutdown_funcs]
cors = [
cast(Callable[..., Awaitable[None]], shutdown)()
if is_coroutine_callable(shutdown)
else run_sync(shutdown)()
for shutdown in self.shutdown_funcs
]
if cors:
try:
await asyncio.gather(*cors)

View File

@@ -27,7 +27,7 @@ from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_dr
try:
import aiohttp
except ImportError:
except ImportError: # pragma: no cover
raise ImportError(
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
) from None
@@ -132,20 +132,33 @@ class WebSocket(BaseWebSocket):
@overrides(BaseWebSocket)
async def receive(self) -> str:
msg = await self._receive()
if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
raise TypeError(
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
)
return msg.data
@overrides(BaseWebSocket)
async def receive_text(self) -> str:
msg = await self._receive()
if msg.type != aiohttp.WSMsgType.TEXT:
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
raise TypeError(
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
)
return msg.data
@overrides(BaseWebSocket)
async def receive_bytes(self) -> bytes:
msg = await self._receive()
if msg.type != aiohttp.WSMsgType.TEXT:
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
if msg.type != aiohttp.WSMsgType.BINARY:
raise TypeError(
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
)
return msg.data
@overrides(BaseWebSocket)
async def send(self, data: str) -> None:
async def send_text(self, data: str) -> None:
await self.websocket.send_str(data)
@overrides(BaseWebSocket)

View File

@@ -9,9 +9,11 @@ FrontMatter:
description: nonebot.drivers.fastapi 模块
"""
import logging
import contextlib
from functools import wraps
from typing import Any, List, Tuple, Callable, Optional
from typing import Any, List, Tuple, Union, Callable, Optional
import uvicorn
from pydantic import BaseSettings
@@ -36,6 +38,8 @@ def catch_closed(func):
return await func(*args, **kwargs)
except WebSocketDisconnect as e:
raise WebSocketClosed(e.code)
except KeyError:
raise TypeError("WebSocket received unexpected frame type")
return decorator
@@ -55,7 +59,7 @@ class Config(BaseSettings):
"""开启/关闭冷重载"""
fastapi_reload_dirs: Optional[List[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
fastapi_reload_delay: Optional[float] = None
fastapi_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值"""
fastapi_reload_includes: Optional[List[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
@@ -184,14 +188,12 @@ class Driver(ReverseDriver):
setup: HTTPServerSetup,
) -> Response:
json: Any = None
try:
with contextlib.suppress(Exception):
json = await request.json()
except Exception:
pass
data: Optional[dict] = None
files: Optional[List[Tuple[str, FileTypes]]] = None
try:
with contextlib.suppress(Exception):
form = await request.form()
data = {}
files = []
@@ -202,8 +204,7 @@ class Driver(ReverseDriver):
)
else:
data[key] = value
except Exception:
pass
http_request = BaseRequest(
request.method,
str(request.url),
@@ -217,7 +218,9 @@ class Driver(ReverseDriver):
)
response = await setup.handle_func(http_request)
return Response(response.content, response.status_code, dict(response.headers))
return Response(
response.content, response.status_code, dict(response.headers.items())
)
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
request = BaseRequest(
@@ -261,9 +264,17 @@ class FastAPIWebSocket(BaseWebSocket):
) -> None:
await self.websocket.close(code)
@overrides(BaseWebSocket)
async def receive(self) -> Union[str, bytes]:
# assert self.websocket.application_state == WebSocketState.CONNECTED
msg = await self.websocket.receive()
if msg["type"] == "websocket.disconnect":
raise WebSocketClosed(msg["code"])
return msg["text"] if "text" in msg else msg["bytes"]
@overrides(BaseWebSocket)
@catch_closed
async def receive(self) -> str:
async def receive_text(self) -> str:
return await self.websocket.receive_text()
@overrides(BaseWebSocket)
@@ -272,7 +283,7 @@ class FastAPIWebSocket(BaseWebSocket):
return await self.websocket.receive_bytes()
@overrides(BaseWebSocket)
async def send(self, data: str) -> None:
async def send_text(self, data: str) -> None:
await self.websocket.send({"type": "websocket.send", "text": data})
@overrides(BaseWebSocket)

View File

@@ -31,7 +31,7 @@ from nonebot.drivers import (
try:
import httpx
except ImportError:
except ImportError: # pragma: no cover
raise ImportError(
"Please install httpx by using `pip install nonebot2[httpx]`"
) from None
@@ -49,22 +49,22 @@ class Mixin(ForwardMixin):
async def request(self, setup: Request) -> Response:
async with httpx.AsyncClient(
http2=setup.version == HTTPVersion.H2,
proxies=setup.proxy,
proxies=setup.proxy, # type: ignore
follow_redirects=True,
) as client:
response = await client.request(
setup.method,
str(setup.url),
content=setup.content,
data=setup.data,
content=setup.content, # type: ignore
data=setup.data, # type: ignore
json=setup.json,
files=setup.files,
files=setup.files, # type: ignore
headers=tuple(setup.headers.items()),
timeout=setup.timeout,
)
return Response(
response.status_code,
headers=response.headers,
headers=response.headers.multi_items(),
content=response.content,
request=setup,
)

View File

@@ -17,7 +17,7 @@ FrontMatter:
import asyncio
from functools import wraps
from typing import List, Tuple, TypeVar, Callable, Optional, Coroutine
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
import uvicorn
from pydantic import BaseSettings
@@ -37,8 +37,8 @@ try:
from quart import Quart, Request, Response
from quart.datastructures import FileStorage
from quart import Websocket as QuartWebSocket
except ImportError:
raise ValueError(
except ImportError: # pragma: no cover
raise ImportError(
"Please install Quart by using `pip install nonebot2[quart]`"
) from None
@@ -63,7 +63,7 @@ class Config(BaseSettings):
"""开启/关闭冷重载"""
quart_reload_dirs: Optional[List[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
quart_reload_delay: Optional[float] = None
quart_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值"""
quart_reload_includes: Optional[List[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
@@ -199,7 +199,7 @@ class Driver(ReverseDriver):
http_request = BaseRequest(
request.method,
request.url,
headers=request.headers.items(),
headers=list(request.headers.items()),
cookies=list(request.cookies.items()),
content=await request.get_data(
cache=False, as_text=False, parse_form_data=False
@@ -224,7 +224,7 @@ class Driver(ReverseDriver):
http_request = BaseRequest(
websocket.method,
websocket.url,
headers=websocket.headers.items(),
headers=list(websocket.headers.items()),
cookies=list(websocket.cookies.items()),
version=websocket.http_version,
)
@@ -257,7 +257,12 @@ class WebSocket(BaseWebSocket):
@overrides(BaseWebSocket)
@catch_closed
async def receive(self) -> str:
async def receive(self) -> Union[str, bytes]:
return await self.websocket.receive()
@overrides(BaseWebSocket)
@catch_closed
async def receive_text(self) -> str:
msg = await self.websocket.receive()
if isinstance(msg, bytes):
raise TypeError("WebSocket received unexpected frame type: bytes")
@@ -272,7 +277,7 @@ class WebSocket(BaseWebSocket):
return msg
@overrides(BaseWebSocket)
async def send(self, data: str):
async def send_text(self, data: str):
await self.websocket.send(data)
@overrides(BaseWebSocket)

View File

@@ -16,8 +16,8 @@ FrontMatter:
"""
import logging
from functools import wraps
from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager
from typing import Type, Union, AsyncGenerator
from nonebot.typing import overrides
from nonebot.log import LoguruHandler
@@ -30,10 +30,10 @@ from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
try:
from websockets.exceptions import ConnectionClosed
from websockets.legacy.client import Connect, WebSocketClientProtocol
except ImportError:
except ImportError: # pragma: no cover
raise ImportError(
"Please install websockets by using `pip install nonebot2[websockets]`"
)
) from None
logger = logging.Logger("websockets.client", "INFO")
logger.addHandler(LoguruHandler())
@@ -46,9 +46,9 @@ def catch_closed(func):
return await func(*args, **kwargs)
except ConnectionClosed as e:
if e.rcvd_then_sent:
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason)
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason) # type: ignore
else:
raise WebSocketClosed(e.sent.code, e.sent.reason)
raise WebSocketClosed(e.sent.code, e.sent.reason) # type: ignore
return decorator
@@ -100,7 +100,13 @@ class WebSocket(BaseWebSocket):
@overrides(BaseWebSocket)
@catch_closed
async def receive(self) -> str:
async def receive(self) -> Union[str, bytes]:
msg = await self.websocket.recv()
return msg
@overrides(BaseWebSocket)
@catch_closed
async def receive_text(self) -> str:
msg = await self.websocket.recv()
if isinstance(msg, bytes):
raise TypeError("WebSocket received unexpected frame type: bytes")
@@ -115,7 +121,7 @@ class WebSocket(BaseWebSocket):
return msg
@overrides(BaseWebSocket)
async def send(self, data: str) -> None:
async def send_text(self, data: str) -> None:
await self.websocket.send(data)
@overrides(BaseWebSocket)

View File

@@ -7,11 +7,11 @@ NoneBotException
├── ParserExit
├── ProcessException
| ├── IgnoredException
| ├── SkippedException
| | └── TypeMisMatch
| ├── MockApiException
| └── StopPropagation
├── MatcherException
| ├── SkippedException
| | └── TypeMisMatch
| ├── PausedException
| ├── RejectedException
| └── FinishedException
@@ -46,10 +46,14 @@ class ParserExit(NoneBotException):
self.status = status
self.message = message
def __repr__(self):
return f"<ParserExit status={self.status} message={self.message}>"
def __repr__(self) -> str:
return (
f"ParserExit(status={self.status}"
+ (f", message={self.message!r}" if self.message else "")
+ ")"
)
def __str__(self):
def __str__(self) -> str:
return self.__repr__()
@@ -68,10 +72,44 @@ class IgnoredException(ProcessException):
def __init__(self, reason: Any):
self.reason: Any = reason
def __repr__(self):
return f"<IgnoredException, reason={self.reason}>"
def __repr__(self) -> str:
return f"IgnoredException(reason={self.reason!r})"
def __str__(self):
def __str__(self) -> str:
return self.__repr__()
class SkippedException(ProcessException):
"""指示 NoneBot 立即结束当前 `Dependent` 的运行。
例如,可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。
用法:
```python
def always_skip():
Matcher.skip()
@matcher.handle()
async def handler(dependency = Depends(always_skip)):
# never run
```
"""
class TypeMisMatch(SkippedException):
"""当前 `Handler` 的参数类型不匹配。"""
def __init__(self, param: ModelField, value: Any):
self.param: ModelField = param
self.value: Any = value
def __repr__(self) -> str:
return (
f"TypeMisMatch(param={self.param.name}, "
f"type={self.param._type_display()}, value={self.value!r}>"
)
def __str__(self) -> str:
return self.__repr__()
@@ -85,10 +123,10 @@ class MockApiException(ProcessException):
def __init__(self, result: Any):
self.result = result
def __repr__(self):
return f"<ApiCancelledException, result={self.result}>"
def __repr__(self) -> str:
return f"MockApiException(result={self.result!r})"
def __str__(self):
def __str__(self) -> str:
return self.__repr__()
@@ -114,37 +152,6 @@ class MatcherException(NoneBotException):
"""所有 Matcher 发生的异常基类。"""
class SkippedException(MatcherException):
"""指示 NoneBot 立即结束当前 `Handler` 的处理,继续处理下一个 `Handler`。
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。
用法:
```python
def always_skip():
Matcher.skip()
@matcher.handle()
async def handler(dependency = Depends(always_skip)):
...
```
"""
class TypeMisMatch(SkippedException):
"""当前 `Handler` 的参数类型不匹配。"""
def __init__(self, param: ModelField, value: Any):
self.param: ModelField = param
self.value: Any = value
def __repr__(self):
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
def __str__(self):
self.__repr__()
class PausedException(MatcherException):
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。可用于用户输入新信息。
@@ -195,7 +202,8 @@ class AdapterException(NoneBotException):
adapter_name: 标识 adapter
"""
def __init__(self, adapter_name: str) -> None:
def __init__(self, adapter_name: str, *args: object) -> None:
super().__init__(*args)
self.adapter_name: str = adapter_name
@@ -231,4 +239,11 @@ class WebSocketClosed(DriverException):
self.reason = reason
def __repr__(self) -> str:
return f"<WebSocketClosed code={self.code} reason={self.reason}>"
return (
f"WebSocketClosed(code={self.code}"
+ (f", reason={self.reason!r}" if self.reason else "")
+ ")"
)
def __str__(self) -> str:
return self.__repr__()

View File

@@ -33,6 +33,9 @@ class Adapter(abc.ABC):
self.bots: Dict[str, Bot] = {}
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
def __repr__(self) -> str:
return f"Adapter(name={self.get_name()!r})"
@classmethod
@abc.abstractmethod
def get_name(cls) -> str:

View File

@@ -1,8 +1,7 @@
import abc
import asyncio
from functools import partial
from typing_extensions import Protocol
from typing import TYPE_CHECKING, Any, Set, Union, Optional
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
from nonebot.log import logger
from nonebot.config import Config
@@ -14,8 +13,7 @@ if TYPE_CHECKING:
from .adapter import Adapter
from .message import Message, MessageSegment
class _ApiCall(Protocol):
class _ApiCall(Protocol):
async def __call__(self, **kwargs: Any) -> Any:
...
@@ -41,7 +39,10 @@ class Bot(abc.ABC):
self.self_id: str = self_id
"""机器人 ID"""
def __getattr__(self, name: str) -> _ApiCall:
def __repr__(self) -> str:
return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
def __getattr__(self, name: str) -> "_ApiCall":
return partial(self.call_api, name)
@property
@@ -72,8 +73,7 @@ class Bot(abc.ABC):
skip_calling_api: bool = False
exception: Optional[Exception] = None
coros = list(map(lambda x: x(self, api, data), self._calling_api_hook))
if coros:
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
try:
logger.debug("Running CallingAPI hooks...")
await asyncio.gather(*coros)
@@ -95,10 +95,9 @@ class Bot(abc.ABC):
except Exception as e:
exception = e
coros = list(
map(lambda x: x(self, exception, api, data, result), self._called_api_hook)
)
if coros:
if coros := [
hook(self, exception, api, data, result) for hook in self._called_api_hook
]:
try:
logger.debug("Running CalledAPI hooks...")
await asyncio.gather(*coros)

View File

@@ -1,4 +1,5 @@
import abc
from typing import Any, Type, TypeVar
from pydantic import BaseModel
@@ -6,6 +7,8 @@ from nonebot.utils import DataclassEncoder
from .message import Message
E = TypeVar("E", bound="Event")
class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
@@ -14,6 +17,12 @@ class Event(abc.ABC, BaseModel):
extra = "allow"
json_encoders = {Message: DataclassEncoder}
@classmethod
def validate(cls: Type["E"], value: Any) -> "E":
if isinstance(value, Event) and not isinstance(value, cls):
raise TypeError(f"{value} is incompatible with Event type {cls}")
return super().validate(value)
@abc.abstractmethod
def get_type(self) -> str:
"""获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。"""

View File

@@ -66,7 +66,11 @@ class MessageSegment(abc.ABC, Generic[TM]):
return value
if not isinstance(value, dict):
raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
return cls(**value)
if "type" not in value:
raise ValueError(
f"Expected dict with 'type' for MessageSegment, got {value}"
)
return cls(type=value["type"], data=value.get("data", {}))
def get(self, key: str, default: Any = None):
return asdict(self).get(key, default)
@@ -182,7 +186,7 @@ class Message(List[TMS], abc.ABC):
elif isinstance(other, Iterable):
self.extend(other)
else:
raise ValueError(f"Unsupported type: {type(other)}") # pragma: no cover
raise TypeError(f"Unsupported type {type(other)!r}")
return self
@overload

View File

@@ -49,11 +49,16 @@ class MessageTemplate(Formatter, Generic[TF]):
) -> None:
...
def __init__(self, template, factory=str) -> None:
def __init__( # type:ignore
self, template, factory=str
) -> None: # TODO: fix type hint here
self.template: TF = template
self.factory: Type[TF] = factory
self.format_specs: Dict[str, FormatSpecFunc] = {}
def __repr__(self) -> str:
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
def add_format_spec(
self, spec: FormatSpecFunc_T, name: Optional[str] = None
) -> FormatSpecFunc_T:
@@ -72,25 +77,37 @@ class MessageTemplate(Formatter, Generic[TF]):
return self._format([], mapping)
def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF:
msg = self.factory()
full_message = self.factory()
used_args, arg_index = set(), 0
if isinstance(self.template, str):
msg += self.vformat(self.template, args, kwargs)
msg, arg_index = self._vformat(
self.template, args, kwargs, used_args, arg_index
)
full_message += msg
elif isinstance(self.template, self.factory):
template = cast("Message[MessageSegment]", self.template)
for seg in template:
msg += self.vformat(str(seg), args, kwargs) if seg.is_text() else seg
if not seg.is_text():
full_message += seg
else:
msg, arg_index = self._vformat(
str(seg), args, kwargs, used_args, arg_index
)
full_message += msg
else:
raise TypeError("template must be a string or instance of Message!")
return msg # type:ignore
self.check_unused_args(list(used_args), args, kwargs)
return cast(TF, full_message)
def vformat(
self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
self,
format_string: str,
args: Sequence[Any],
kwargs: Mapping[str, Any],
) -> TF:
used_args = set()
result, _ = self._vformat(format_string, args, kwargs, used_args, 2)
self.check_unused_args(list(used_args), args, kwargs)
return result
raise NotImplementedError("`vformat` has merged into `_format`")
def _vformat(
self,
@@ -98,12 +115,8 @@ class MessageTemplate(Formatter, Generic[TF]):
args: Sequence[Any],
kwargs: Mapping[str, Any],
used_args: Set[Union[int, str]],
recursion_depth: int,
auto_arg_index: int = 0,
) -> Tuple[TF, int]:
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
results: List[Any] = [self.factory()]
for (literal_text, field_name, format_spec, conversion) in self.parse(
@@ -143,23 +156,13 @@ class MessageTemplate(Formatter, Generic[TF]):
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
assert format_spec is not None
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion) if conversion else obj
# expand the format spec, if needed
format_control, auto_arg_index = self._vformat(
format_spec,
args,
kwargs,
used_args,
recursion_depth - 1,
auto_arg_index,
)
# format the object and append to the result
formatted_text = self.format_field(obj, str(format_control))
formatted_text = (
self.format_field(obj, format_spec) if format_spec else obj
)
results.append(formatted_text)
return functools.reduce(self._add, results), auto_arg_index

View File

@@ -4,9 +4,10 @@ from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from nonebot.log import logger
from nonebot.utils import escape_tag
from nonebot.config import Env, Config
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
from nonebot.internal.params import BotParam, DependParam, DefaultParam
@@ -39,12 +40,18 @@ class Driver(abc.ABC):
"""环境名称"""
self.config: Config = config
"""全局配置对象"""
self._clients: Dict[str, "Bot"] = {}
self._bots: Dict[str, "Bot"] = {}
def __repr__(self) -> str:
return (
f"Driver(type={self.type!r}, "
f"adapters={len(self._adapters)}, bots={len(self._bots)})"
)
@property
def bots(self) -> Dict[str, "Bot"]:
"""获取当前所有已连接的 Bot"""
return self._clients
return self._bots
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
@@ -123,12 +130,17 @@ class Driver(abc.ABC):
def _bot_connect(self, bot: "Bot") -> None:
"""在连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients:
if bot.self_id in self._bots:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot
self._bots[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_connection_hook))
coros = list(
map(
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
self._bot_connection_hook,
)
)
if coros:
try:
await asyncio.gather(*coros)
@@ -142,11 +154,16 @@ class Driver(abc.ABC):
def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
if bot.self_id in self._bots:
del self._bots[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_disconnection_hook))
coros = list(
map(
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
self._bot_disconnection_hook,
)
)
if coros:
try:
await asyncio.gather(*coros)
@@ -222,13 +239,11 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
if not mixins:
return driver
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
@property
def type(self) -> str:
def type_(self: ForwardDriver) -> str:
return (
driver.type.__get__(self)
+ "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
)
return CombinedDriver
return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore

View File

@@ -131,9 +131,7 @@ class Request:
self.files.append((name, file_info)) # type: ignore
def __repr__(self) -> str:
class_name = self.__class__.__name__
url = str(self.url)
return f"<{class_name}({self.method!r}, {url!r})>"
return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')"
class Response:
@@ -161,12 +159,18 @@ class Response:
# request
self.request: Optional[Request] = request
def __repr__(self) -> str:
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
class WebSocket(abc.ABC):
def __init__(self, *, request: Request):
# request
self.request: Request = request
def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.request.url!s}')"
@property
@abc.abstractmethod
def closed(self) -> bool:
@@ -186,7 +190,12 @@ class WebSocket(abc.ABC):
raise NotImplementedError
@abc.abstractmethod
async def receive(self) -> str:
async def receive(self) -> Union[str, bytes]:
"""接收一条 WebSocket text/bytes 信息"""
raise NotImplementedError
@abc.abstractmethod
async def receive_text(self) -> str:
"""接收一条 WebSocket text 信息"""
raise NotImplementedError
@@ -195,8 +204,17 @@ class WebSocket(abc.ABC):
"""接收一条 WebSocket binary 信息"""
raise NotImplementedError
async def send(self, data: Union[str, bytes]) -> None:
"""发送一条 WebSocket text/bytes 信息"""
if isinstance(data, str):
await self.send_text(data)
elif isinstance(data, bytes):
await self.send_bytes(data)
else:
raise TypeError("WebSocker send method expects str or bytes!")
@abc.abstractmethod
async def send(self, data: str) -> None:
async def send_text(self, data: str) -> None:
"""发送一条 WebSocket text 信息"""
raise NotImplementedError
@@ -248,8 +266,8 @@ class Cookies(MutableMapping):
self,
name: str,
default: Optional[str] = None,
domain: str = None,
path: str = None,
domain: Optional[str] = None,
path: Optional[str] = None,
) -> Optional[str]:
value: Optional[str] = None
for cookie in self.jar:
@@ -306,17 +324,14 @@ class Cookies(MutableMapping):
return len(self.jar)
def __iter__(self) -> Iterator[Cookie]:
return (cookie for cookie in self.jar)
return iter(self.jar)
def __repr__(self) -> str:
cookies_repr = ", ".join(
[
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
f"Cookie({cookie.name}={cookie.value} for {cookie.domain})"
for cookie in self.jar
]
)
return f"<Cookies [{cookies_repr}]>"
return f"{self.__class__.__name__}({cookies_repr})"
@dataclass

View File

@@ -1,8 +1,8 @@
from types import ModuleType
from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict
from contextlib import AsyncExitStack
from datetime import datetime, timedelta
from typing import (
TYPE_CHECKING,
Any,
@@ -12,8 +12,10 @@ from typing import (
Union,
TypeVar,
Callable,
Iterable,
NoReturn,
Optional,
overload,
)
from nonebot.log import logger
@@ -34,7 +36,6 @@ from nonebot.typing import (
T_PermissionUpdater,
)
from nonebot.exception import (
TypeMisMatch,
PausedException,
StopPropagation,
SkippedException,
@@ -43,7 +44,7 @@ from nonebot.exception import (
)
from .rule import Rule
from .permission import USER, Permission
from .permission import USER, User, Permission
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
from .params import (
Depends,
@@ -71,29 +72,16 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>"
f"Matcher(type={self.type!r}"
+ (f", module={self.module_name}" if self.module_name else "")
+ ")"
)
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
@@ -132,7 +120,7 @@ class Matcher(metaclass=MatcherMeta):
_default_permission_updater: Optional[Dependent[Permission]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES = [
HANDLER_PARAM_TYPES = (
DependParam,
BotParam,
EventParam,
@@ -140,7 +128,7 @@ class Matcher(metaclass=MatcherMeta):
ArgParam,
MatcherParam,
DefaultParam,
]
)
def __init__(self):
self.handlers = self.handlers.copy()
@@ -148,13 +136,11 @@ class Matcher(metaclass=MatcherMeta):
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>"
f"Matcher(type={self.type!r}"
+ (f", module={self.module_name}" if self.module_name else "")
+ ")"
)
def __str__(self) -> str:
return repr(self)
@classmethod
def new(
cls,
@@ -168,7 +154,7 @@ class Matcher(metaclass=MatcherMeta):
*,
plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None,
expire_time: Optional[Union[datetime, timedelta]] = None,
default_state: Optional[T_State] = None,
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
default_permission_updater: Optional[
@@ -216,26 +202,38 @@ class Matcher(metaclass=MatcherMeta):
if handlers
else [],
"temp": temp,
"expire_time": expire_time,
"expire_time": (
expire_time
and (
expire_time
if isinstance(expire_time, datetime)
else datetime.now() + expire_time
)
),
"priority": priority,
"block": block,
"_default_state": default_state or {},
"_default_type_updater": (
default_type_updater
and (
default_type_updater
if isinstance(default_type_updater, Dependent)
else default_type_updater
and Dependent[str].parse(
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES
else Dependent[str].parse(
call=default_type_updater,
allow_types=cls.HANDLER_PARAM_TYPES,
)
)
),
"_default_permission_updater": (
default_permission_updater
and (
default_permission_updater
if isinstance(default_permission_updater, Dependent)
else default_permission_updater
and Dependent[Permission].parse(
else Dependent[Permission].parse(
call=default_permission_updater,
allow_types=cls.HANDLER_PARAM_TYPES,
)
)
),
},
)
@@ -322,7 +320,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod
def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
) -> Dependent[Any]:
handler_ = Dependent[Any].parse(
call=handler,
@@ -334,7 +332,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod
def handle(
cls, parameterless: Optional[List[Any]] = None
cls, parameterless: Optional[Iterable[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来向事件响应器直接添加一个处理函数
@@ -350,7 +348,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod
def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None
cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
@@ -368,14 +366,21 @@ class Matcher(metaclass=MatcherMeta):
return
await matcher.reject()
_parameterless = [Depends(_receive), *(parameterless or [])]
_parameterless = (Depends(_receive), *(parameterless or tuple()))
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
new_handler = Dependent(
call=func_handler.call,
params=func_handler.params,
parameterless=Dependent.parse_parameterless(
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
)
+ func_handler.parameterless,
)
cls.handlers[-1] = new_handler
else:
cls.append_handler(func, parameterless=_parameterless)
@@ -388,7 +393,7 @@ class Matcher(metaclass=MatcherMeta):
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None,
parameterless: Optional[Iterable[Any]] = None,
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
@@ -409,17 +414,21 @@ class Matcher(metaclass=MatcherMeta):
return
await matcher.reject(prompt)
_parameterless = [
Depends(_key_getter),
*(parameterless or []),
]
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
new_handler = Dependent(
call=func_handler.call,
params=func_handler.params,
parameterless=Dependent.parse_parameterless(
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
)
+ func_handler.parameterless,
)
cls.handlers[-1] = new_handler
else:
cls.append_handler(func, parameterless=_parameterless)
@@ -547,7 +556,17 @@ class Matcher(metaclass=MatcherMeta):
"""
raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
@overload
def get_receive(self, id: str) -> Union[Event, None]:
...
@overload
def get_receive(self, id: str, default: T) -> Union[Event, T]:
...
def get_receive(
self, id: str, default: Optional[T] = None
) -> Optional[Union[Event, T]]:
"""获取一个 `receive` 事件
如果没有找到对应的事件,返回 `default` 值
@@ -559,14 +578,34 @@ class Matcher(metaclass=MatcherMeta):
self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]:
@overload
def get_last_receive(self) -> Union[Event, None]:
...
@overload
def get_last_receive(self, default: T) -> Union[Event, T]:
...
def get_last_receive(
self, default: Optional[T] = None
) -> Optional[Union[Event, T]]:
"""获取最近一次 `receive` 事件
如果没有事件,返回 `default` 值
"""
return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
@overload
def get_arg(self, key: str) -> Union[Message, None]:
...
@overload
def get_arg(self, key: str, default: T) -> Union[Message, T]:
...
def get_arg(
self, key: str, default: Optional[T] = None
) -> Optional[Union[Message, T]]:
"""获取一个 `got` 消息
如果没有找到对应的消息,返回 `default` 值
@@ -583,7 +622,15 @@ class Matcher(metaclass=MatcherMeta):
else:
self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]:
@overload
def get_target(self) -> Union[str, None]:
...
@overload
def get_target(self, default: T) -> Union[str, T]:
...
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
return self.state.get(REJECT_TARGET, default)
def stop_propagation(self):
@@ -592,15 +639,21 @@ class Matcher(metaclass=MatcherMeta):
async def update_type(self, bot: Bot, event: Event) -> str:
updater = self.__class__._default_type_updater
if not updater:
return "message"
return await updater(bot=bot, event=event, state=self.state, matcher=self)
return (
await updater(bot=bot, event=event, state=self.state, matcher=self)
if updater
else "message"
)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
updater = self.__class__._default_permission_updater
if not updater:
return USER(event.get_session_id(), perm=self.permission)
if updater := self.__class__._default_permission_updater:
return await updater(bot=bot, event=event, state=self.state, matcher=self)
permission = self.permission
if len(permission.checkers) == 1 and isinstance(
user_perm := tuple(permission.checkers)[0].call, User
):
permission = user_perm.perm
return USER(event.get_session_id(), perm=permission)
async def resolve_reject(self):
handler = current_handler.get()
@@ -617,8 +670,8 @@ class Matcher(metaclass=MatcherMeta):
dependency_cache: Optional[T_DependencyCache] = None,
):
logger.trace(
f"Matcher {self} run with incoming args: "
f"bot={bot}, event={event}, state={state}"
f"{self} run with incoming args: "
f"bot={bot}, event={event!r}, state={state!r}"
)
b_t = current_bot.set(bot)
e_t = current_event.set(event)
@@ -640,17 +693,12 @@ class Matcher(metaclass=MatcherMeta):
stack=stack,
dependency_cache=dependency_cache,
)
except TypeMisMatch as e:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
except SkippedException:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally:
logger.info(f"Matcher {self} running complete")
logger.info(f"{self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
@@ -682,7 +730,7 @@ class Matcher(metaclass=MatcherMeta):
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
expire_time=bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
@@ -701,7 +749,7 @@ class Matcher(metaclass=MatcherMeta):
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
expire_time=bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,

View File

@@ -1,14 +1,10 @@
import asyncio
import inspect
import warnings
from typing_extensions import Literal
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
from pydantic.fields import Required, Undefined, ModelField
from nonebot.log import logger
from nonebot.exception import TypeMisMatch
from nonebot.dependencies.utils import check_field_type
from nonebot.dependencies import Param, Dependent, CustomConfig
from nonebot.typing import T_State, T_Handler, T_DependencyCache
@@ -40,7 +36,7 @@ class DependsInner:
def __repr__(self) -> str:
dep = get_name(self.dependency)
cache = "" if self.use_cache else ", use_cache=False"
return f"{self.__class__.__name__}({dep}{cache})"
return f"DependsInner({dep}{cache})"
def Depends(
@@ -75,12 +71,12 @@ def Depends(
class DependParam(Param):
"""子依赖参数"""
def __repr__(self) -> str:
return f"Depends({self.extra['dependent']})"
@classmethod
def _check_param(
cls,
dependent: Dependent,
name: str,
param: inspect.Parameter,
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
dependency: T_Handler
@@ -91,22 +87,20 @@ class DependParam(Param):
dependency = param.default.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=dependent.allow_types,
allow_types=allow_types,
)
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
sub_dependent.pre_checkers.clear()
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
cls, value: Any, allow_types: Tuple[Type[Param], ...]
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
dependent = Dependent[Any].parse(
call=value.dependency, allow_types=dependent.allow_types
call=value.dependency, allow_types=allow_types
)
return cls(Required, use_cache=value.use_cache, dependent=dependent)
@@ -120,8 +114,7 @@ class DependParam(Param):
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["dependent"]
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
call = sub_dependent.call
call = cast(Callable[..., Any], sub_dependent.call)
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
@@ -133,7 +126,7 @@ class DependParam(Param):
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
solved = await dependency_cache[call]
return await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
@@ -144,134 +137,124 @@ class DependParam(Param):
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
solved = await task
return await task
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
solved = await task
return await task
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
solved = await task
return await task
return solved
class _BotChecker(Param):
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, bot)
except TypeMisMatch:
logger.debug(
f"Bot type {type(bot)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
async def _check(self, **kwargs: Any) -> None:
# run sub dependent pre-checkers
sub_dependent: Dependent = self.extra["dependent"]
await sub_dependent.check(**kwargs)
class BotParam(Param):
"""{ref}`nonebot.adapters.Bot` 参数"""
def __repr__(self) -> str:
return (
"BotParam("
+ (
repr(cast(ModelField, checker).type_)
if (checker := self.extra.get("checker"))
else ""
)
+ ")"
)
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["BotParam"]:
from nonebot.adapters import Bot
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Bot):
checker: Optional[ModelField] = None
if param.annotation is not Bot:
dependent.pre_checkers.append(
_BotChecker(
Required,
field=ModelField(
name=name,
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "bot":
return cls(Required, checker=checker)
elif param.annotation == param.empty and param.name == "bot":
return cls(Required)
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
return bot
class _EventChecker(Param):
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, event)
except TypeMisMatch:
logger.debug(
f"Event type {type(event)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
if checker := self.extra.get("checker"):
check_field_type(checker, bot)
class EventParam(Param):
"""{ref}`nonebot.adapters.Event` 参数"""
def __repr__(self) -> str:
return (
"EventParam("
+ (
repr(cast(ModelField, checker).type_)
if (checker := self.extra.get("checker"))
else ""
)
+ ")"
)
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["EventParam"]:
from nonebot.adapters import Event
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Event):
checker: Optional[ModelField] = None
if param.annotation is not Event:
dependent.pre_checkers.append(
_EventChecker(
Required,
field=ModelField(
name=name,
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "event":
return cls(Required, checker=checker)
elif param.annotation == param.empty and param.name == "event":
return cls(Required)
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
return event
class StateInner(T_State):
...
def State() -> T_State:
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
return StateInner()
async def _check(self, event: "Event", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, event)
class StateParam(Param):
"""事件处理状态参数"""
def __repr__(self) -> str:
return "StateParam()"
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["StateParam"]:
if isinstance(param.default, StateInner):
return cls(Required)
elif param.default == param.empty:
if param.default == param.empty:
if param.annotation is T_State:
return cls(Required)
elif param.annotation == param.empty and name == "state":
elif param.annotation == param.empty and param.name == "state":
return cls(Required)
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
@@ -281,14 +264,17 @@ class StateParam(Param):
class MatcherParam(Param):
"""事件响应器实例参数"""
def __repr__(self) -> str:
return "MatcherParam()"
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["MatcherParam"]:
from nonebot.matcher import Matcher
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and name == "matcher"
param.annotation == param.empty and param.name == "matcher"
):
return cls(Required)
@@ -303,6 +289,9 @@ class ArgInner:
self.key = key
self.type = type
def __repr__(self) -> str:
return f"ArgInner(key={self.key!r}, type={self.type!r})"
def Arg(key: Optional[str] = None) -> Any:
"""`got` 的 Arg 参数消息"""
@@ -322,12 +311,17 @@ def ArgPlainText(key: Optional[str] = None) -> str:
class ArgParam(Param):
"""`got` 的 Arg 参数"""
def __repr__(self) -> str:
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["ArgParam"]:
if isinstance(param.default, ArgInner):
return cls(Required, key=param.default.key or name, type=param.default.type)
return cls(
Required, key=param.default.key or param.name, type=param.default.type
)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
@@ -344,12 +338,15 @@ class ArgParam(Param):
class ExceptionParam(Param):
"""`run_postprocessor` 的异常参数"""
def __repr__(self) -> str:
return "ExceptionParam()"
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and name == "exception"
param.annotation == param.empty and param.name == "exception"
):
return cls(Required)
@@ -360,9 +357,12 @@ class ExceptionParam(Param):
class DefaultParam(Param):
"""默认值参数"""
def __repr__(self) -> str:
return f"DefaultParam(default={self.default!r})"
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["DefaultParam"]:
if param.default != param.empty:
return cls(param.default)

View File

@@ -1,8 +1,9 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
from typing import Set, Tuple, Union, NoReturn, Optional
from nonebot.dependencies import Dependent
from nonebot.utils import run_coro_with_catch
from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_PermissionChecker
@@ -10,13 +11,6 @@ from .adapter import Bot, Event
from .params import BotParam, EventParam, DependParam, DefaultParam
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
try:
return await coro
except SkippedException:
return False
class Permission:
"""{ref}`nonebot.matcher.Matcher` 权限类。
@@ -43,16 +37,19 @@ class Permission:
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
self.checkers: Set[Dependent[bool]] = {
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
}
"""存储 `PermissionChecker`"""
def __repr__(self) -> str:
return f"Permission({', '.join(repr(checker) for checker in self.checkers)})"
async def __call__(
self,
bot: Bot,
@@ -72,20 +69,22 @@ class Permission:
return True
results = await asyncio.gather(
*(
_run_coro_with_catch(
run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
)
),
(SkippedException,),
False,
)
for checker in self.checkers
),
)
return any(results)
def __and__(self, other) -> NoReturn:
def __and__(self, other: object) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")
def __or__(
@@ -98,6 +97,16 @@ class Permission:
else:
return Permission(*self.checkers, other)
def __ror__(
self, other: Optional[Union["Permission", T_PermissionChecker]]
) -> "Permission":
if other is None:
return self
elif isinstance(other, Permission):
return Permission(*other.checkers, *self.checkers)
else:
return Permission(other, *self.checkers)
class User:
"""检查当前事件是否属于指定会话
@@ -115,10 +124,20 @@ class User:
self.users = users
self.perm = perm
def __repr__(self) -> str:
return (
f"User(users={self.users}"
+ (f", permission={self.perm})" if self.perm else "")
+ ")"
)
async def __call__(self, bot: Bot, event: Event) -> bool:
try:
session = event.get_session_id()
except Exception:
return False
return bool(
event.get_session_id() in self.users
and (self.perm is None or await self.perm(bot, event))
session in self.users and (self.perm is None or await self.perm(bot, event))
)

View File

@@ -37,16 +37,19 @@ class Rule:
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
self.checkers: Set[Dependent[bool]] = {
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
}
"""存储 `RuleChecker`"""
def __repr__(self) -> str:
return f"Rule({', '.join(repr(checker) for checker in self.checkers)})"
async def __call__(
self,
bot: Bot,
@@ -91,5 +94,13 @@ class Rule:
else:
return Rule(*self.checkers, other)
def __or__(self, other) -> NoReturn:
def __rand__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:
return self
elif isinstance(other, Rule):
return Rule(*other.checkers, *self.checkers)
else:
return Rule(other, *self.checkers)
def __or__(self, other: object) -> NoReturn:
raise RuntimeError("Or operation between rules is not allowed.")

View File

@@ -14,16 +14,14 @@ FrontMatter:
import sys
import logging
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
import loguru
if TYPE_CHECKING:
# avoid sphinx autodoc resolve annotation failed
# because loguru module do not have `Logger` class actually
from loguru import Logger
from nonebot.plugin import Plugin
from loguru import Logger, Record
# logger = logging.getLogger("nonebot")
logger: "Logger" = loguru.logger
@@ -47,26 +45,10 @@ logger: "Logger" = loguru.logger
# logger.addHandler(default_handler)
class Filter:
def __init__(self) -> None:
self.level: Union[int, str] = "INFO"
def __call__(self, record):
module_name: str = record["name"]
# TODO: get plugin name instead of module name
# module = sys.modules.get(module_name)
# if module and hasattr(module, "__plugin__"):
# plugin: "Plugin" = getattr(module, "__plugin__")
# module_name = plugin.module_name
record["name"] = module_name.split(".")[0]
levelno = (
logger.level(self.level).no if isinstance(self.level, str) else self.level
)
return record["level"].no >= levelno
class LoguruHandler(logging.Handler): # pragma: no cover
def emit(self, record):
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
def emit(self, record: logging.LogRecord):
try:
level = logger.level(record.levelname).name
except ValueError:
@@ -82,9 +64,13 @@ class LoguruHandler(logging.Handler): # pragma: no cover
)
logger.remove()
default_filter: Filter = Filter()
"""默认日志等级过滤器"""
def default_filter(record: "Record"):
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
log_level = record["extra"].get("nonebot_log_level", "INFO")
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
return record["level"].no >= levelno
default_format: str = (
"<g>{time:MM-DD HH:mm:ss}</g> "
"[<lvl>{level}</lvl>] "
@@ -93,13 +79,14 @@ default_format: str = (
"{message}"
)
"""默认日志格式"""
logger.remove()
logger_id = logger.add(
sys.stdout,
level=0,
colorize=True,
diagnose=False,
filter=default_filter,
format=default_format,
)
__autodoc__ = {"Filter": False, "LoguruHandler": False}
__autodoc__ = {"logger_id": False}

View File

@@ -8,15 +8,16 @@ FrontMatter:
"""
import asyncio
import contextlib
from datetime import datetime
from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
from nonebot.log import logger
from nonebot.rule import TrieRule
from nonebot.utils import escape_tag
from nonebot.dependencies import Dependent
from nonebot.matcher import Matcher, matchers
from nonebot.utils import escape_tag, run_coro_with_catch
from nonebot.exception import (
NoLogException,
StopPropagation,
@@ -50,14 +51,14 @@ _event_postprocessors: Set[Dependent[Any]] = set()
_run_preprocessors: Set[Dependent[Any]] = set()
_run_postprocessors: Set[Dependent[Any]] = set()
EVENT_PCS_PARAMS = [
EVENT_PCS_PARAMS = (
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
RUN_PREPCS_PARAMS = [
)
RUN_PREPCS_PARAMS = (
DependParam,
BotParam,
EventParam,
@@ -65,8 +66,8 @@ RUN_PREPCS_PARAMS = [
ArgParam,
MatcherParam,
DefaultParam,
]
RUN_POSTPCS_PARAMS = [
)
RUN_POSTPCS_PARAMS = (
DependParam,
ExceptionParam,
BotParam,
@@ -75,7 +76,7 @@ RUN_POSTPCS_PARAMS = [
ArgParam,
MatcherParam,
DefaultParam,
]
)
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
@@ -110,13 +111,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
return func
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]) -> Any:
try:
return await coro
except SkippedException:
pass
async def _check_matcher(
priority: int,
Matcher: Type[Matcher],
@@ -127,10 +121,8 @@ async def _check_matcher(
dependency_cache: Optional[T_DependencyCache] = None,
) -> None:
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
try:
with contextlib.suppress(Exception):
matchers[priority].remove(Matcher)
except Exception:
pass
return
try:
@@ -145,11 +137,8 @@ async def _check_matcher(
return
if Matcher.temp:
try:
with contextlib.suppress(Exception):
matchers[priority].remove(Matcher)
except Exception:
pass
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
@@ -164,65 +153,58 @@ async def _run_matcher(
logger.info(f"Event will be handled by {Matcher}")
matcher = Matcher()
coros = list(
map(
lambda x: _run_coro_with_catch(
x(
if coros := [
run_coro_with_catch(
proc(
matcher=matcher,
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
),
_run_preprocessors,
(SkippedException,),
)
)
if coros:
for proc in _run_preprocessors
]:
try:
await asyncio.gather(*coros)
except IgnoredException:
logger.opt(colors=True).info(
f"Matcher {matcher} running is <b>cancelled</b>"
)
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
return
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
"Running cancelled!</bg #f8bbd0></r>"
"<r><bg #f8bbd0>Error when running RunPreProcessors. Running cancelled!</bg #f8bbd0></r>"
)
return
exception = None
try:
logger.debug(f"Running matcher {matcher}")
logger.debug(f"Running {matcher}")
await matcher.run(bot, event, state, stack, dependency_cache)
except Exception as e:
logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
f"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>"
)
exception = e
coros = list(
map(
lambda x: _run_coro_with_catch(
x(
if coros := [
run_coro_with_catch(
proc(
matcher=matcher,
exception=exception,
bot=bot,
event=event,
state=state,
state=matcher.state,
stack=stack,
dependency_cache=dependency_cache,
)
),
_run_postprocessors,
(SkippedException,),
)
)
if coros:
for proc in _run_postprocessors
]:
try:
await asyncio.gather(*coros)
except Exception as e:
@@ -249,7 +231,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
```
"""
show_log = True
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | "
log_msg = f"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | "
try:
log_msg += event.get_log_string()
except NoLogException:
@@ -261,21 +243,19 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
coros = list(
map(
lambda x: _run_coro_with_catch(
x(
if coros := [
run_coro_with_catch(
proc(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
),
_event_preprocessors,
(SkippedException,),
)
)
if coros:
for proc in _event_preprocessors
]:
try:
if show_log:
logger.debug("Running PreProcessors...")
@@ -328,21 +308,19 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
)
coros = list(
map(
lambda x: _run_coro_with_catch(
x(
if coros := [
run_coro_with_catch(
proc(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
),
_event_postprocessors,
(SkippedException,),
)
)
if coros:
for proc in _event_postprocessors
]:
try:
if show_log:
logger.debug("Running PostProcessors...")

View File

@@ -5,17 +5,16 @@ FrontMatter:
description: nonebot.params 模块
"""
from typing import Any, Dict, List, Tuple, Optional
from typing import Any, Dict, List, Tuple, Union, Optional
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.adapters import Event, Message
from nonebot.internal.params import Arg as Arg
from nonebot.internal.params import State as State
from nonebot.internal.params import ArgStr as ArgStr
from nonebot.internal.params import Depends as Depends
from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import BotParam as BotParam
from nonebot.adapters import Event, Message, MessageSegment
from nonebot.internal.params import EventParam as EventParam
from nonebot.internal.params import StateParam as StateParam
from nonebot.internal.params import DependParam as DependParam
@@ -32,6 +31,7 @@ from nonebot.consts import (
CMD_ARG_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
CMD_START_KEY,
REGEX_MATCHED,
)
@@ -99,16 +99,25 @@ def CommandArg() -> Any:
return Depends(_command_arg)
def _command_start(state: T_State) -> str:
return state[PREFIX_KEY][CMD_START_KEY]
def CommandStart() -> str:
"""消息命令开头"""
return Depends(_command_start)
def _shell_command_args(state: T_State) -> Any:
return state[SHELL_ARGS]
return state[SHELL_ARGS] # Namespace or ParserExit
def ShellCommandArgs():
def ShellCommandArgs() -> Any:
"""shell 命令解析后的参数字典"""
return Depends(_shell_command_args, use_cache=False)
def _shell_command_argv(state: T_State) -> List[str]:
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
return state[SHELL_ARGV]
@@ -164,7 +173,6 @@ def LastReceived(default: Any = None) -> Any:
__autodoc__ = {
"Arg": True,
"State": True,
"ArgStr": True,
"Depends": True,
"ArgParam": True,

View File

@@ -20,6 +20,9 @@ class Message:
__slots__ = ()
def __repr__(self) -> str:
return "Message()"
async def __call__(self, type: str = EventType()) -> bool:
return type == "message"
@@ -29,6 +32,9 @@ class Notice:
__slots__ = ()
def __repr__(self) -> str:
return "Notice()"
async def __call__(self, type: str = EventType()) -> bool:
return type == "notice"
@@ -38,6 +44,9 @@ class Request:
__slots__ = ()
def __repr__(self) -> str:
return "Request()"
async def __call__(self, type: str = EventType()) -> bool:
return type == "request"
@@ -47,6 +56,9 @@ class MetaEvent:
__slots__ = ()
def __repr__(self) -> str:
return "MetaEvent()"
async def __call__(self, type: str = EventType()) -> bool:
return type == "meta_event"
@@ -78,16 +90,23 @@ class SuperUser:
__slots__ = ()
def __repr__(self) -> str:
return "Superuser()"
async def __call__(self, bot: Bot, event: Event) -> bool:
return event.get_type() == "message" and (
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
try:
user_id = event.get_user_id()
except Exception:
return False
return (
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{user_id}"
in bot.config.superusers
or event.get_user_id() in bot.config.superusers # 兼容旧配置
or user_id in bot.config.superusers # 兼容旧配置
)
SUPERUSER: Permission = Permission(SuperUser())
"""匹配任意超级用户消息类型事件"""
"""匹配任意超级用户事件"""
__autodoc__ = {
"Permission": True,

View File

@@ -11,10 +11,12 @@
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
@@ -24,28 +26,87 @@
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
- `export` => {ref}``export` <nonebot.plugin.export.export>`
- `require` => {ref}``require` <nonebot.plugin.load.require>`
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
FrontMatter:
sidebar_position: 0
description: nonebot.plugin 模块
"""
from typing import List, Optional
from itertools import chain
from types import ModuleType
from contextvars import ContextVar
from typing import Set, Dict, List, Tuple, Optional
_plugins: Dict[str, "Plugin"] = {}
_managers: List["PluginManager"] = []
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
"_current_plugin", default=None
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
"_current_plugin_chain", default=tuple()
)
def _module_name_to_plugin_name(module_name: str) -> str:
return module_name.rsplit(".", 1)[-1]
def _new_plugin(
module_name: str, module: ModuleType, manager: "PluginManager"
) -> "Plugin":
plugin_name = _module_name_to_plugin_name(module_name)
if plugin_name in _plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugin = Plugin(plugin_name, module, module_name, manager)
_plugins[plugin_name] = plugin
return plugin
def _revert_plugin(plugin: "Plugin") -> None:
if plugin.name not in _plugins:
raise RuntimeError("Plugin not found!")
del _plugins[plugin.name]
def get_plugin(name: str) -> Optional["Plugin"]:
"""获取已经导入的某个插件。
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
参数:
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
"""
return _plugins.get(name)
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
"""通过模块名获取已经导入的某个插件。
如果提供的模块名为某个插件的子模块,同样会返回该插件。
参数:
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
"""
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
has_parent = True
while has_parent:
if module_name in loaded:
return loaded[module_name]
module_name, *has_parent = module_name.rsplit(".", 1)
def get_loaded_plugins() -> Set["Plugin"]:
"""获取当前已导入的所有插件。"""
return set(_plugins.values())
def get_available_plugin_names() -> Set[str]:
"""获取当前所有可用的插件名(包含尚未加载的插件)。"""
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
from .on import on as on
from .manager import PluginManager
from .export import Export as Export
from .export import export as export
from .on import on_type as on_type
from .load import require as require
from .on import on_regex as on_regex
from .plugin import Plugin as Plugin
@@ -58,14 +119,14 @@ from .on import on_endswith as on_endswith
from .load import load_plugin as load_plugin
from .on import CommandGroup as CommandGroup
from .on import MatcherGroup as MatcherGroup
from .on import on_fullmatch as on_fullmatch
from .on import on_metaevent as on_metaevent
from .plugin import get_plugin as get_plugin
from .load import load_plugins as load_plugins
from .on import on_startswith as on_startswith
from .load import load_from_json as load_from_json
from .load import load_from_toml as load_from_toml
from .on import on_shell_command as on_shell_command
from .plugin import PluginMetadata as PluginMetadata
from .load import load_all_plugins as load_all_plugins
from .load import load_builtin_plugin as load_builtin_plugin
from .plugin import get_loaded_plugins as get_loaded_plugins
from .load import load_builtin_plugins as load_builtin_plugins

View File

@@ -1,57 +0,0 @@
"""本模块定义了插件导出的内容对象。
在新版插件系统中,推荐优先使用直接 import 所需要的插件内容。
FrontMatter:
sidebar_position: 4
description: nonebot.plugin.export 模块
"""
from . import _current_plugin
class Export(dict):
"""插件导出内容以使得其他插件可以获得。
用法:
```python
nonebot.export().default = "bar"
@nonebot.export()
def some_function():
pass
# this doesn't work before python 3.9
# use
# export = nonebot.export(); @export.sub
# instead
# See also PEP-614: https://www.python.org/dev/peps/pep-0614/
@nonebot.export().sub
def something_else():
pass
```
"""
def __call__(self, func, **kwargs):
self[func.__name__] = func
self.update(kwargs)
return func
def __setitem__(self, key, value):
super().__setitem__(key, Export(value) if isinstance(value, dict) else value)
def __setattr__(self, name, value):
self[name] = Export(value) if isinstance(value, dict) else value
def __getattr__(self, name):
if name not in self:
self[name] = Export()
return self[name]
def export() -> Export:
"""获取当前插件的导出内容对象"""
plugin = _current_plugin.get()
if not plugin:
raise RuntimeError("Export outside of the plugin!")
return plugin.export

View File

@@ -5,24 +5,30 @@ FrontMatter:
description: nonebot.plugin.load 模块
"""
import json
import warnings
from typing import Set, Iterable, Optional
from pathlib import Path
from types import ModuleType
from typing import Set, Union, Iterable, Optional
import tomlkit
from . import _managers
from .export import Export
from nonebot.utils import path_to_module_name
from .plugin import Plugin
from .manager import PluginManager
from .plugin import Plugin, get_plugin
from . import _managers, get_plugin, _module_name_to_plugin_name
def load_plugin(module_path: str) -> Optional[Plugin]:
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`
"""
module_path = (
path_to_module_name(module_path)
if isinstance(module_path, Path)
else module_path
)
manager = PluginManager([module_path])
_managers.append(manager)
return manager.load_plugin(module_path)
@@ -74,6 +80,8 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
"""
with open(file_path, "r", encoding=encoding) as f:
data = json.load(f)
if not isinstance(data, dict):
raise TypeError("json file must contains a dict!")
plugins = data.get("plugins")
plugin_dirs = data.get("plugin_dirs")
assert isinstance(plugins, list), "plugins must be a list of plugin name"
@@ -103,15 +111,10 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
data = tomlkit.parse(f.read()) # type: ignore
nonebot_data = data.get("tool", {}).get("nonebot")
if not nonebot_data:
nonebot_data = data.get("nonebot", {}).get("plugins")
if nonebot_data:
warnings.warn(
"[nonebot.plugins] table are now deprecated. Use [tool.nonebot] instead.",
DeprecationWarning,
)
else:
if nonebot_data is None:
raise ValueError("Cannot find '[tool.nonebot]' in given toml file!")
if not isinstance(nonebot_data, dict):
raise TypeError("'[tool.nonebot]' must be a Table!")
plugins = nonebot_data.get("plugins", [])
plugin_dirs = nonebot_data.get("plugin_dirs", [])
assert isinstance(plugins, list), "plugins must be a list of plugin name"
@@ -128,7 +131,7 @@ def load_builtin_plugin(name: str) -> Optional[Plugin]:
return load_plugin(f"nonebot.plugins.{name}")
def load_builtin_plugins(*plugins) -> Set[Plugin]:
def load_builtin_plugins(*plugins: str) -> Set[Plugin]:
"""导入多个 NoneBot 内置插件。
参数:
@@ -143,7 +146,7 @@ def _find_manager_by_name(name: str) -> Optional[PluginManager]:
return manager
def require(name: str) -> Export:
def require(name: str) -> ModuleType:
"""获取一个插件的导出内容。
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
@@ -154,13 +157,12 @@ def require(name: str) -> Export:
异常:
RuntimeError: 插件无法加载
"""
plugin = get_plugin(name.rsplit(".", 1)[-1])
plugin = get_plugin(_module_name_to_plugin_name(name))
if not plugin:
manager = _find_manager_by_name(name)
if manager:
if manager := _find_manager_by_name(name):
plugin = manager.load_plugin(name)
else:
plugin = load_plugin(name)
if not plugin:
raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.export
return plugin.module

View File

@@ -17,94 +17,133 @@ from importlib.machinery import PathFinder, SourceFileLoader
from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
from nonebot.log import logger
from nonebot.utils import escape_tag
from nonebot.utils import escape_tag, path_to_module_name
from . import _managers, _current_plugin
from .plugin import Plugin, _new_plugin, _confirm_plugin
from .plugin import Plugin, PluginMetadata
from . import (
_managers,
_new_plugin,
_revert_plugin,
_current_plugin_chain,
_module_name_to_plugin_name,
)
class PluginManager:
"""插件管理器。
参数:
plugins: 独立插件模块名集合。
search_path: 插件搜索路径(文件夹)。
"""
def __init__(
self,
plugins: Optional[Iterable[str]] = None,
search_path: Optional[Iterable[str]] = None,
):
# simple plugin not in search path
self.plugins: Set[str] = set(plugins or [])
self.search_path: Set[str] = set(search_path or [])
# cache plugins
self.searched_plugins: Dict[str, Path] = {}
self.list_plugins()
self._third_party_plugin_names: Dict[str, str] = {}
self._searched_plugin_names: Dict[str, Path] = {}
self.prepare_plugins()
def _path_to_module_name(self, path: Path) -> str:
rel_path = path.resolve().relative_to(Path(".").resolve())
if rel_path.stem == "__init__":
return ".".join(rel_path.parts[:-1])
else:
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
def __repr__(self) -> str:
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
def _previous_plugins(self) -> List[str]:
@property
def third_party_plugins(self) -> Set[str]:
"""返回所有独立插件名称。"""
return set(self._third_party_plugin_names.keys())
@property
def searched_plugins(self) -> Set[str]:
"""返回已搜索到的插件名称。"""
return set(self._searched_plugin_names.keys())
@property
def available_plugins(self) -> Set[str]:
"""返回当前插件管理器中可用的插件名称。"""
return self.third_party_plugins | self.searched_plugins
def _previous_plugins(self) -> Set[str]:
_pre_managers: List[PluginManager]
if self in _managers:
_pre_managers = _managers[: _managers.index(self)]
else:
_pre_managers = _managers[:]
return [
*chain.from_iterable(
[
*map(lambda x: x.rsplit(".", 1)[-1], manager.plugins),
*manager.searched_plugins.keys(),
]
for manager in _pre_managers
)
]
return {
*chain.from_iterable(manager.available_plugins for manager in _pre_managers)
}
def list_plugins(self) -> Set[str]:
def prepare_plugins(self) -> Set[str]:
"""搜索插件并缓存插件名称。"""
# get all previous ready to load plugins
previous_plugins = self._previous_plugins()
searched_plugins: Dict[str, Path] = {}
third_party_plugins: Set[str] = set()
third_party_plugins: Dict[str, str] = {}
# check third party plugins
for plugin in self.plugins:
name = plugin.rsplit(".", 1)[-1]
name = _module_name_to_plugin_name(plugin)
if name in third_party_plugins or name in previous_plugins:
raise RuntimeError(
f"Plugin already exists: {name}! Check your plugin name"
)
third_party_plugins.add(plugin)
third_party_plugins[name] = plugin
self._third_party_plugin_names = third_party_plugins
# check plugins in search path
for module_info in pkgutil.iter_modules(self.search_path):
# ignore if startswith "_"
if module_info.name.startswith("_"):
continue
if (
module_info.name in searched_plugins.keys()
module_info.name in searched_plugins
or module_info.name in previous_plugins
or module_info.name in third_party_plugins
):
raise RuntimeError(
f"Plugin already exists: {module_info.name}! Check your plugin name"
)
module_spec = module_info.module_finder.find_spec(module_info.name, None)
if not module_spec:
if not (
module_spec := module_info.module_finder.find_spec(
module_info.name, None
)
):
continue
module_path = module_spec.origin
if not module_path:
if not (module_path := module_spec.origin):
continue
searched_plugins[module_info.name] = Path(module_path).resolve()
self.searched_plugins = searched_plugins
self._searched_plugin_names = searched_plugins
return third_party_plugins | set(self.searched_plugins.keys())
return self.available_plugins
def load_plugin(self, name: str) -> Optional[Plugin]:
"""加载指定插件。
对于独立插件,可以使用完整插件模块名或者插件名称。
参数:
name: 插件名称。
"""
try:
if name in self.plugins:
module = importlib.import_module(name)
elif name in self.searched_plugins:
elif name in self._third_party_plugin_names:
module = importlib.import_module(self._third_party_plugin_names[name])
elif name in self._searched_plugin_names:
module = importlib.import_module(
self._path_to_module_name(self.searched_plugins[name])
path_to_module_name(self._searched_plugin_names[name])
)
else:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
@@ -112,8 +151,7 @@ class PluginManager:
logger.opt(colors=True).success(
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
)
plugin = getattr(module, "__plugin__", None)
if plugin is None:
if (plugin := getattr(module, "__plugin__", None)) is None:
raise RuntimeError(
f"Module {module.__name__} is not loaded as a plugin! "
"Make sure not to import it before loading."
@@ -125,8 +163,10 @@ class PluginManager:
)
def load_all_plugins(self) -> Set[Plugin]:
"""加载所有可用插件。"""
return set(
filter(None, (self.load_plugin(name) for name in self.list_plugins()))
filter(None, (self.load_plugin(name) for name in self.available_plugins))
)
@@ -147,9 +187,10 @@ class PluginFinder(MetaPathFinder):
module_path = Path(module_origin).resolve()
for manager in reversed(_managers):
# use path instead of name in case of submodule name conflict
if (
fullname in manager.plugins
or module_path in manager.searched_plugins.values()
or module_path in manager._searched_plugin_names.values()
):
module_spec.loader = PluginLoader(manager, fullname, module_origin)
return module_spec
@@ -173,29 +214,34 @@ class PluginLoader(SourceFileLoader):
if self.loaded:
return
# create plugin before executing
plugin = _new_plugin(self.name, module, self.manager)
parent_plugin = _current_plugin.get()
if parent_plugin and _managers.index(parent_plugin.manager) < _managers.index(
self.manager
):
plugin.parent_plugin = parent_plugin
parent_plugin.sub_plugins.add(plugin)
_plugin_token = _current_plugin.set(plugin)
setattr(module, "__plugin__", plugin)
# try:
# super().exec_module(module)
# except Exception as e:
# raise ImportError(
# f"Error when executing module {module_name} from {module.__file__}."
# ) from e
# detect parent plugin before entering current plugin context
parent_plugins = _current_plugin_chain.get()
for pre_plugin in reversed(parent_plugins):
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
plugin.parent_plugin = pre_plugin
pre_plugin.sub_plugins.add(plugin)
break
# enter plugin context
_plugin_token = _current_plugin_chain.set(parent_plugins + (plugin,))
try:
super().exec_module(module)
except Exception:
_revert_plugin(plugin)
raise
finally:
# leave plugin context
_current_plugin_chain.reset(_plugin_token)
_confirm_plugin(plugin)
# get plugin metadata
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
plugin.metadata = metadata
_current_plugin.reset(_plugin_token)
return

View File

@@ -5,11 +5,12 @@ FrontMatter:
description: nonebot.plugin.on 模块
"""
import re
import sys
import inspect
from types import ModuleType
from datetime import datetime, timedelta
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot.permission import Permission
from nonebot.dependencies import Dependent
@@ -19,20 +20,21 @@ from nonebot.rule import (
ArgumentParser,
regex,
command,
is_type,
keyword,
endswith,
fullmatch,
startswith,
shell_command,
)
from .manager import _current_plugin
from .manager import _current_plugin_chain
def _store_matcher(matcher: Type[Matcher]) -> None:
plugin = _current_plugin.get()
# only store the matcher defined in the plugin
if plugin:
plugin.matcher.add(matcher)
if plugins := _current_plugin_chain.get():
plugins[-1].matcher.add(matcher)
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
@@ -40,8 +42,7 @@ def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
if current_frame is None:
return None
frame = inspect.getouterframes(current_frame)[depth + 1].frame
module_name = frame.f_globals["__name__"]
return sys.modules.get(module_name)
return inspect.getmodule(frame)
def on(
@@ -51,13 +52,13 @@ def on(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个基础事件响应器,可自定义类型。
"""注册一个基础事件响应器,可自定义类型。
参数:
type: 事件响应器类型
@@ -65,19 +66,22 @@ def on(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
type,
Rule() & rule,
Permission() | permission,
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
@@ -90,31 +94,34 @@ def on_metaevent(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个元事件响应器。
"""注册一个元事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
"meta_event",
Rule() & rule,
Permission(),
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
@@ -128,32 +135,35 @@ def on_message(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,
block: bool = True,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个消息事件响应器。
"""注册一个消息事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
"message",
Rule() & rule,
Permission() | permission,
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
@@ -166,31 +176,34 @@ def on_notice(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个通知事件响应器。
"""注册一个通知事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
"notice",
Rule() & rule,
Permission(),
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
@@ -203,31 +216,34 @@ def on_request(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个请求事件响应器。
"""注册一个请求事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
"request",
Rule() & rule,
Permission(),
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
@@ -237,13 +253,12 @@ def on_request(
def on_startswith(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数:
msg: 指定消息开头内容
@@ -252,6 +267,7 @@ def on_startswith(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
@@ -261,13 +277,12 @@ def on_startswith(
def on_endswith(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数:
msg: 指定消息结尾内容
@@ -276,6 +291,7 @@ def on_endswith(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
@@ -283,14 +299,37 @@ def on_endswith(
return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
def on_fullmatch(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
ignorecase: bool = False,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数:
msg: 指定消息全匹配内容
rule: 事件响应规则
ignorecase: 是否忽略大小写
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on_message(fullmatch(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
def on_keyword(
keywords: Set[str],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数:
keywords: 关键词列表
@@ -298,6 +337,7 @@ def on_keyword(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
@@ -312,8 +352,7 @@ def on_command(
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
@@ -324,12 +363,13 @@ def on_command(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
commands = set([cmd]) | (aliases or set())
commands = {cmd} | (aliases or set())
block = kwargs.pop("block", False)
return on_message(
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
@@ -344,8 +384,7 @@ def on_shell_command(
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -359,12 +398,13 @@ def on_shell_command(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
commands = set([cmd]) | (aliases or set())
commands = {cmd} | (aliases or set())
return on_message(
shell_command(*commands, parser=parser) & rule,
**kwargs,
@@ -379,8 +419,7 @@ def on_regex(
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -391,6 +430,7 @@ def on_regex(
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
@@ -398,78 +438,134 @@ def on_regex(
return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)
class CommandGroup:
"""命令组,用于声明一组有相同名称前缀的命令。"""
def on_type(
types: Union[Type[Event], Tuple[Type[Event]]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:
types: 事件类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
event_types = types if isinstance(types, tuple) else (types,)
return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1)
class _Group:
def __init__(self, **kwargs):
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
self.matchers: List[Type[Matcher]] = []
"""组内事件响应器列表"""
self.base_kwargs: Dict[str, Any] = kwargs
"""其他传递给 `on` 的参数默认值"""
def _get_final_kwargs(
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
) -> Dict[str, Any]:
"""获取最终传递给 `on` 的参数
参数:
update: 更新的关键字参数
exclude: 需要排除的参数
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(update)
if exclude:
for key in exclude:
final_kwargs.pop(key, None)
final_kwargs["_depth"] = 1
return final_kwargs
class CommandGroup(_Group):
"""命令组,用于声明一组有相同名称前缀的命令。
参数:
cmd: 指定命令内容
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
"""
参数:
cmd: 命令前缀
**kwargs: `on_command` 的参数默认值,参考 `on_command <#on-command-cmd-rule-none-aliases-none-kwargs>`_
"""
"""命令前缀"""
super().__init__(**kwargs)
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
"""
命令前缀
"""
if "aliases" in kwargs:
del kwargs["aliases"]
self.base_kwargs: Dict[str, Any] = kwargs
"""
其他传递给 `on_command` 的参数默认值
"""
self.base_kwargs.pop("aliases", None)
def __repr__(self) -> str:
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
"""
注册一个新的命令。
"""注册一个新的命令。新参数将会覆盖命令组默认值
参数:
cmd: 命令前缀
**kwargs: `on_command` 的参数,将会覆盖命令组默认值
cmd: 指定命令内容
aliases: 命令别名
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
return on_command(cmd, **final_kwargs, _depth=1)
matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
self.matchers.append(matcher)
return matcher
def shell_command(
self, cmd: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
"""
注册一个新的命令。
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
参数:
cmd: 命令前缀
**kwargs: `on_shell_command` 的参数,将会覆盖命令组默认值
cmd: 指定命令内容
rule: 事件响应规则
aliases: 命令别名
parser: `nonebot.rule.ArgumentParser` 对象
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
return on_shell_command(cmd, **final_kwargs, _depth=1)
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
self.matchers.append(matcher)
return matcher
class MatcherGroup:
class MatcherGroup(_Group):
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
def __init__(self, **kwargs):
"""
创建一个事件响应器组合,参数为默认值,与 `on` 一致
"""
self.matchers: List[Type[Matcher]] = []
"""
组内事件响应器列表
"""
self.base_kwargs: Dict[str, Any] = kwargs
"""
其他传递给 `on` 的参数默认值
"""
def __repr__(self) -> str:
return f"MatcherGroup(matchers={len(self.matchers)})"
def on(self, **kwargs) -> Type[Matcher]:
"""
注册一个基础事件响应器,可自定义类型。
"""注册一个基础事件响应器,可自定义类型。
参数:
type: 事件响应器类型
@@ -477,99 +573,88 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
matcher = on(**final_kwargs, _depth=1)
matcher = on(**self._get_final_kwargs(kwargs))
self.matchers.append(matcher)
return matcher
def on_metaevent(self, **kwargs) -> Type[Matcher]:
"""
注册一个元事件响应器。
"""注册一个元事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
final_kwargs.pop("permission", None)
matcher = on_metaevent(**final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
matcher = on_metaevent(**final_kwargs)
self.matchers.append(matcher)
return matcher
def on_message(self, **kwargs) -> Type[Matcher]:
"""
注册一个消息事件响应器。
"""注册一个消息事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_message(**final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_message(**final_kwargs)
self.matchers.append(matcher)
return matcher
def on_notice(self, **kwargs) -> Type[Matcher]:
"""
注册一个通知事件响应器。
"""注册一个通知事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_notice(**final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
matcher = on_notice(**final_kwargs)
self.matchers.append(matcher)
return matcher
def on_request(self, **kwargs) -> Type[Matcher]:
"""
注册一个请求事件响应器。
"""注册一个请求事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_request(**final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
matcher = on_request(**final_kwargs)
self.matchers.append(matcher)
return matcher
def on_startswith(
self, msg: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数:
msg: 指定消息开头内容
@@ -578,20 +663,18 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_startswith(msg, **final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_startswith(msg, **final_kwargs)
self.matchers.append(matcher)
return matcher
def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数:
msg: 指定消息结尾内容
@@ -600,20 +683,38 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_endswith(msg, **final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_endswith(msg, **final_kwargs)
self.matchers.append(matcher)
return matcher
def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数:
msg: 指定消息全匹配内容
rule: 事件响应规则
ignorecase: 是否忽略大小写
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_fullmatch(msg, **final_kwargs)
self.matchers.append(matcher)
return matcher
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数:
keywords: 关键词列表
@@ -621,14 +722,13 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_keyword(keywords, **final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_keyword(keywords, **final_kwargs)
self.matchers.append(matcher)
return matcher
@@ -638,8 +738,7 @@ class MatcherGroup:
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
**kwargs,
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
@@ -650,14 +749,13 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_command(cmd, aliases=aliases, **final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_command(cmd, aliases=aliases, **final_kwargs)
self.matchers.append(matcher)
return matcher
@@ -668,8 +766,7 @@ class MatcherGroup:
parser: Optional[ArgumentParser] = None,
**kwargs,
) -> Type[Matcher]:
"""
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -683,24 +780,20 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_shell_command(
cmd, aliases=aliases, parser=parser, **final_kwargs, _depth=1
)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs)
self.matchers.append(matcher)
return matcher
def on_regex(
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
) -> Type[Matcher]:
"""
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -711,13 +804,33 @@ class MatcherGroup:
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
matcher = on_regex(pattern, flags=flags, **final_kwargs, _depth=1)
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_regex(pattern, flags=flags, **final_kwargs)
self.matchers.append(matcher)
return matcher
def on_type(
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
) -> Type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:
types: 事件类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_type(types, **final_kwargs)
self.matchers.append(matcher)
return matcher

View File

@@ -1,6 +1,8 @@
import re
from datetime import datetime, timedelta
from typing import Set, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot.permission import Permission
from nonebot.dependencies import Dependent
@@ -14,6 +16,7 @@ def on(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -23,6 +26,7 @@ def on_metaevent(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -33,6 +37,7 @@ def on_message(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -42,6 +47,7 @@ def on_notice(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -51,30 +57,46 @@ def on_request(
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_startswith(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
ignorecase: bool = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_endswith(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
ignorecase: bool = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_fullmatch(
msg: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
ignorecase: bool = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -86,6 +108,7 @@ def on_keyword(
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -98,6 +121,7 @@ def on_command(
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -111,6 +135,7 @@ def on_shell_command(
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -123,6 +148,19 @@ def on_regex(
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_type(
types: Union[Type[Event], Tuple[Type[Event]]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -137,6 +175,7 @@ class CommandGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -145,11 +184,12 @@ class CommandGroup:
self,
cmd: Union[str, Tuple[str, ...]],
*,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -159,11 +199,12 @@ class CommandGroup:
cmd: Union[str, Tuple[str, ...]],
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
parser: Optional[ArgumentParser] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -178,6 +219,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -190,6 +232,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -200,6 +243,7 @@ class MatcherGroup:
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -211,6 +255,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -221,6 +266,7 @@ class MatcherGroup:
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -231,6 +277,7 @@ class MatcherGroup:
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -244,6 +291,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -257,6 +305,21 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_fullmatch(
self,
msg: Union[str, Tuple[str, ...]],
*,
ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -269,6 +332,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -282,6 +346,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -296,6 +361,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
@@ -309,6 +375,20 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_type(
self,
types: Union[Type[Event], Tuple[Type[Event]]],
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,

View File

@@ -6,66 +6,49 @@ FrontMatter:
"""
from types import ModuleType
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Set, Dict, Type, Optional
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
from pydantic import BaseModel
from nonebot.matcher import Matcher
from .export import Export
from . import _plugins as plugins # FIXME: backport for nonebug
if TYPE_CHECKING:
from .manager import PluginManager
plugins: Dict[str, "Plugin"] = {}
"""已加载的插件"""
@dataclass(eq=False)
class PluginMetadata:
"""插件元信息,由插件编写者提供"""
name: str
"""插件可阅读名称"""
description: str
"""插件功能介绍"""
usage: str
"""插件使用方法"""
config: Optional[Type[BaseModel]] = None
"""插件配置项"""
extra: Dict[Any, Any] = field(default_factory=dict)
@dataclass(eq=False)
class Plugin(object):
class Plugin:
"""存储插件信息"""
name: str
"""插件名称,使用 文件/文件夹 名称作为插件名"""
"""插件索引标识NoneBot 使用 文件/文件夹 名称作为标识符"""
module: ModuleType
"""插件模块对象"""
module_name: str
"""点分割模块路径"""
manager: "PluginManager"
"""导入该插件的插件管理器"""
export: Export = field(default_factory=Export)
"""插件内定义的导出内容"""
matcher: Set[Type[Matcher]] = field(default_factory=set)
"""插件内定义的 `Matcher`"""
parent_plugin: Optional["Plugin"] = None
"""父插件"""
sub_plugins: Set["Plugin"] = field(default_factory=set)
"""子插件集合"""
def get_plugin(name: str) -> Optional[Plugin]:
"""获取已经导入的某个插件。
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
参数:
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
"""
return plugins.get(name)
def get_loaded_plugins() -> Set[Plugin]:
"""获取当前已导入的所有插件。"""
return set(plugins.values())
def _new_plugin(fullname: str, module: ModuleType, manager: "PluginManager") -> Plugin:
name = fullname.rsplit(".", 1)[-1] if "." in fullname else fullname
if name in plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugin = Plugin(name, module, fullname, manager)
return plugin
def _confirm_plugin(plugin: Plugin) -> None:
if plugin.name in plugins:
raise RuntimeError("Plugin already exists! Check your plugin name.")
plugins[plugin.name] = plugin
metadata: Optional[PluginMetadata] = None

View File

@@ -7,5 +7,5 @@ echo = on_command("echo", to_me())
@echo.handle()
async def echo_escape(message: Message = CommandArg()):
async def handle_echo(message: Message = CommandArg()):
await echo.send(message=message)

View File

@@ -15,8 +15,7 @@ async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
yield result
else:
current_event_id = id(event)
event_id = _running_matcher.get(session_id, None)
if event_id:
if event_id := _running_matcher.get(session_id, None):
result = event_id != current_event_id
else:
_running_matcher[session_id] = current_event_id

View File

@@ -10,11 +10,27 @@ FrontMatter:
import re
import shlex
from itertools import product
from argparse import Namespace
from typing_extensions import TypedDict
from argparse import Action
from argparse import ArgumentError
from itertools import chain, product
from argparse import Namespace as Namespace
from argparse import ArgumentParser as ArgParser
from typing import Any, List, Tuple, Union, Optional, Sequence
from typing import (
IO,
TYPE_CHECKING,
Any,
List,
Type,
Tuple,
Union,
TypeVar,
Optional,
Sequence,
TypedDict,
NamedTuple,
cast,
overload,
)
from pygtrie import CharTrie
@@ -23,15 +39,8 @@ from nonebot.log import logger
from nonebot.typing import T_State
from nonebot.exception import ParserExit
from nonebot.internal.rule import Rule as Rule
from nonebot.params import Command, EventToMe, CommandArg
from nonebot.adapters import Bot, Event, Message, MessageSegment
from nonebot.params import (
Command,
EventToMe,
EventType,
CommandArg,
EventMessage,
EventPlainText,
)
from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
@@ -41,24 +50,32 @@ from nonebot.consts import (
CMD_ARG_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
CMD_START_KEY,
REGEX_MATCHED,
)
T = TypeVar("T")
CMD_RESULT = TypedDict(
"CMD_RESULT",
{
"command": Optional[Tuple[str, ...]],
"raw_command": Optional[str],
"command_arg": Optional[Message[MessageSegment]],
"command_start": Optional[str],
},
)
TRIE_VALUE = NamedTuple(
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
)
class TrieRule:
prefix: CharTrie = CharTrie()
@classmethod
def add_prefix(cls, prefix: str, value: Any):
def add_prefix(cls, prefix: str, value: TRIE_VALUE) -> None:
if prefix in cls.prefix:
logger.warning(f'Duplicated prefix rule "{prefix}"')
return
@@ -66,7 +83,9 @@ class TrieRule:
@classmethod
def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:
prefix = CMD_RESULT(command=None, raw_command=None, command_arg=None)
prefix = CMD_RESULT(
command=None, raw_command=None, command_arg=None, command_start=None
)
state[PREFIX_KEY] = prefix
if event.get_type() != "message":
return prefix
@@ -75,10 +94,11 @@ class TrieRule:
message_seg: MessageSegment = message[0]
if message_seg.is_text():
segment_text = str(message_seg).lstrip()
pf = cls.prefix.longest_prefix(segment_text)
if pf := cls.prefix.longest_prefix(segment_text):
value: TRIE_VALUE = pf.value
prefix[RAW_CMD_KEY] = pf.key
prefix[CMD_KEY] = pf.value
if pf.key:
prefix[CMD_START_KEY] = value.command_start
prefix[CMD_KEY] = value.command
msg = message.copy()
msg.pop(0)
new_message = msg.__class__(segment_text[len(pf.key) :].lstrip())
@@ -103,10 +123,25 @@ class StartswithRule:
self.msg = msg
self.ignorecase = ignorecase
async def __call__(
self, type: str = EventType(), text: str = EventPlainText()
) -> Any:
if type != "message":
def __repr__(self) -> str:
return f"Startswith(msg={self.msg}, ignorecase={self.ignorecase})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, StartswithRule)
and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
)
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
return False
return bool(
re.match(
@@ -144,10 +179,25 @@ class EndswithRule:
self.msg = msg
self.ignorecase = ignorecase
async def __call__(
self, type: str = EventType(), text: str = EventPlainText()
) -> Any:
if type != "message":
def __repr__(self) -> str:
return f"Endswith(msg={self.msg}, ignorecase={self.ignorecase})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, EndswithRule)
and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
)
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
return False
return bool(
re.search(
@@ -171,6 +221,56 @@ def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule
return Rule(EndswithRule(msg, ignorecase))
class FullmatchRule:
"""检查消息纯文本是否与指定字符串全匹配。
参数:
msg: 指定消息全匹配字符串元组
ignorecase: 是否忽略大小写
"""
__slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
self.ignorecase = ignorecase
def __repr__(self) -> str:
return f"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, FullmatchRule)
and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
)
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
return False
return (text.casefold() if self.ignorecase else text) in self.msg
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""完全匹配消息。
参数:
msg: 指定消息全匹配字符串元组
ignorecase: 是否忽略大小写
"""
if isinstance(msg, str):
msg = (msg,)
return Rule(FullmatchRule(msg, ignorecase))
class KeywordsRule:
"""检查消息纯文本是否包含指定关键字。
@@ -183,10 +283,23 @@ class KeywordsRule:
def __init__(self, *keywords: str):
self.keywords = keywords
async def __call__(
self, type: str = EventType(), text: str = EventPlainText()
) -> bool:
if type != "message":
def __repr__(self) -> str:
return f"Keywords(keywords={self.keywords})"
def __eq__(self, other: object) -> bool:
return isinstance(other, KeywordsRule) and frozenset(
self.keywords
) == frozenset(other.keywords)
def __hash__(self) -> int:
return hash(frozenset(self.keywords))
async def __call__(self, event: Event) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
return False
return bool(text and any(keyword in text for keyword in self.keywords))
@@ -211,14 +324,22 @@ class CommandRule:
__slots__ = ("cmds",)
def __init__(self, cmds: List[Tuple[str, ...]]):
self.cmds = cmds
self.cmds = tuple(cmds)
def __repr__(self) -> str:
return f"Command(cmds={self.cmds})"
def __eq__(self, other: object) -> bool:
return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset(
other.cmds
)
def __hash__(self) -> int:
return hash((frozenset(self.cmds),))
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
return cmd in self.cmds
def __repr__(self):
return f"<Command {self.cmds}>"
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
"""匹配消息命令。
@@ -256,10 +377,12 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
if len(command) == 1:
for start in command_start:
TrieRule.add_prefix(f"{start}{command[0]}", command)
TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command))
else:
for start, sep in product(command_start, command_sep):
TrieRule.add_prefix(f"{start}{sep.join(command)}", command)
TrieRule.add_prefix(
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
)
return Rule(CommandRule(commands))
@@ -272,25 +395,48 @@ class ArgumentParser(ArgParser):
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
"""
def _print_message(self, message, file=None):
old_message: str = getattr(self, "message", "")
if old_message:
old_message += "\n"
old_message += message
setattr(self, "message", old_message)
if TYPE_CHECKING:
def exit(self, status: int = 0, message: Optional[str] = None):
raise ParserExit(
status=status, message=message or getattr(self, "message", None)
)
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]] = ...
) -> Namespace:
...
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: None
) -> Namespace:
... # type: ignore[misc]
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
) -> T:
...
def parse_args(
self,
args: Optional[Sequence[str]] = None,
namespace: Optional[Namespace] = None,
) -> Namespace:
setattr(self, "message", "")
return super().parse_args(args=args, namespace=namespace) # type: ignore
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: Optional[T] = None,
) -> Union[Namespace, T]:
...
def _parse_optional(
self, arg_string: Union[str, MessageSegment]
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
return (
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
)
def _print_message(self, message: str, file: Optional[IO[str]] = None):
if message:
setattr(self, "_message", getattr(self, "_message", "") + message)
def exit(self, status: int = 0, message: Optional[str] = None):
if message:
self._print_message(message)
raise ParserExit(status=status, message=getattr(self, "_message", None))
class ShellCommandRule:
@@ -304,27 +450,47 @@ class ShellCommandRule:
__slots__ = ("cmds", "parser")
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
self.cmds = cmds
self.cmds = tuple(cmds)
self.parser = parser
def __repr__(self) -> str:
return f"ShellCommand(cmds={self.cmds}, parser={self.parser})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, ShellCommandRule)
and frozenset(self.cmds) == frozenset(other.cmds)
and self.parser is other.parser
)
def __hash__(self) -> int:
return hash((frozenset(self.cmds), self.parser))
async def __call__(
self,
state: T_State,
cmd: Optional[Tuple[str, ...]] = Command(),
msg: Optional[Message] = CommandArg(),
) -> bool:
if cmd in self.cmds and msg is not None:
message = str(msg)
state[SHELL_ARGV] = shlex.split(message)
if cmd not in self.cmds or msg is None:
return False
state[SHELL_ARGV] = list(
chain.from_iterable(
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
for seg in msg
)
)
if self.parser:
try:
args = self.parser.parse_args(state[SHELL_ARGV])
state[SHELL_ARGS] = args
except ArgumentError as e:
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
except ParserExit as e:
state[SHELL_ARGS] = e
return True
else:
return False
def shell_command(
@@ -380,10 +546,12 @@ def shell_command(
if len(command) == 1:
for start in command_start:
TrieRule.add_prefix(f"{start}{command[0]}", command)
TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command))
else:
for start, sep in product(command_start, command_sep):
TrieRule.add_prefix(f"{start}{sep.join(command)}", command)
TrieRule.add_prefix(
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
)
return Rule(ShellCommandRule(commands, parser))
@@ -402,16 +570,27 @@ class RegexRule:
self.regex = regex
self.flags = flags
async def __call__(
self,
state: T_State,
type: str = EventType(),
msg: Message = EventMessage(),
) -> bool:
if type != "message":
def __repr__(self) -> str:
return f"Regex(regex={self.regex!r}, flags={self.flags})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, RegexRule)
and self.regex == other.regex
and self.flags == other.flags
)
def __hash__(self) -> int:
return hash((self.regex, self.flags))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
matched = re.search(self.regex, str(msg), self.flags)
if matched:
try:
msg = event.get_message()
except Exception:
return False
if matched := re.search(self.regex, str(msg), self.flags):
state[REGEX_MATCHED] = matched.group()
state[REGEX_GROUP] = matched.groups()
state[REGEX_DICT] = matched.groupdict()
@@ -448,6 +627,15 @@ class ToMeRule:
__slots__ = ()
def __repr__(self) -> str:
return "ToMe()"
def __eq__(self, other: object) -> bool:
return isinstance(other, ToMeRule)
def __hash__(self) -> int:
return hash((self.__class__,))
async def __call__(self, to_me: bool = EventToMe()) -> bool:
return to_me
@@ -458,6 +646,37 @@ def to_me() -> Rule:
return Rule(ToMeRule())
class IsTypeRule:
"""检查事件类型是否为指定类型。"""
__slots__ = ("types",)
def __init__(self, *types: Type[Event]):
self.types = types
def __repr__(self) -> str:
return f"IsType(types={tuple(type.__name__ for type in self.types)})"
def __eq__(self, other: object) -> bool:
return isinstance(other, IsTypeRule) and self.types == other.types
def __hash__(self) -> int:
return hash((self.types,))
async def __call__(self, event: Event) -> bool:
return isinstance(event, self.types)
def is_type(*types: Type[Event]) -> Rule:
"""匹配事件类型。
参数:
types: 事件类型
"""
return Rule(IsTypeRule(*types))
__autodoc__ = {
"Rule": True,
"Rule.__call__": True,

View File

@@ -11,6 +11,7 @@ FrontMatter:
sidebar_position: 11
description: nonebot.typing 模块
"""
from typing import (
TYPE_CHECKING,
Any,
@@ -28,6 +29,8 @@ if TYPE_CHECKING:
from nonebot.adapters import Bot
from nonebot.permission import Permission
T = TypeVar("T")
T_Wrapped = TypeVar("T_Wrapped", bound=Callable)
@@ -41,13 +44,33 @@ def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
return overrider
# state
T_State = Dict[Any, Any]
"""事件处理状态 State 类型"""
T_BotConnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接建立时钩子函数"""
T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接断开时钩子函数"""
_DependentCallable = Union[Callable[..., T], Callable[..., Awaitable[T]]]
# driver hooks
T_BotConnectionHook = _DependentCallable[Any]
"""Bot 连接建立时钩子函数
依赖参数:
- DependParam: 子依赖参数
- BotParam: Bot 对象
- DefaultParam: 带有默认值的参数
"""
T_BotDisconnectionHook = _DependentCallable[Any]
"""Bot 连接断开时钩子函数
依赖参数:
- DependParam: 子依赖参数
- BotParam: Bot 对象
- DefaultParam: 带有默认值的参数
"""
# api hooks
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
"""`bot.call_api` 钩子函数"""
T_CalledAPIHook = Callable[
@@ -55,7 +78,8 @@ T_CalledAPIHook = Callable[
]
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
# event hooks
T_EventPreProcessor = _DependentCallable[Any]
"""事件预处理函数 EventPreProcessor 类型
依赖参数:
@@ -66,7 +90,7 @@ T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
T_EventPostProcessor = _DependentCallable[Any]
"""事件预处理函数 EventPostProcessor 类型
依赖参数:
@@ -77,7 +101,9 @@ T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
# matcher run hooks
T_RunPreProcessor = _DependentCallable[Any]
"""事件响应器运行前预处理函数 RunPreProcessor 类型
依赖参数:
@@ -89,8 +115,8 @@ T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
"""事件响应器运行前预处理函数 RunPostProcessor 类型
T_RunPostProcessor = _DependentCallable[Any]
"""事件响应器运行后后处理函数 RunPostProcessor 类型
依赖参数:
@@ -103,7 +129,8 @@ T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
- DefaultParam: 带有默认值的参数
"""
T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
# rule, permission
T_RuleChecker = _DependentCallable[bool]
"""RuleChecker 即判断是否响应事件的处理函数。
依赖参数:
@@ -114,7 +141,7 @@ T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
- StateParam: State 对象
- DefaultParam: 带有默认值的参数
"""
T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
T_PermissionChecker = _DependentCallable[bool]
"""PermissionChecker 即判断事件是否满足权限的处理函数。
依赖参数:
@@ -125,9 +152,9 @@ T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
- DefaultParam: 带有默认值的参数
"""
T_Handler = Callable[..., Any]
T_Handler = _DependentCallable[Any]
"""Handler 处理函数。"""
T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
T_TypeUpdater = _DependentCallable[str]
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。
依赖参数:
@@ -139,7 +166,7 @@ T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]]
T_PermissionUpdater = _DependentCallable["Permission"]
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。
依赖参数:
@@ -151,5 +178,5 @@ T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]
- MatcherParam: Matcher 对象
- DefaultParam: 带有默认值的参数
"""
T_DependencyCache = Dict[Callable[..., Any], "Task[Any]"]
T_DependencyCache = Dict[_DependentCallable[Any], "Task[Any]"]
"""依赖缓存, 用于存储依赖函数的返回值"""

View File

@@ -10,6 +10,7 @@ import json
import asyncio
import inspect
import dataclasses
from pathlib import Path
from functools import wraps, partial
from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, get_args, get_origin
@@ -21,10 +22,10 @@ from typing import (
TypeVar,
Callable,
Optional,
Awaitable,
Coroutine,
AsyncGenerator,
ContextManager,
overload,
)
from pydantic.typing import is_union, is_none_type
@@ -63,12 +64,10 @@ def generic_check_issubclass(
except TypeError:
origin = get_origin(cls)
if is_union(origin):
for type_ in get_args(cls):
if not is_none_type(type_) and not generic_check_issubclass(
type_, class_or_tuple
):
return False
return True
return all(
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
for type_ in get_args(cls)
)
elif origin:
return issubclass(origin, class_or_tuple)
return False
@@ -132,6 +131,34 @@ async def run_sync_ctx_manager(
await run_sync(cm.__exit__)(None, None, None)
@overload
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
) -> Union[T, None]:
...
@overload
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: R,
) -> Union[T, R]:
...
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: Optional[R] = None,
) -> Optional[Union[T, R]]:
try:
return await coro
except exc:
return return_on_err
def get_name(obj: Any) -> str:
"""获取对象的名称"""
if inspect.isfunction(obj) or inspect.isclass(obj):
@@ -139,13 +166,21 @@ def get_name(obj: Any) -> str:
return obj.__class__.__name__
def path_to_module_name(path: Path) -> str:
rel_path = path.resolve().relative_to(Path(".").resolve())
if rel_path.stem == "__init__":
return ".".join(rel_path.parts[:-1])
else:
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
class DataclassEncoder(json.JSONEncoder):
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
@overrides(json.JSONEncoder)
def default(self, o):
if dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}
return super().default(o)
@@ -165,7 +200,7 @@ def logger_wrapper(logger_name: str):
def log(level: str, message: str, exception: Optional[Exception] = None):
logger.opt(colors=True, exception=exception).log(
level, f"<m>{escape_tag(logger_name)}</m> | " + message
level, f"<m>{escape_tag(logger_name)}</m> | {message}"
)
return log

View File

@@ -17,7 +17,7 @@ _✨ NoneBot 本地文档插件 ✨_
<a href="https://pypi.python.org/pypi/nonebot-plugin-docs">
<img src="https://img.shields.io/pypi/v/nonebot-plugin-docs.svg" alt="pypi">
</a>
<img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="python">
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="python">
</p>
## 使用方式

View File

@@ -12,7 +12,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
[tool.poetry.dependencies]
python = "^3.7.3"
python = "^3.8"
nonebot2 = "^2.0.0-beta.1"
[tool.poetry.dev-dependencies]

1462
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot2"
version = "2.0.0-beta.2"
version = "2.0.0-rc.1"
description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
@@ -22,27 +22,28 @@ packages = [
include = ["nonebot/py.typed"]
[tool.poetry.dependencies]
python = "^3.7.3"
python = "^3.8"
yarl = "^1.7.2"
loguru = "^0.6.0"
pygtrie = "^2.4.1"
tomlkit = "^0.9.0"
fastapi = "^0.73.0"
fastapi = "^0.79.0"
tomlkit = ">=0.10.0,<1.0.0"
typing-extensions = ">=3.10.0,<5.0.0"
Quart = { version = "^0.16.0", optional = true }
Quart = { version = "^0.17.0", optional = true }
websockets = { version="^10.0", optional = true }
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
uvicorn = { version = "^0.17.0", extras = ["standard"] }
uvicorn = { version = "^0.18.0", extras = ["standard"] }
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
[tool.poetry.dev-dependencies]
isort = "^5.10.1"
black = "^22.1.0"
nonemoji = "^0.1.2"
pytest-cov = "^3.0.0"
pre-commit = "^2.16.0"
pytest-xdist = "^2.5.0"
pytest-asyncio = "^0.18.1"
pytest-asyncio = "^0.19.0"
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
@@ -53,14 +54,13 @@ aiohttp = ["aiohttp"]
websockets = ["websockets"]
all = ["quart", "aiohttp", "httpx", "websockets"]
# [[tool.poetry.source]]
# name = "aliyun"
# url = "https://mirrors.aliyun.com/pypi/simple/"
# default = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "--cov=nonebot --cov-report=term-missing"
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]
[tool.black]
line-length = 88

View File

@@ -1,6 +1,7 @@
[report]
exclude_lines =
def __repr__
def __str__
pragma: no cover
if TYPE_CHECKING:
@(abc\.)?abstractmethod

View File

@@ -1,3 +1,5 @@
LOG_LEVEL=TRACE
NICKNAME=["test"]
SUPERUSERS=["test", "fake:faketest"]
CONFIG_FROM_ENV=
CONFIG_OVERRIDE=old

View File

@@ -0,0 +1,6 @@
import nonebot
plugin = nonebot.get_plugin("bad_plugin")
assert plugin
x = 1 / 0

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,2 @@
[tool]
nonebot = []

4
tests/plugins.json Normal file
View File

@@ -0,0 +1,4 @@
{
"plugins": [],
"plugin_dirs": ["plugins"]
}

3
tests/plugins.toml Normal file
View File

@@ -0,0 +1,3 @@
[tool.nonebot]
plugins = []
plugin_dirs = ["plugins"]

1
tests/plugins/_hidden.py Normal file
View File

@@ -0,0 +1 @@
assert False

View File

@@ -1,6 +1,2 @@
from nonebot import export
@export()
def test():
...
return "export"

View File

@@ -0,0 +1,9 @@
from datetime import datetime, timedelta
from nonebot.matcher import Matcher
test_temp_matcher = Matcher.new("test", temp=True)
test_datetime_matcher = Matcher.new(
"test", expire_time=datetime.now() - timedelta(seconds=1)
)
test_timedelta_matcher = Matcher.new("test", expire_time=timedelta(seconds=-1))

View File

@@ -1,10 +1,14 @@
from nonebot.matcher import Matcher
from nonebot.permission import Permission
from nonebot.permission import USER, Permission
default_permission = Permission()
test_permission_updater = Matcher.new(permission=default_permission)
test_user_permission_updater = Matcher.new(
permission=USER("test", perm=default_permission)
)
test_custom_updater = Matcher.new(permission=default_permission)

16
tests/plugins/metadata.py Normal file
View File

@@ -0,0 +1,16 @@
from pydantic import BaseModel
from nonebot.plugin import PluginMetadata
class Config(BaseModel):
custom: str = ""
__plugin_meta__ = PluginMetadata(
name="测试插件",
description="测试插件元信息",
usage="无法使用",
config=Config,
extra={"author": "NoneBot"},
)

View File

@@ -0,0 +1,13 @@
from pathlib import Path
import nonebot
from nonebot.plugin import PluginManager, _managers
manager = PluginManager(
search_path=[str((Path(__file__).parent / "plugins").resolve())]
)
_managers.append(manager)
# test load nested plugin with require
manager.load_plugin("nested_subplugin")
manager.load_plugin("nested_subplugin2")

View File

@@ -0,0 +1 @@
from .nested_subplugin2 import a

View File

@@ -0,0 +1 @@
a = "required by another subplugin"

View File

@@ -1,5 +1,35 @@
from typing import Union
from nonebot.adapters import Bot
async def get_bot(b: Bot):
async def get_bot(b: Bot) -> Bot:
return b
async def legacy_bot(bot):
return bot
async def not_legacy_bot(bot: int):
...
class FooBot(Bot):
...
async def sub_bot(b: FooBot) -> FooBot:
return b
class BarBot(Bot):
...
async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
return b
async def not_bot(b: Union[int, Bot]):
...

View File

@@ -1,3 +1,5 @@
from typing import Union
from nonebot.adapters import Event, Message
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
@@ -6,6 +8,34 @@ async def event(e: Event) -> Event:
return e
async def legacy_event(event):
return event
async def not_legacy_event(event: int):
...
class FooEvent(Event):
...
async def sub_event(e: FooEvent) -> FooEvent:
return e
class BarEvent(Event):
...
async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]:
return e
async def not_event(e: Union[int, Event]):
...
async def event_type(t: str = EventType()) -> str:
return t

View File

@@ -8,6 +8,7 @@ from nonebot.params import (
CommandArg,
RawCommand,
RegexGroup,
CommandStart,
RegexMatched,
ShellCommandArgs,
ShellCommandArgv,
@@ -18,6 +19,14 @@ async def state(x: T_State) -> T_State:
return x
async def legacy_state(state):
return state
async def not_legacy_state(state: int):
...
async def command(cmd: Tuple[str, ...] = Command()) -> Tuple[str, ...]:
return cmd
@@ -30,6 +39,10 @@ async def command_arg(cmd_arg: Message = CommandArg()) -> Message:
return cmd_arg
async def command_start(start: str = CommandStart()) -> str:
return start
async def shell_command_args(
shell_command_args: dict = ShellCommandArgs(),
) -> dict:

View File

@@ -0,0 +1 @@
from . import matchers

View File

@@ -0,0 +1,243 @@
from datetime import datetime, timezone
from nonebot.adapters import Event
from nonebot import (
CommandGroup,
MatcherGroup,
on,
on_type,
on_regex,
on_notice,
on_command,
on_keyword,
on_message,
on_request,
on_endswith,
on_fullmatch,
on_metaevent,
on_startswith,
on_shell_command,
)
async def rule() -> bool:
return True
async def permission() -> bool:
return True
async def handler():
return
expire_time = datetime.now(timezone.utc)
priority = 100
state = {"test": "test"}
matcher_on = on(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_metaevent = on_metaevent(
rule=rule,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_message = on_message(
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_notice = on_notice(
rule=rule,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_request = on_request(
rule=rule,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_startswith = on_startswith(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_endswith = on_endswith(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_fullmatch = on_fullmatch(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_keyword = on_keyword(
{"test"},
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_command = on_command(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_shell_command = on_shell_command(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_on_regex = on_regex(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
class TestEvent(Event):
...
matcher_on_type = on_type(
TestEvent,
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
cmd_group = CommandGroup(
"test",
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_sub_cmd = cmd_group.command("sub")
matcher_sub_shell_cmd = cmd_group.shell_command("sub")
matcher_group = MatcherGroup(
rule=rule,
permission=permission,
handlers=[handler],
temp=True,
expire_time=expire_time,
priority=priority,
block=True,
state=state,
)
matcher_group_on = matcher_group.on(type="test")
matcher_group_on_metaevent = matcher_group.on_metaevent()
matcher_group_on_message = matcher_group.on_message()
matcher_group_on_notice = matcher_group.on_notice()
matcher_group_on_request = matcher_group.on_request()
matcher_group_on_startswith = matcher_group.on_startswith("test")
matcher_group_on_endswith = matcher_group.on_endswith("test")
matcher_group_on_fullmatch = matcher_group.on_fullmatch("test")
matcher_group_on_keyword = matcher_group.on_keyword({"test"})
matcher_group_on_command = matcher_group.on_command("test")
matcher_group_on_shell_command = matcher_group.on_shell_command("test")
matcher_group_on_regex = matcher_group.on_regex("test")
matcher_group_on_type = matcher_group.on_type(TestEvent)

View File

@@ -1,8 +1,7 @@
from nonebot import require
from plugins.export import test
from .export import test as test_related
test_require = require("export").test
assert test is test_related and test is test_require, "Export Require Error"
from plugins.export import test
assert test is test_require and test() == "export", "Export Require Error"

View File

@@ -1,3 +1,4 @@
import pytest
from pydantic import ValidationError, parse_obj_as
from utils import make_fake_message
@@ -29,14 +30,15 @@ def test_segment_validate():
MessageSegment = Message.get_segment_class()
assert parse_obj_as(
MessageSegment, {"type": "text", "data": {"text": "text"}}
MessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == MessageSegment.text("text")
try:
with pytest.raises(ValidationError):
parse_obj_as(MessageSegment, "some str")
assert False
except ValidationError:
assert True
with pytest.raises(ValidationError):
parse_obj_as(MessageSegment, {"data": {}})
def test_segment():
@@ -129,11 +131,8 @@ def test_message_validate():
assert parse_obj_as(Message, Message([])) == Message([])
try:
with pytest.raises(ValidationError):
parse_obj_as(Message, Message_([]))
assert False
except ValidationError:
assert True
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")])
@@ -146,8 +145,5 @@ def test_message_validate():
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
try:
with pytest.raises(ValidationError):
parse_obj_as(Message, object())
assert False
except ValidationError:
assert True

View File

@@ -11,11 +11,11 @@ def test_template_basis():
def test_template_message():
Message = make_fake_message()
template = Message.template("{a:custom}{b:text}{c:image}")
template = Message.template("{a:custom}{b:text}{c:image}/{d}")
@template.add_format_spec
def custom(input: str) -> str:
return input + "-custom!"
return f"{input}-custom!"
try:
template.add_format_spec(custom)
@@ -24,12 +24,37 @@ def test_template_message():
else:
raise AssertionError("Should raise ValueError")
format_args = {"a": "custom", "b": "text", "c": "https://example.com/test"}
format_args = {
"a": "custom",
"b": "text",
"c": "https://example.com/test",
"d": 114,
}
formatted = template.format(**format_args)
assert template.format_map(format_args) == formatted
assert formatted.extract_plain_text() == "custom-custom!text"
assert str(formatted) == "custom-custom!text[fake:image]"
assert formatted.extract_plain_text() == "custom-custom!text/114"
assert str(formatted) == "custom-custom!text[fake:image]/114"
def test_rich_template_message():
Message = make_fake_message()
MS = Message.get_segment_class()
pic1, pic2, pic3 = (
MS.image("file:///pic1.jpg"),
MS.image("file:///pic2.jpg"),
MS.image("file:///pic3.jpg"),
)
template = Message.template("{}{}" + pic2 + "{}")
result = template.format(pic1, "[fake:image]", pic3)
assert result["image"] == Message([pic1, pic2, pic3])
assert str(result) == (
"[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]"
)
def test_message_injection():

View File

@@ -15,6 +15,7 @@ from nonebug import App
)
async def test_reverse_driver(app: App):
import nonebot
from nonebot.exception import WebSocketClosed
from nonebot.drivers import (
URL,
Request,
@@ -36,7 +37,21 @@ async def test_reverse_driver(app: App):
data = await ws.receive()
assert data == "ping"
await ws.send("pong")
await ws.close()
data = await ws.receive()
assert data == b"ping"
await ws.send(b"pong")
data = await ws.receive_text()
assert data == "ping"
await ws.send("pong")
data = await ws.receive_bytes()
assert data == b"ping"
await ws.send(b"pong")
with pytest.raises(WebSocketClosed):
await ws.receive()
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
driver.setup_http_server(http_setup)
@@ -53,3 +68,37 @@ async def test_reverse_driver(app: App):
async with client.websocket_connect("/ws_test") as ws:
await ws.send_text("ping")
assert await ws.receive_text() == "pong"
await ws.send_bytes(b"ping")
assert await ws.receive_bytes() == b"pong"
await ws.send_text("ping")
assert await ws.receive_text() == "pong"
await ws.send_bytes(b"ping")
assert await ws.receive_bytes() == b"pong"
await ws.close()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"nonebug_init, driver_type",
[
pytest.param(
{"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"},
"fastapi+aiohttp",
id="fastapi+aiohttp",
),
pytest.param(
{"driver": "~httpx:Driver+~websockets"},
"block_driver+httpx+websockets",
id="httpx+websockets",
),
],
indirect=["nonebug_init"],
)
async def test_combine_driver(app: App, driver_type: str):
import nonebot
driver = nonebot.get_driver()
assert driver.type == driver_type

View File

@@ -3,7 +3,7 @@ from nonebug import App
@pytest.mark.asyncio
async def test_weather(app: App):
async def test_weather(app: App, load_example):
from examples.weather import weather
from utils import make_fake_event, make_fake_message

View File

@@ -3,6 +3,7 @@ import os
import pytest
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
os.environ["CONFIG_OVERRIDE"] = "new"
@pytest.mark.asyncio
@@ -25,6 +26,7 @@ async def test_init(nonebug_init):
config = get_driver().config
assert config.config_from_env == {"test": "test"}
assert config.config_override == "new"
assert config.config_from_init == "init"
assert config.common_config == "common"
@@ -35,11 +37,8 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
from nonebot.drivers import ForwardDriver, ReverseDriver
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
try:
with pytest.raises(ValueError):
get_driver()
assert False, "Driver can only be got after initialization"
except ValueError:
assert True
nonebot.init(driver="nonebot.drivers.fastapi")
@@ -59,13 +58,10 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
nonebot.run("arg", kwarg="kwarg")
assert runned
try:
with pytest.raises(ValueError):
get_bot()
assert False
except ValueError:
assert True
monkeypatch.setattr(driver, "_clients", {"test": "test"})
monkeypatch.setattr(driver, "_bots", {"test": "test"})
assert get_bot() == "test"
assert get_bot("test") == "test"
assert get_bots() == {"test": "test"}

View File

@@ -104,6 +104,7 @@ async def test_permission_updater(app: App, load_plugin):
default_permission,
test_custom_updater,
test_permission_updater,
test_user_permission_updater,
)
event = make_fake_event(_session_id="test")()
@@ -119,6 +120,19 @@ async def test_permission_updater(app: App, load_plugin):
assert checker.users == ("test",)
assert checker.perm is default_permission
user_permission = list(test_user_permission_updater.permission.checkers)[0].call
assert isinstance(user_permission, User)
assert user_permission.perm is default_permission
async with app.test_api() as ctx:
bot = ctx.create_bot()
matcher = test_user_permission_updater()
new_perm = await matcher.update_permission(bot, event)
assert len(new_perm.checkers) == 1
checker = list(new_perm.checkers)[0].call
assert isinstance(checker, User)
assert checker.users == ("test",)
assert checker.perm is default_permission
assert test_custom_updater.permission is default_permission
async with app.test_api() as ctx:
bot = ctx.create_bot()
@@ -157,3 +171,41 @@ async def test_run(app: App):
await test_pause().run(bot, event, {})
assert len(matchers[0]) == 1
assert len(matchers[0][0].handlers) == 0
@pytest.mark.asyncio
async def test_expire(app: App, load_plugin):
from nonebot.matcher import matchers
from nonebot.message import _check_matcher
from plugins.matcher.matcher_expire import (
test_temp_matcher,
test_datetime_matcher,
test_timedelta_matcher,
)
event = make_fake_event(_type="test")()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_temp_matcher in matchers[test_temp_matcher.priority]
await _check_matcher(
test_temp_matcher.priority, test_temp_matcher, bot, event, {}
)
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
event = make_fake_event()()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
await _check_matcher(
test_datetime_matcher.priority, test_datetime_matcher, bot, event, {}
)
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
event = make_fake_event()()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
await _check_matcher(
test_timedelta_matcher.priority, test_timedelta_matcher, bot, event, {}
)
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]

View File

@@ -36,32 +36,103 @@ async def test_depend(app: App, load_plugin):
@pytest.mark.asyncio
async def test_bot(app: App, load_plugin):
from nonebot.params import BotParam
from plugins.param.param_bot import get_bot
from nonebot.exception import TypeMisMatch
from plugins.param.param_bot import (
FooBot,
get_bot,
not_bot,
sub_bot,
union_bot,
legacy_bot,
not_legacy_bot,
)
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
async with app.test_dependent(legacy_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(ValueError):
async with app.test_dependent(not_legacy_bot, allow_types=[BotParam]) as ctx:
...
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot(base=FooBot)
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(TypeMisMatch):
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot(base=FooBot)
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(ValueError):
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx:
...
@pytest.mark.asyncio
async def test_event(app: App, load_plugin):
from nonebot.exception import TypeMisMatch
from nonebot.params import EventParam, DependParam
from plugins.param.param_event import (
FooEvent,
event,
not_event,
sub_event,
event_type,
event_to_me,
union_event,
legacy_event,
event_message,
event_plain_text,
not_legacy_event,
)
fake_message = make_fake_message()("text")
fake_event = make_fake_event(_message=fake_message)()
fake_fooevent = make_fake_event(_base=FooEvent)()
async with app.test_dependent(event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
async with app.test_dependent(legacy_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
with pytest.raises(ValueError):
async with app.test_dependent(
not_legacy_event, allow_types=[EventParam]
) as ctx:
...
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_fooevent)
with pytest.raises(TypeMisMatch):
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_event)
with pytest.raises(ValueError):
async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx:
...
async with app.test_dependent(
event_type, allow_types=[EventParam, DependParam]
) as ctx:
@@ -99,6 +170,7 @@ async def test_state(app: App, load_plugin):
CMD_ARG_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
CMD_START_KEY,
REGEX_MATCHED,
)
from plugins.param.param_state import (
@@ -108,14 +180,22 @@ async def test_state(app: App, load_plugin):
command_arg,
raw_command,
regex_group,
legacy_state,
command_start,
regex_matched,
not_legacy_state,
shell_command_args,
shell_command_argv,
)
fake_message = make_fake_message()("text")
fake_state = {
PREFIX_KEY: {CMD_KEY: ("cmd",), RAW_CMD_KEY: "/cmd", CMD_ARG_KEY: fake_message},
PREFIX_KEY: {
CMD_KEY: ("cmd",),
RAW_CMD_KEY: "/cmd",
CMD_START_KEY: "/",
CMD_ARG_KEY: fake_message,
},
SHELL_ARGV: ["-h"],
SHELL_ARGS: {"help": True},
REGEX_MATCHED: "[cq:test,arg=value]",
@@ -127,6 +207,16 @@ async def test_state(app: App, load_plugin):
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state)
async with app.test_dependent(legacy_state, allow_types=[StateParam]) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state)
with pytest.raises(ValueError):
async with app.test_dependent(
not_legacy_state, allow_types=[StateParam]
) as ctx:
...
async with app.test_dependent(
command, allow_types=[StateParam, DependParam]
) as ctx:
@@ -145,6 +235,12 @@ async def test_state(app: App, load_plugin):
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[PREFIX_KEY][CMD_ARG_KEY])
async with app.test_dependent(
command_start, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])
async with app.test_dependent(
shell_command_argv, allow_types=[StateParam, DependParam]
) as ctx:

194
tests/test_permission.py Normal file
View File

@@ -0,0 +1,194 @@
from typing import Tuple, Optional
import pytest
from nonebug import App
from utils import make_fake_event
@pytest.mark.asyncio
async def test_permission(app: App):
from nonebot.permission import Permission
from nonebot.exception import SkippedException
async def falsy():
return False
async def truthy():
return True
async def skipped() -> bool:
raise SkippedException
def _is_eq(a: Permission, b: Permission) -> bool:
return {d.call for d in a.checkers} == {d.call for d in b.checkers}
assert _is_eq(Permission(truthy) | None, Permission(truthy))
assert _is_eq(Permission(truthy) | falsy, Permission(truthy, falsy))
assert _is_eq(Permission(truthy) | Permission(falsy), Permission(truthy, falsy))
assert _is_eq(None | Permission(truthy), Permission(truthy))
assert _is_eq(truthy | Permission(falsy), Permission(truthy, falsy))
event = make_fake_event()()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert await Permission(falsy)(bot, event) == False
assert await Permission(truthy)(bot, event) == True
assert await Permission(skipped)(bot, event) == False
assert await Permission(truthy, falsy)(bot, event) == True
assert await Permission(truthy, skipped)(bot, event) == True
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,expected",
[
("message", True),
("notice", False),
],
)
async def test_message(
app: App,
type: str,
expected: bool,
):
from nonebot.permission import MESSAGE, Message
dependent = list(MESSAGE.checkers)[0]
checker = dependent.call
assert isinstance(checker, Message)
event = make_fake_event(_type=type)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,expected",
[
("message", False),
("notice", True),
],
)
async def test_notice(
app: App,
type: str,
expected: bool,
):
from nonebot.permission import NOTICE, Notice
dependent = list(NOTICE.checkers)[0]
checker = dependent.call
assert isinstance(checker, Notice)
event = make_fake_event(_type=type)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,expected",
[
("message", False),
("request", True),
],
)
async def test_request(
app: App,
type: str,
expected: bool,
):
from nonebot.permission import REQUEST, Request
dependent = list(REQUEST.checkers)[0]
checker = dependent.call
assert isinstance(checker, Request)
event = make_fake_event(_type=type)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,expected",
[
("message", False),
("meta_event", True),
],
)
async def test_metaevent(
app: App,
type: str,
expected: bool,
):
from nonebot.permission import METAEVENT, MetaEvent
dependent = list(METAEVENT.checkers)[0]
checker = dependent.call
assert isinstance(checker, MetaEvent)
event = make_fake_event(_type=type)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,user_id,expected",
[
("message", "test", True),
("message", "foo", False),
("message", "faketest", True),
("message", None, False),
("notice", "test", True),
],
)
async def test_superuser(
app: App,
type: str,
user_id: str,
expected: bool,
):
from nonebot.permission import SUPERUSER, SuperUser
dependent = list(SUPERUSER.checkers)[0]
checker = dependent.call
assert isinstance(checker, SuperUser)
event = make_fake_event(_type=type, _user_id=user_id)()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert await dependent(bot=bot, event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"session_ids,session_id,expected",
[
(("user", "foo"), "user", True),
(("user", "foo"), "bar", False),
(("user", "foo"), None, False),
],
)
async def test_user(
app: App, session_ids: Tuple[str, ...], session_id: Optional[str], expected: bool
):
from nonebot.permission import USER, User
dependent = list(USER(*session_ids).checkers)[0]
checker = dependent.call
assert isinstance(checker, User)
event = make_fake_event(_session_id=session_id)()
async with app.test_api() as ctx:
bot = ctx.create_bot()
assert await dependent(bot=bot, event=event) == expected

View File

@@ -0,0 +1,39 @@
from typing import TYPE_CHECKING, Set
import pytest
from nonebug import App
if TYPE_CHECKING:
from nonebot.plugin import Plugin
@pytest.mark.asyncio
async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
import nonebot
# check simple plugin
plugin = nonebot.get_plugin("export")
assert plugin
assert plugin.module_name == "plugins.export"
# check sub plugin
plugin = nonebot.get_plugin("nested_subplugin")
assert plugin
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
# check get plugin by module name
plugin = nonebot.get_plugin_by_module_name("plugins.nested.utils")
assert plugin
assert plugin.module_name == "plugins.nested"
@pytest.mark.asyncio
async def test_get_available_plugin(app: App):
import nonebot
from nonebot.plugin import PluginManager, _managers
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
# check get available plugins
plugin_names = nonebot.get_available_plugin_names()
assert plugin_names == {"export", "require"}

View File

@@ -1,4 +1,6 @@
import sys
from pathlib import Path
from dataclasses import asdict
from typing import TYPE_CHECKING, Set
import pytest
@@ -9,25 +11,88 @@ if TYPE_CHECKING:
@pytest.mark.asyncio
async def test_load_plugin(load_plugin: Set["Plugin"]):
async def test_load_plugin(app: App):
import nonebot
loaded_plugins = set(
# check regular
assert nonebot.load_plugin("plugins.metadata")
# check path
assert nonebot.load_plugin(Path("plugins/export"))
# check not found
assert nonebot.load_plugin("some_plugin_not_exist") is None
@pytest.mark.asyncio
async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
import nonebot
from nonebot.plugin import PluginManager
loaded_plugins = {
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
)
}
assert loaded_plugins == load_plugin
plugin = nonebot.get_plugin("export")
assert plugin
assert plugin.module_name == "plugins.export"
# check simple plugin
assert "plugins.export" in sys.modules
try:
nonebot.load_plugin("plugins.export")
assert False
except RuntimeError:
assert True
# check sub plugin
plugin = nonebot.get_plugin("nested_subplugin")
assert plugin
assert "plugins.nested.plugins.nested_subplugin" in sys.modules
assert plugin.parent_plugin == nonebot.get_plugin("nested")
assert nonebot.load_plugin("some_plugin_not_exist") is None
# check load again
with pytest.raises(RuntimeError):
PluginManager(plugins=["plugins.export"]).load_all_plugins()
with pytest.raises(RuntimeError):
PluginManager(search_path=["plugins"]).load_all_plugins()
@pytest.mark.asyncio
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
import nonebot
parent_plugin = nonebot.get_plugin("nested")
sub_plugin = nonebot.get_plugin("nested_subplugin")
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
assert parent_plugin and sub_plugin and sub_plugin2
assert sub_plugin.parent_plugin is parent_plugin
assert sub_plugin2.parent_plugin is parent_plugin
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
@pytest.mark.asyncio
async def test_load_json(app: App):
import nonebot
nonebot.load_from_json("./plugins.json")
with pytest.raises(TypeError):
nonebot.load_from_json("./plugins.invalid.json")
@pytest.mark.asyncio
async def test_load_toml(app: App):
import nonebot
nonebot.load_from_toml("./plugins.toml")
with pytest.raises(ValueError):
nonebot.load_from_toml("./plugins.empty.toml")
with pytest.raises(TypeError):
nonebot.load_from_toml("./plugins.invalid.toml")
@pytest.mark.asyncio
async def test_bad_plugin(app: App):
import nonebot
nonebot.load_plugins("bad_plugins")
assert nonebot.get_plugin("bad_plugins") is None
@pytest.mark.asyncio
@@ -47,8 +112,7 @@ async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.asyncio
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot
from nonebot.plugin import _managers
from nonebot.plugin.manager import PluginManager
from nonebot.plugin import PluginManager, _managers
m = PluginManager(["plugins.export"])
_managers.append(m)
@@ -80,10 +144,23 @@ async def test_require_not_declared(app: App):
@pytest.mark.asyncio
async def test_require_not_found(app: App):
import nonebot
from nonebot.plugin import _managers
try:
with pytest.raises(RuntimeError):
nonebot.require("some_plugin_not_exist")
assert False
except RuntimeError:
assert True
@pytest.mark.asyncio
async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
import nonebot
from plugins.metadata import Config
plugin = nonebot.get_plugin("metadata")
assert plugin
assert plugin.metadata
assert asdict(plugin.metadata) == {
"name": "测试插件",
"description": "测试插件元信息",
"usage": "无法使用",
"config": Config,
"extra": {"author": "NoneBot"},
}

View File

@@ -0,0 +1,12 @@
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_load_plugin_name(app: App):
from nonebot.plugin import PluginManager
m = PluginManager(plugins=["plugins.export"])
module1 = m.load_plugin("export")
module2 = m.load_plugin("plugins.export")
assert module1 is module2

View File

@@ -0,0 +1,116 @@
from typing import Type, Optional
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_on(app: App, load_plugin):
import nonebot
import plugins.plugin.matchers as module
from nonebot.typing import T_RuleChecker
from nonebot.matcher import Matcher, matchers
from nonebot.rule import (
RegexRule,
IsTypeRule,
CommandRule,
EndswithRule,
KeywordsRule,
FullmatchRule,
StartswithRule,
ShellCommandRule,
)
from plugins.plugin.matchers import (
TestEvent,
rule,
state,
handler,
priority,
matcher_on,
permission,
expire_time,
matcher_on_type,
matcher_sub_cmd,
matcher_group_on,
matcher_on_regex,
matcher_on_notice,
matcher_on_command,
matcher_on_keyword,
matcher_on_message,
matcher_on_request,
matcher_on_endswith,
matcher_on_fullmatch,
matcher_on_metaevent,
matcher_group_on_type,
matcher_on_startswith,
matcher_sub_shell_cmd,
matcher_group_on_regex,
matcher_group_on_notice,
matcher_group_on_command,
matcher_group_on_keyword,
matcher_group_on_message,
matcher_group_on_request,
matcher_on_shell_command,
matcher_group_on_endswith,
matcher_group_on_fullmatch,
matcher_group_on_metaevent,
matcher_group_on_startswith,
matcher_group_on_shell_command,
)
plugin = nonebot.get_plugin("plugin")
def _check(
matcher: Type[Matcher],
pre_rule: Optional[T_RuleChecker],
has_permission: bool,
):
assert {dependent.call for dependent in matcher.rule.checkers} == (
{pre_rule, rule} if pre_rule else {rule}
)
if has_permission:
assert {dependent.call for dependent in matcher.permission.checkers} == {
permission
}
else:
assert not matcher.permission.checkers
assert [dependent.call for dependent in matcher.handlers] == [handler]
assert matcher.temp is True
assert matcher.expire_time == expire_time
assert matcher in matchers[priority]
assert matcher.block is True
assert matcher._default_state == state
assert matcher.plugin is plugin
assert matcher.module is module
assert matcher.plugin_name == "plugin"
assert matcher.module_name == "plugins.plugin.matchers"
_check(matcher_on, None, True)
_check(matcher_on_metaevent, None, False)
_check(matcher_on_message, None, True)
_check(matcher_on_notice, None, False)
_check(matcher_on_request, None, False)
_check(matcher_on_startswith, StartswithRule(("test",)), True)
_check(matcher_on_endswith, EndswithRule(("test",)), True)
_check(matcher_on_fullmatch, FullmatchRule(("test",)), True)
_check(matcher_on_keyword, KeywordsRule("test"), True)
_check(matcher_on_command, CommandRule([("test",)]), True)
_check(matcher_on_shell_command, ShellCommandRule([("test",)], None), True)
_check(matcher_on_regex, RegexRule("test"), True)
_check(matcher_on_type, IsTypeRule(TestEvent), True)
_check(matcher_sub_cmd, CommandRule([("test", "sub")]), True)
_check(matcher_sub_shell_cmd, ShellCommandRule([("test", "sub")], None), True)
_check(matcher_group_on, None, True)
_check(matcher_group_on_metaevent, None, False)
_check(matcher_group_on_message, None, True)
_check(matcher_group_on_notice, None, False)
_check(matcher_group_on_request, None, False)
_check(matcher_group_on_startswith, StartswithRule(("test",)), True)
_check(matcher_group_on_endswith, EndswithRule(("test",)), True)
_check(matcher_group_on_fullmatch, FullmatchRule(("test",)), True)
_check(matcher_group_on_keyword, KeywordsRule("test"), True)
_check(matcher_group_on_command, CommandRule([("test",)]), True)
_check(matcher_group_on_shell_command, ShellCommandRule([("test",)], None), True)
_check(matcher_group_on_regex, RegexRule("test"), True)
_check(matcher_group_on_type, IsTypeRule(TestEvent), True)

View File

@@ -1,4 +1,5 @@
from typing import Tuple, Union
import sys
from typing import Dict, Tuple, Union, Optional
import pytest
from nonebug import App
@@ -20,6 +21,16 @@ async def test_rule(app: App):
async def skipped() -> bool:
raise SkippedException
def _is_eq(a: Rule, b: Rule) -> bool:
return {d.call for d in a.checkers} == {d.call for d in b.checkers}
assert _is_eq(Rule(truthy) & None, Rule(truthy))
assert _is_eq(Rule(truthy) & falsy, Rule(truthy, falsy))
assert _is_eq(Rule(truthy) & Rule(falsy), Rule(truthy, falsy))
assert _is_eq(None & Rule(truthy), Rule(truthy))
assert _is_eq(truthy & Rule(falsy), Rule(truthy, falsy))
event = make_fake_event()()
async with app.test_api() as ctx:
@@ -41,6 +52,7 @@ async def test_rule(app: App):
("prefix", True, "message", "Prefix_", True),
("prefix", False, "message", "prefoo", False),
("prefix", False, "message", "fooprefix", False),
("prefix", False, "message", None, False),
(("prefix", "foo"), False, "message", "fooprefix", True),
("prefix", False, "notice", "foo", False),
],
@@ -50,7 +62,7 @@ async def test_startswith(
msg: Union[str, Tuple[str, ...]],
ignorecase: bool,
type: str,
text: str,
text: Optional[str],
expected: bool,
):
from nonebot.rule import StartswithRule, startswith
@@ -63,7 +75,7 @@ async def test_startswith(
assert checker.msg == (msg,) if isinstance(msg, str) else msg
assert checker.ignorecase == ignorecase
message = make_fake_message()(text)
message = text if text is None else make_fake_message()(text)
event = make_fake_event(_type=type, _message=message)()
assert await dependent(event=event) == expected
@@ -78,6 +90,7 @@ async def test_startswith(
("suffix", True, "message", "_Suffix", True),
("suffix", False, "message", "suffoo", False),
("suffix", False, "message", "suffixfoo", False),
("suffix", False, "message", None, False),
(("suffix", "foo"), False, "message", "suffixfoo", True),
("suffix", False, "notice", "foo", False),
],
@@ -87,7 +100,7 @@ async def test_endswith(
msg: Union[str, Tuple[str, ...]],
ignorecase: bool,
type: str,
text: str,
text: Optional[str],
expected: bool,
):
from nonebot.rule import EndswithRule, endswith
@@ -100,7 +113,45 @@ async def test_endswith(
assert checker.msg == (msg,) if isinstance(msg, str) else msg
assert checker.ignorecase == ignorecase
message = make_fake_message()(text)
message = text if text is None else make_fake_message()(text)
event = make_fake_event(_type=type, _message=message)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"msg,ignorecase,type,text,expected",
[
("fullmatch", False, "message", "fullmatch", True),
("fullmatch", False, "message", "Fullmatch", False),
("fullmatch", True, "message", "fullmatch", True),
("fullmatch", True, "message", "Fullmatch", True),
("fullmatch", False, "message", "fullfoo", False),
("fullmatch", False, "message", "_fullmatch_", False),
("fullmatch", False, "message", None, False),
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
("fullmatch", False, "notice", "foo", False),
],
)
async def test_fullmatch(
app: App,
msg: Union[str, Tuple[str, ...]],
ignorecase: bool,
type: str,
text: Optional[str],
expected: bool,
):
from nonebot.rule import FullmatchRule, fullmatch
test_fullmatch = fullmatch(msg, ignorecase)
dependent = list(test_fullmatch.checkers)[0]
checker = dependent.call
assert isinstance(checker, FullmatchRule)
assert checker.msg == ((msg,) if isinstance(msg, str) else msg)
assert checker.ignorecase == ignorecase
message = text if text is None else make_fake_message()(text)
event = make_fake_event(_type=type, _message=message)()
assert await dependent(event=event) == expected
@@ -111,6 +162,7 @@ async def test_endswith(
[
(("key",), "message", "_key_", True),
(("key", "foo"), "message", "_foo_", True),
(("key",), "message", None, False),
(("key",), "notice", "foo", False),
],
)
@@ -118,7 +170,7 @@ async def test_keyword(
app: App,
kws: Tuple[str, ...],
type: str,
text: str,
text: Optional[str],
expected: bool,
):
from nonebot.rule import KeywordsRule, keyword
@@ -130,7 +182,7 @@ async def test_keyword(
assert isinstance(checker, KeywordsRule)
assert checker.keywords == kws
message = make_fake_message()(text)
message = text if text is None else make_fake_message()(text)
event = make_fake_event(_type=type, _message=message)()
assert await dependent(event=event) == expected
@@ -148,16 +200,157 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
checker = dependent.call
assert isinstance(checker, CommandRule)
assert checker.cmds == list(cmds)
assert checker.cmds == cmds
for cmd in cmds:
state = {PREFIX_KEY: {CMD_KEY: cmd}}
assert await dependent(state=state)
# TODO: shell command
@pytest.mark.asyncio
async def test_shell_command(app: App):
from nonebot.typing import T_State
from nonebot.exception import ParserExit
from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY
from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command
# TODO: regex
state: T_State
CMD = ("test",)
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
test_not_cmd = shell_command(CMD)
dependent = list(test_not_cmd.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message()
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: ("not",), CMD_ARG_KEY: message}}
assert not await dependent(event=event, state=state)
test_no_parser = shell_command(CMD)
dependent = list(test_no_parser.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message()
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == []
assert SHELL_ARGS not in state
parser = ArgumentParser("test")
parser.add_argument("-a", required=True)
test_simple_parser = shell_command(CMD, parser=parser)
dependent = list(test_simple_parser.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message("-a 1")
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == ["-a", "1"]
assert state[SHELL_ARGS] == Namespace(a="1")
test_parser_help = shell_command(CMD, parser=parser)
dependent = list(test_parser_help.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message("-h")
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == ["-h"]
assert isinstance(state[SHELL_ARGS], ParserExit)
assert state[SHELL_ARGS].status == 0
assert state[SHELL_ARGS].message == parser.format_help()
test_parser_error = shell_command(CMD, parser=parser)
dependent = list(test_parser_error.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message()
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == []
assert isinstance(state[SHELL_ARGS], ParserExit)
assert state[SHELL_ARGS].status != 0
test_message_parser = shell_command(CMD, parser=parser)
dependent = list(test_message_parser.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = MessageSegment.text("-a") + MessageSegment.image("test")
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == ["-a", MessageSegment.image("test")]
assert state[SHELL_ARGS] == Namespace(a=MessageSegment.image("test"))
if sys.version_info >= (3, 9):
parser = ArgumentParser("test", exit_on_error=False)
parser.add_argument("-a", required=True)
test_not_exit = shell_command(CMD, parser=parser)
dependent = list(test_not_exit.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = Message()
event = make_fake_event(_message=message)()
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
assert await dependent(event=event, state=state)
assert state[SHELL_ARGV] == []
assert isinstance(state[SHELL_ARGS], ParserExit)
assert state[SHELL_ARGS].status != 0
@pytest.mark.asyncio
@pytest.mark.parametrize(
"pattern,type,text,expected,matched,group,dict",
[
(
r"(?P<key>key\d)",
"message",
"_key1_",
True,
"key1",
("key1",),
{"key": "key1"},
),
(r"foo", "message", None, False, None, None, None),
(r"foo", "notice", "foo", False, None, None, None),
],
)
async def test_regex(
app: App,
pattern: str,
type: str,
text: Optional[str],
expected: bool,
matched: Optional[str],
group: Optional[Tuple[str, ...]],
dict: Optional[Dict[str, str]],
):
from nonebot.typing import T_State
from nonebot.rule import RegexRule, regex
from nonebot.consts import REGEX_DICT, REGEX_GROUP, REGEX_MATCHED
test_regex = regex(pattern)
dependent = list(test_regex.checkers)[0]
checker = dependent.call
assert isinstance(checker, RegexRule)
assert checker.regex == pattern
message = text if text is None else make_fake_message()(text)
event = make_fake_event(_type=type, _message=message)()
state = {}
assert await dependent(event=event, state=state) == expected
assert state.get(REGEX_MATCHED) == matched
assert state.get(REGEX_GROUP) == group
assert state.get(REGEX_DICT) == dict
@pytest.mark.asyncio
@@ -165,11 +358,32 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
async def test_to_me(app: App, expected: bool):
from nonebot.rule import ToMeRule, to_me
test_keyword = to_me()
dependent = list(test_keyword.checkers)[0]
test_to_me = to_me()
dependent = list(test_to_me.checkers)[0]
checker = dependent.call
assert isinstance(checker, ToMeRule)
event = make_fake_event(_to_me=expected)()
assert await dependent(event=event) == expected
@pytest.mark.asyncio
async def test_is_type(app: App):
from nonebot.rule import IsTypeRule, is_type
Event1 = make_fake_event()
Event2 = make_fake_event()
Event3 = make_fake_event()
test_type = is_type(Event1, Event2)
dependent = list(test_type.checkers)[0]
checker = dependent.call
assert isinstance(checker, IsTypeRule)
event = Event1()
assert await dependent(event=event)
event = Event3()
assert not await dependent(event=event)

19
tests/test_utils.py Normal file
View File

@@ -0,0 +1,19 @@
import json
from utils import make_fake_message
def test_dataclass_encoder():
from nonebot.utils import DataclassEncoder
simple = json.dumps("123", cls=DataclassEncoder)
assert simple == '"123"'
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
ms = MessageSegment.nested(Message(MessageSegment.text("text")))
s = json.dumps(ms, cls=DataclassEncoder)
assert (
s
== '{"type": "node", "data": {"content": [{"type": "text", "data": {"text": "text"}}]}}'
)

View File

@@ -32,6 +32,10 @@ def make_fake_message():
def image(url: str):
return FakeMessageSegment("image", {"url": url})
@staticmethod
def nested(content: "FakeMessage"):
return FakeMessageSegment("node", {"content": content})
def is_text(self) -> bool:
return self.type == "text"
@@ -57,10 +61,11 @@ def make_fake_message():
def make_fake_event(
_base: Optional[Type["Event"]] = None,
_type: str = "message",
_name: str = "test",
_description: str = "test",
_user_id: str = "test",
_user_id: Optional[str] = "test",
_session_id: Optional[str] = "test",
_message: Optional["Message"] = None,
_to_me: bool = True,
@@ -68,7 +73,7 @@ def make_fake_event(
) -> Type["Event"]:
from nonebot.adapters import Event
_Fake = create_model("_Fake", __base__=Event, **fields)
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
class FakeEvent(_Fake):
def get_type(self) -> str:
@@ -81,7 +86,9 @@ def make_fake_event(
return _description
def get_user_id(self) -> str:
if _user_id is not None:
return _user_id
raise NotImplementedError
def get_session_id(self) -> str:
if _session_id is not None:

View File

@@ -8,7 +8,7 @@ slug: /
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
需要注意的是NoneBot2 仅支持 **Python 3.7.3 以上版本**
需要注意的是NoneBot2 仅支持 **Python 3.8 以上版本**
## 特色

View File

@@ -1,5 +1,7 @@
---
id: index
sidebar_position: 0
description: 深入了解 NoneBot2 运行机制
slug: /advanced/
options:

View File

@@ -1,3 +1,4 @@
{
"label": "依赖注入"
"label": "依赖注入",
"position": 5
}

View File

@@ -1,10 +1,10 @@
---
sidebar_position: 3
sidebar_position: 2
description: 重载事件处理函数
options:
menu:
weight: 62
weight: 61
category: advanced
---

Some files were not shown because too many files have changed in this diff Show More