Compare commits

...

454 Commits

Author SHA1 Message Date
noneflow[bot]
8dcfe92f13 🔖 Release 2.0.0 2023-06-01 06:26:07 +00:00
Ju4tCode
f3d5c1f226 🔖 Release: v2.0.0 (#2070) 2023-06-01 14:18:16 +08:00
noneflow[bot]
8af21f6e76 📝 Update changelog 2023-05-31 14:35:34 +00:00
Tarrailt
9bf3dc4274 📝 Docs: 添加 Alconna 响应器介绍 (#2069)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-31 22:34:10 +08:00
noneflow[bot]
40d2f975cb 📝 Update changelog 2023-05-30 08:18:36 +00:00
Ju4tCode
784ba287aa 📝 Docs: 更新 README 适配器链接 (#2068) 2023-05-30 16:17:24 +08:00
noneflow[bot]
3b9cf6cc51 📝 Update changelog 2023-05-30 07:21:42 +00:00
Ju4tCode
f52abc8314 Feature: 优化事件分发方法 (#2067) 2023-05-30 15:20:31 +08:00
noneflow[bot]
3199fc454a 📝 Update changelog 2023-05-28 14:00:27 +00:00
DiaoDaiaChan
738f8cae3b 🍻 publish plugin stablediffusion绘画插件 (#2065) 2023-05-28 13:59:10 +00:00
noneflow[bot]
9406c117a6 📝 Update changelog 2023-05-28 13:20:29 +00:00
Ikaros-521
7b0e62c128 🍻 publish plugin 随机抽取自定义内容 (#2063) 2023-05-28 13:19:05 +00:00
noneflow[bot]
5d0d91b87b 📝 Update changelog 2023-05-28 11:34:55 +00:00
ssttkkl
ed687d8ff6 🍻 publish plugin NAGA公交车 (#2061) 2023-05-28 11:33:36 +00:00
noneflow[bot]
cc214c320d 📝 Update changelog 2023-05-24 16:10:20 +00:00
Xie-Tiao
0897f3b0f7 🍻 publish plugin 本子标题关键词提取 (#2055) 2023-05-24 16:09:01 +00:00
noneflow[bot]
7986c45b3f 📝 Update changelog 2023-05-24 09:54:54 +00:00
Akirami
8ea0241aa2 ✏️ Plugin: Hello World 添加 tag (#2056) 2023-05-24 17:53:28 +08:00
noneflow[bot]
fabc2faa4c 📝 Update changelog 2023-05-24 09:39:52 +00:00
Akirami
3216479530 ✏️ 修改 nonebot-plugin-logpile 的名称和描述 (#2057) 2023-05-24 17:38:06 +08:00
noneflow[bot]
6c66a54223 📝 Update changelog 2023-05-24 03:01:50 +00:00
initialencounter
e760290beb 🍻 publish plugin puzzle (#2053) 2023-05-24 03:00:26 +00:00
noneflow[bot]
3beefdff72 📝 Update changelog 2023-05-24 02:53:49 +00:00
Special-Week
104f610ea7 🍻 publish plugin homo_mathematician (#2051) 2023-05-24 02:51:54 +00:00
noneflow[bot]
2b337e1310 📝 Update changelog 2023-05-23 16:06:23 +00:00
initialencounter
b78455f910 🍻 publish plugin cuber (#2045) 2023-05-23 16:05:13 +00:00
noneflow[bot]
c3d6e20120 📝 Update changelog 2023-05-23 15:56:26 +00:00
synodriver
b32af0f6ba 🍻 publish plugin nonebot-plugin-lua (#2047) 2023-05-23 15:55:09 +00:00
noneflow[bot]
3469b0dbb7 📝 Update changelog 2023-05-23 02:26:54 +00:00
ElainaFanBoy
0fd7396665 🍻 publish plugin Github仓库卡片 (#2041) 2023-05-23 02:25:40 +00:00
noneflow[bot]
c6f41e1975 📝 Update changelog 2023-05-21 16:01:58 +00:00
Ju4tCode
2cfc20c143 🐛 fix require new plugin context error (#2040) 2023-05-22 00:00:50 +08:00
noneflow[bot]
99197f30f6 📝 Update changelog 2023-05-21 08:03:01 +00:00
Ju4tCode
aa48299d5d improve dependency injection params (#2034) 2023-05-21 16:01:55 +08:00
noneflow[bot]
dd80191761 📝 Update changelog 2023-05-20 09:32:41 +00:00
canxin
34c1c33996 ✏️ Plugin: 移除 nonebot_paddle_ocrnonebot_poe_chat (#2039) 2023-05-20 17:31:20 +08:00
noneflow[bot]
2dcbce9cd7 📝 Update changelog 2023-05-20 06:02:19 +00:00
MingxuanGame
c4fbd1cac3 🔥 remove plugin nonebot-plugin-rtfm (#2037) 2023-05-20 14:01:16 +08:00
noneflow[bot]
575e3fb920 📝 Update changelog 2023-05-20 03:12:19 +00:00
Calanosay
50fd4acccb 🍻 publish plugin 股票看盘助手 (#2031) 2023-05-20 03:11:15 +00:00
noneflow[bot]
f9e214de93 📝 Update changelog 2023-05-19 09:42:59 +00:00
xi-yue-233
f28d354875 🍻 publish plugin 便携插件安装器 (#2026) 2023-05-19 09:41:47 +00:00
noneflow[bot]
648b838a75 📝 Update changelog 2023-05-19 09:10:44 +00:00
worldmozara
157fe31051 ✏️ Plugin: 移除 extrautils 工具拓展插件(暂停维护) (#2033) 2023-05-19 17:09:33 +08:00
noneflow[bot]
170fb94896 📝 Update changelog 2023-05-18 08:01:35 +00:00
Ju4tCode
9616b4c0ca Feature: 添加插件元数据字段 type homepage supported_adapters (#2012) 2023-05-18 16:00:10 +08:00
noneflow[bot]
0c4a040394 📝 Update changelog 2023-05-16 14:55:41 +00:00
MeetWq
8592e77e15 🍻 publish plugin 会话 id (#2024) 2023-05-16 14:54:33 +00:00
noneflow[bot]
fc8850496e 📝 Update changelog 2023-05-16 08:44:08 +00:00
evan-gyy
227afbfd8d 🍻 publish plugin SD绘画插件 (#2022) 2023-05-16 08:42:50 +00:00
noneflow[bot]
4672af12fe 📝 Update changelog 2023-05-16 08:38:57 +00:00
xi-yue-233
079996d936 🍻 publish plugin 《女神异闻录5》预告信生成器 (#2016) 2023-05-16 08:37:36 +00:00
noneflow[bot]
bd9b05b990 📝 Update changelog 2023-05-15 07:01:11 +00:00
chaichaisi
f2f3f7ab8e 🍻 publish plugin 小小的WEBAPI调用插件 (#2019) 2023-05-15 06:59:55 +00:00
noneflow[bot]
22222e79b6 📝 Update changelog 2023-05-15 04:42:14 +00:00
lgc2333
55164e8ece 🍻 publish plugin MultiNCM (#2017) 2023-05-15 04:41:11 +00:00
noneflow[bot]
336822ff5c 📝 Update changelog 2023-05-14 14:44:54 +00:00
zhulinyv
82e8417b9a 🍻 publish plugin 签到 (#2013) 2023-05-14 14:43:48 +00:00
noneflow[bot]
9c0ecb441f 📝 Update changelog 2023-05-13 10:10:24 +00:00
mute23-code
0c0fabcb89 🍻 publish plugin 链接解析 (#1959) 2023-05-13 10:09:14 +00:00
noneflow[bot]
202f437aea 📝 Update changelog 2023-05-13 07:33:31 +00:00
Ju4tCode
a8b06aa7c7 publish to store using issue form (#2010) 2023-05-13 15:32:28 +08:00
noneflow[bot]
a5e634319a 📝 Update changelog 2023-05-13 03:27:01 +00:00
bingqiu456
6d1262f402 🍻 publish bot 狐尾 (#2007) 2023-05-13 03:25:46 +00:00
noneflow[bot]
a5fd182bd0 📝 Update changelog 2023-05-13 02:31:25 +00:00
NCBM
771cf8bdcf 🍻 publish plugin 信鸽巴夫 (#2006) 2023-05-13 02:29:55 +00:00
noneflow[bot]
56304aea8d 📝 Update changelog 2023-05-12 15:22:56 +00:00
RF-Tar-Railt
c6e69ddc17 🍻 publish plugin 明日方舟抽卡模拟 (#2004) 2023-05-12 15:21:31 +00:00
noneflow[bot]
ae55ec3e1b 📝 Update changelog 2023-05-11 14:56:56 +00:00
Well2333
f72243304f 🍻 publish plugin 雷神工业 (#2002) 2023-05-11 14:55:42 +00:00
noneflow[bot]
5425180aec 📝 Update changelog 2023-05-10 10:34:12 +00:00
A-kirami
a496db4ddf 🍻 publish plugin nonebot-plugin-logpile (#1998) 2023-05-10 10:33:02 +00:00
noneflow[bot]
26bad8eb4b 📝 Update changelog 2023-05-10 10:10:30 +00:00
canxin121
f526080611 🍻 publish plugin Spark-GPT (#1996) 2023-05-10 10:09:04 +00:00
noneflow[bot]
6c269825c9 📝 Update changelog 2023-05-09 03:41:33 +00:00
AzideCupric
17959b7056 🍻 publish plugin 企鹅物流统计数据查询 (#1994) 2023-05-09 03:40:32 +00:00
noneflow[bot]
17a8ed379a 📝 Update changelog 2023-05-07 12:33:29 +00:00
863109569
163e5001d3 🍻 publish bot ay机器人 (#1992) 2023-05-07 12:32:26 +00:00
noneflow[bot]
5d27646ef9 📝 Update changelog 2023-05-07 03:21:34 +00:00
lgc2333
38be147e8a 🍻 publish plugin CallAPI (#1989) 2023-05-07 03:20:30 +00:00
noneflow[bot]
93829aeb80 📝 Update changelog 2023-05-07 03:11:18 +00:00
ZM25XC
9098dbae9a 🍻 publish plugin 群聊人数锁定 (#1987) 2023-05-07 03:10:13 +00:00
noneflow[bot]
9edc51c2a4 📝 Update changelog 2023-05-07 03:06:01 +00:00
roiiiu
f1aa2b1cb2 🍻 publish plugin CSGO开箱模拟器 (#1985) 2023-05-07 03:04:51 +00:00
noneflow[bot]
bbb2cb3a2c 📝 Update changelog 2023-05-06 17:04:48 +00:00
Lptr-byte
7a0a32398b 📝 Docs: 修复获取事件信息文档代码范例中的高亮行 (#1983) 2023-05-07 01:03:43 +08:00
noneflow[bot]
272ed8e85c 📝 Update changelog 2023-05-06 16:57:35 +00:00
Lptr-byte
e308d4cfac Docs: 修复事件处理函数文档代码范例中缺失的 import (#1982) 2023-05-07 00:56:22 +08:00
noneflow[bot]
0162360cfe 📝 Update changelog 2023-05-06 16:14:46 +00:00
Lptr-byte
4ba4c0bebc 📝 Docs: 修复获取事件信息文档代码范例中缺失的 import (#1980)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-05-07 00:13:44 +08:00
noneflow[bot]
4b0b1e69a4 📝 Update changelog 2023-05-05 13:05:45 +00:00
mobyw
22c0f81054 🍻 publish bot March7th (#1977) 2023-05-05 13:04:32 +00:00
noneflow[bot]
2494e615fd 📝 Update changelog 2023-05-05 11:34:56 +00:00
Special-Week
ab637d217b 🍻 publish plugin wordle_help (#1973) 2023-05-05 11:33:34 +00:00
noneflow[bot]
9d8f16f940 📝 Update changelog 2023-05-04 06:26:26 +00:00
Ju4tCode
dc2c5e3c80 🐛 fix command whitespace if no arg (#1975) 2023-05-04 14:25:09 +08:00
noneflow[bot]
6cfdbbe597 📝 Update changelog 2023-05-03 07:52:31 +00:00
17TheWord
487867a967 ✏️ Adapter: 更新 Minecraft 适配器 (#1972)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-03 15:51:21 +08:00
noneflow[bot]
f3a692d294 📝 Update changelog 2023-05-03 07:39:16 +00:00
synodriver
72b798f7ae 🐛 Fix: run_sync 上下文 (#1968) 2023-05-03 15:37:53 +08:00
pre-commit-ci[bot]
50237fb778 ⬆️ auto update by pre-commit hooks (#1971)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-02 14:11:59 +08:00
noneflow[bot]
fa5b8853af 📝 Update changelog 2023-05-02 04:51:39 +00:00
nicklly
0cd2282640 🍻 publish plugin 星穹铁道活动日历 (#1969) 2023-05-02 04:50:35 +00:00
noneflow[bot]
c2cea75bb7 📝 Update changelog 2023-05-02 04:13:18 +00:00
X-Skirt-X
b832ae742b 🍻 publish plugin 水印大师 (#1962) 2023-05-02 04:12:07 +00:00
noneflow[bot]
e98d28f3b4 📝 Update changelog 2023-04-30 14:28:36 +00:00
Akirami
b2e26cd6bd ✏️ Docs: 更正 issue 表单部分内容 (#1961)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-04-30 22:27:32 +08:00
noneflow[bot]
cfe182f452 📝 Update changelog 2023-04-29 04:15:20 +00:00
maoxig
729a894a04 🍻 publish plugin 图片/漫画翻译 (#1954) 2023-04-29 04:14:09 +00:00
noneflow[bot]
7231d493d0 📝 Update changelog 2023-04-29 03:29:31 +00:00
youlanan
abf1d52168 🍻 publish plugin 为美好群聊献上爆炎 (#1952) 2023-04-29 03:28:28 +00:00
noneflow[bot]
a5618f163f 📝 Update changelog 2023-04-29 00:45:55 +00:00
djkcyl
7d6c512f27 🍻 publish plugin 公共画板插件 (#1956) 2023-04-29 00:44:41 +00:00
noneflow[bot]
f00c0ae71c 📝 Update changelog 2023-04-27 14:00:11 +00:00
Ju4tCode
93b79ddcb3 Feature: 支持 re.Match 依赖注入 (#1950) 2023-04-27 21:58:56 +08:00
Ju4tCode
6691f6ef70 support exit none driver (#1951) 2023-04-27 17:26:59 +08:00
noneflow[bot]
6173836bdb 📝 Update changelog 2023-04-24 08:15:02 +00:00
student_2333
a2a88c1414 ✏️ Plugin: 更新 AutoReply 插件描述 (#1949) 2023-04-24 16:13:57 +08:00
noneflow[bot]
6114867e34 📝 Update changelog 2023-04-24 06:50:20 +00:00
Yincmewy
3c5cd6046d 🍻 publish plugin 运行代码 (#1941) 2023-04-24 06:49:10 +00:00
noneflow[bot]
7dc3702db5 📝 Update changelog 2023-04-24 02:46:05 +00:00
17TheWord
791f75c13e ✏️ Plugin: 移除 MC_QQ_MCRcon (#1948) 2023-04-24 10:44:53 +08:00
noneflow[bot]
4cfc8fcb44 📝 Update changelog 2023-04-24 02:36:03 +00:00
campanulamediuml
57fc04e4aa 🍻 publish plugin brainfuck (#1943) 2023-04-24 02:34:51 +00:00
noneflow[bot]
1e74c4eacf 📝 Update changelog 2023-04-24 02:34:33 +00:00
Well404
e55052ecfd 📝 Docs: 新增插件跨平台指南 (#1938)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-04-24 10:33:23 +08:00
noneflow[bot]
dc0aea9e3e 📝 Update changelog 2023-04-23 12:31:49 +00:00
NCBM
295b55da44 🍻 publish plugin Mixin (#1946) 2023-04-23 12:30:45 +00:00
noneflow[bot]
4b9ae5fd68 📝 Update changelog 2023-04-23 07:08:13 +00:00
Ju4tCode
cc8b6fa7a2 ✏️ enable blank issues (#1945) 2023-04-23 15:06:52 +08:00
noneflow[bot]
f28de96ea9 📝 Update changelog 2023-04-23 03:59:29 +00:00
Akirami
5e225b2898 📝 Docs: 使用 issue 表单替换 issue 模板 (#1928)
Co-authored-by: StarHeart <starheart233@gmail.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-04-23 11:58:25 +08:00
noneflow[bot]
392502dd68 📝 Update changelog 2023-04-22 07:35:22 +00:00
XZhouQD
ba329e5ef2 🍻 publish plugin AppInsights日志监控 (#1939) 2023-04-22 07:34:17 +00:00
noneflow[bot]
68b64a6004 📝 Update changelog 2023-04-22 03:18:34 +00:00
student_2333
0f694aa157 ✏️ Plugin: 更新 lgc2333 插件仓库地址 (#1935) 2023-04-22 11:17:24 +08:00
noneflow[bot]
0d7c399094 📝 Update changelog 2023-04-22 01:56:49 +00:00
canxin121
2f6685ab45 🍻 publish plugin nonebot_poe_chat (#1936) 2023-04-22 01:55:36 +00:00
noneflow[bot]
2061887276 📝 Update changelog 2023-04-20 02:05:46 +00:00
forchannot
2f40024edb 🍻 publish plugin 更改BOT群名片 (#1933) 2023-04-20 02:04:55 +00:00
noneflow[bot]
9799809ebd 📝 Update changelog 2023-04-18 10:57:48 +00:00
This-is-XiaoDeng
2a08bc5a14 🍻 publish bot XDbot2 (#1931) 2023-04-18 10:57:00 +00:00
noneflow[bot]
ff1ace7a04 📝 Update changelog 2023-04-16 10:47:18 +00:00
Well404
96f0daf535 📝 Docs: 修正教程中部分 import 缺失的问题 (#1927)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-04-16 18:46:29 +08:00
noneflow[bot]
1b2f560ad7 📝 Update changelog 2023-04-15 01:54:44 +00:00
lgc2333
1c033d3a53 🍻 publish plugin Akinator (#1924) 2023-04-15 01:53:52 +00:00
noneflow[bot]
99b26fccb0 📝 Update changelog 2023-04-14 08:27:59 +00:00
Ju4tCode
565aba61dc 🐛 Fix: shell command 包含富文本时报错信息出错 (#1923) 2023-04-14 16:26:54 +08:00
noneflow[bot]
21eb289411 📝 Update changelog 2023-04-13 16:01:18 +00:00
Agnes4m
441e772f48 🍻 publish plugin Bilifan (#1920) 2023-04-13 16:00:22 +00:00
noneflow[bot]
f360e439e9 📝 Update changelog 2023-04-13 14:10:16 +00:00
mas-alone
98b3affe91 🍻 publish plugin osu!入群审批 (#1918) 2023-04-13 14:09:19 +00:00
noneflow[bot]
04be5cb8e8 📝 Update changelog 2023-04-11 17:00:26 +00:00
nikissXI
7f925536b5 🍻 publish plugin 与ChatGpt聊天 (#1916) 2023-04-11 16:59:39 +00:00
noneflow[bot]
73dfcc53e1 📝 Update changelog 2023-04-11 16:43:02 +00:00
aaron-lii
3e543977c9 🍻 publish plugin TataruBot2 (#1914) 2023-04-11 16:42:18 +00:00
noneflow[bot]
97829aa122 📝 Update changelog 2023-04-10 16:38:02 +00:00
IllusiveBull
e42dd109f9 🍻 publish plugin 宝可梦融合 (#1911) 2023-04-10 16:37:16 +00:00
noneflow[bot]
98a6dfc514 📝 Update changelog 2023-04-10 16:17:08 +00:00
lgc2333
d6fd1b8614 🍻 publish plugin FuckYou (#1909) 2023-04-10 16:16:20 +00:00
noneflow[bot]
dd57aabfdc 📝 Update changelog 2023-04-10 08:02:28 +00:00
A60
44c8b4c29d ✏️ Plugin: 更新多功能哔哩哔哩解析工具 (#1913) 2023-04-10 16:01:38 +08:00
noneflow[bot]
1bc3a8eb02 📝 Update changelog 2023-04-09 15:03:56 +00:00
thx114
7371d3e7bb 🍻 publish plugin SDGPT (#1907) 2023-04-09 15:02:56 +00:00
noneflow[bot]
139eeac6ce 📝 Update changelog 2023-04-09 07:27:13 +00:00
Zeta-qixi
d33d2653cf 🍻 publish plugin nonebot clock 群闹钟 (#1861) 2023-04-09 07:26:28 +00:00
noneflow[bot]
ba3fc6abc4 📝 Update changelog 2023-04-08 14:30:44 +00:00
uy/sun
1d60714054 👷 跳过 PR 仓库为 fork 的情况 (#1905) 2023-04-08 22:29:58 +08:00
noneflow[bot]
020705bd4b 📝 Update changelog 2023-04-08 03:29:28 +00:00
Wuyi无疑
1495b34e39 ✏️ Plugin: 移除旧版本的 GenshinUID (#1904) 2023-04-08 11:28:42 +08:00
noneflow[bot]
e0d11226db 📝 Update changelog 2023-04-08 03:16:58 +00:00
zangxx66
8749bc9dc5 🍻 publish plugin B站直播间路灯 (#1900) 2023-04-08 03:16:08 +00:00
noneflow[bot]
716a047aba 📝 Update changelog 2023-04-08 01:04:49 +00:00
KimigaiiWuyi
ed6d436a50 🍻 publish plugin GenshinUID (#1902) 2023-04-08 01:03:55 +00:00
noneflow[bot]
028b51facf 📝 Update changelog 2023-04-07 16:08:37 +00:00
uy/sun
8f28124237 👷 CI: 使用最新的 NoneFlow (#1899) 2023-04-08 00:07:39 +08:00
noneflow[bot]
f3d7a30c66 📝 Update changelog 2023-04-06 09:53:35 +00:00
noneflow[bot]
8f3e9f87cb 🍻 publish plugin 多功能哔哩哔哩解析工具 (#1897)
Co-authored-by: djkcyl <djkcyl@users.noreply.github.com>
2023-04-06 17:52:24 +08:00
noneflow[bot]
36e4c02699 📝 Update changelog 2023-04-04 13:43:12 +00:00
Ju4tCode
1817102a7c Feature: 为消息类添加 has join include exclude 方法 (#1895) 2023-04-04 21:42:01 +08:00
pre-commit-ci[bot]
20820e72ad ⬆️ auto update by pre-commit hooks (#1896)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-04 17:33:00 +08:00
noneflow[bot]
fb4d957025 📝 Update changelog 2023-04-04 03:23:10 +00:00
he0119
fe5635db62 🍻 publish bot CoolQBot (#1893) 2023-04-04 11:22:06 +08:00
noneflow[bot]
30a8230eea 📝 Update changelog 2023-04-04 02:27:57 +00:00
Ju4tCode
908622cf61 👷 use noneflow app (#1892) 2023-04-04 09:48:31 +08:00
github-actions[bot]
8e5ec5c4e7 📝 Update changelog 2023-04-03 17:34:29 +00:00
nek0us
ca071bfc48 🍻 publish plugin Steam游戏状态播报 (#1886) 2023-04-04 01:33:15 +08:00
github-actions[bot]
4e22252c3e 📝 Update changelog 2023-04-03 17:26:30 +00:00
Alpaca4610
c0eee74968 🍻 publish plugin AI生成PPT (#1883) 2023-04-04 01:25:21 +08:00
github-actions[bot]
1e054a4370 📝 Update changelog 2023-04-03 17:16:39 +00:00
canxin121
76ccd241fc 🍻 publish plugin nonebot_paddle_ocr (#1881) 2023-04-04 01:15:30 +08:00
github-actions[bot]
e984b64fe3 📝 Update changelog 2023-04-03 17:01:58 +00:00
canxin121
3256cf7fce 🍻 publish plugin nonebot_api_paddle (#1879) 2023-04-04 01:00:55 +08:00
github-actions[bot]
d9eeb690ac 📝 Update changelog 2023-04-03 13:59:17 +00:00
Ju4tCode
b66f4436bf 📝 add walle-q to readme (#1891) 2023-04-03 21:57:56 +08:00
github-actions[bot]
c11bc7b78f 📝 Update changelog 2023-04-03 13:33:23 +00:00
Ju4tCode
3bbb48dd25 📝 update deploy docs (#1890) 2023-04-03 21:32:12 +08:00
github-actions[bot]
73b92be1e4 📝 Update changelog 2023-04-03 13:27:57 +00:00
abrahum
e977d79ebd 🍻 publish adapter Walle-Q (#1888) 2023-04-03 21:26:50 +08:00
github-actions[bot]
d02896065e 📝 Update changelog 2023-04-02 07:14:23 +00:00
mas-alone
f468aa992d 🍻 publish plugin 来份睡眠套餐 (#1875) 2023-04-02 15:13:21 +08:00
github-actions[bot]
3bfbbcf111 📝 Update changelog 2023-04-02 06:47:44 +00:00
glamorgan9826
e2e8b0a8cd 🍻 publish plugin 今日老婆 (#1873) 2023-04-02 14:46:45 +08:00
github-actions[bot]
c8c5f17fd1 📝 Update changelog 2023-04-01 11:48:37 +00:00
Ju4tCode
7f6fc56bd8 👷 unlock poetry version (#1872) 2023-04-01 19:47:33 +08:00
github-actions[bot]
40855ade01 📝 Update changelog 2023-04-01 09:47:08 +00:00
Umamusume-Agnes-Digital
d116563958 🍻 publish plugin 激战2!!! (#1869) 2023-04-01 17:45:44 +08:00
github-actions[bot]
8f603d3112 📝 Update changelog 2023-04-01 09:06:57 +00:00
mas-alone
998752926f 🍻 publish plugin ROLL (#1867) 2023-04-01 17:05:26 +08:00
github-actions[bot]
890b2ee22f 🔖 Release 2.0.0rc4 2023-04-01 03:59:10 +00:00
Ju4tCode
408292d679 🔖 bump version 2.0.0rc4 (#1870) 2023-04-01 11:52:43 +08:00
github-actions[bot]
ec4761c3a9 📝 Update changelog 2023-03-31 06:02:26 +00:00
AkashiCoin
0091a03653 🍻 publish plugin ChatGPT网页端API (#1864) 2023-03-31 14:01:18 +08:00
github-actions[bot]
e1a63f980f 📝 Update changelog 2023-03-31 04:48:41 +00:00
Ju4tCode
d982e14793 📝 update nonebug flow action arg (#1866) 2023-03-31 12:47:12 +08:00
Ju4tCode
43933920ed 📝 add editor config field name (#1863) 2023-03-29 23:26:27 +08:00
github-actions[bot]
fc03c58c70 📝 Update changelog 2023-03-29 15:10:44 +00:00
Akirami
283560daa7 Feature: 公开自定义 on 函数所需的函数 (#1856)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-03-29 23:09:33 +08:00
github-actions[bot]
efc4f5a0d5 📝 Update changelog 2023-03-29 11:01:45 +00:00
Ju4tCode
9f707469da fix test coverage condition (#1862) 2023-03-29 19:00:25 +08:00
github-actions[bot]
6396a7558a 📝 Update changelog 2023-03-29 08:01:17 +00:00
Ju4tCode
a8a76393a5 Feature: 重构驱动器 lifespan 方法 (#1860) 2023-03-29 15:59:54 +08:00
github-actions[bot]
0d0bc656c8 📝 Update changelog 2023-03-29 04:24:13 +00:00
Ju4tCode
2a2f7b6dce 🐛 detect runtime plugin (#1857) 2023-03-29 12:22:50 +08:00
github-actions[bot]
17c86f7da2 📝 Update changelog 2023-03-29 03:59:10 +00:00
Ju4tCode
1213e89bf5 add coverage condition annotation (#1858) 2023-03-29 11:57:33 +08:00
github-actions[bot]
ae08568daf 📝 Update changelog 2023-03-29 02:40:00 +00:00
Ju4tCode
8fbc85cf50 🐛 fix matcher create missing block (#1859) 2023-03-29 10:38:39 +08:00
github-actions[bot]
315dcb329e 📝 Update changelog 2023-03-28 15:22:44 +00:00
Cvandia
438e4f57e3 🍻 publish plugin 原神cos (#1854) 2023-03-28 23:21:19 +08:00
github-actions[bot]
a346efd684 📝 Update changelog 2023-03-28 15:13:45 +00:00
Ju4tCode
e3151c5f5e 📝 add warning to template (#1853) 2023-03-28 23:12:23 +08:00
github-actions[bot]
47536e6554 📝 Update changelog 2023-03-28 13:59:04 +00:00
NumberSir
79ac7f024f 🍻 publish plugin 颠倒问号 (#1848) 2023-03-28 21:57:34 +08:00
github-actions[bot]
3709e0ba4f 📝 Update changelog 2023-03-27 05:02:04 +00:00
CMHopeSunshine
c8ffafc1e8 🍻 publish plugin nonebot-plugin-miao (#1850) 2023-03-27 13:00:58 +08:00
github-actions[bot]
fedb67d4ae 📝 Update changelog 2023-03-27 04:53:42 +00:00
NCBM
076611166a 🍻 publish plugin 通括膨胀 (#1846) 2023-03-27 12:52:30 +08:00
github-actions[bot]
d5234e44f5 📝 Update changelog 2023-03-26 15:14:52 +00:00
A-kirami
64f78c279a 🍻 publish plugin Hello World (#1844) 2023-03-26 23:13:04 +08:00
github-actions[bot]
744443ab18 📝 Update changelog 2023-03-25 04:26:31 +00:00
nikissXI
9e5cde490e 🍻 publish plugin 喵喵点歌 (#1837) 2023-03-25 12:25:04 +08:00
github-actions[bot]
080c0db64b 📝 Update changelog 2023-03-25 03:11:14 +00:00
StarHeart
ec41b5f57f 📝 Docs: 移除 Messenger 移动端预期外的蓝色遮罩 (#1842) 2023-03-25 11:09:49 +08:00
github-actions[bot]
c441ec7080 📝 Update changelog 2023-03-25 02:59:11 +00:00
uy/sun
8bb92309d5 📝 Docs: 更新指向文档的链接 (#1841) 2023-03-25 10:58:03 +08:00
github-actions[bot]
9ab7666b6d 📝 Update changelog 2023-03-24 09:37:06 +00:00
Ju4tCode
add1f1473d update setup svg (#1840) 2023-03-24 17:32:24 +08:00
github-actions[bot]
cba38c399b 📝 Update changelog 2023-03-24 08:35:40 +00:00
Ju4tCode
18beb63d55 📝 Docs: 重写教程与进阶指南 (#1604)
Co-authored-by: Johnny Hsieh <32300164+mnixry@users.noreply.github.com>
2023-03-24 16:34:21 +08:00
github-actions[bot]
8977be2985 📝 Update changelog 2023-03-24 03:48:29 +00:00
uy/sun
00686380b8 Feature: 在 Windows 上处理 SIGBREAK 信号 (#1836) 2023-03-24 11:47:02 +08:00
github-actions[bot]
d4da953ad8 📝 Update changelog 2023-03-23 02:51:53 +00:00
QNLanYang
e5de8c8053 🍻 publish plugin ChatGLM-6B API版 (#1833) 2023-03-23 10:50:37 +08:00
github-actions[bot]
c2a2f8d420 📝 Update changelog 2023-03-23 02:28:08 +00:00
DaoMingze
af2ea7b83a 🍻 publish plugin ChatGLM (#1827) 2023-03-23 10:26:56 +08:00
github-actions[bot]
909a335106 📝 Update changelog 2023-03-22 12:55:55 +00:00
Johnny Hsieh
3e92fccd4e Feature: 为子依赖添加 PEP593 Annotated 支持 (#1832)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-22 20:54:46 +08:00
github-actions[bot]
9afaf3d516 📝 Update changelog 2023-03-21 06:01:34 +00:00
Alpaca4610
8ca56f24e3 🍻 publish plugin 基于OpenAI的AI模拟面试官 (#1828) 2023-03-21 14:00:19 +08:00
github-actions[bot]
8d09dd97f5 📝 Update changelog 2023-03-20 14:39:30 +00:00
Ju4tCode
78bbf9e623 🐛 Fix: 修复 bot hook 缺少依赖缓存和上下文管理 (#1826) 2023-03-20 22:37:57 +08:00
github-actions[bot]
05a6af46b9 📝 Update changelog 2023-03-20 05:52:12 +00:00
Astolfocat
243ad3f896 🍻 publish plugin 多平台热搜获取插件 (#1822) 2023-03-20 13:51:05 +08:00
github-actions[bot]
709c36bf5f 📝 Update changelog 2023-03-20 04:40:26 +00:00
Ju4tCode
ba808c85d5 improve user permission accessibility (#1825) 2023-03-20 12:39:17 +08:00
github-actions[bot]
c5444799f5 📝 Update changelog 2023-03-19 07:46:49 +00:00
Ju4tCode
36e99bc3ea Feature: 移除内置响应规则事件类型限制 (#1824) 2023-03-19 15:45:32 +08:00
github-actions[bot]
f65127e655 📝 Update changelog 2023-03-19 07:37:11 +00:00
QingMuCat
600c4f3268 🍻 publish plugin 随机点名 (#1818) 2023-03-19 15:36:02 +08:00
github-actions[bot]
53898dfb51 📝 Update changelog 2023-03-19 03:07:53 +00:00
MeetWq
95e3650c51 🍻 publish plugin 表情包制作(调用API版) (#1820) 2023-03-19 11:06:35 +08:00
github-actions[bot]
9f1b9ce2f3 📝 Update changelog 2023-03-18 06:41:17 +00:00
RongRongJi
551963c6c3 🍻 publish plugin 群聊语录库 (#1816) 2023-03-18 14:40:06 +08:00
github-actions[bot]
d59c999554 📝 Update changelog 2023-03-17 07:49:58 +00:00
Ju4tCode
8f44df371a allow using matcher subclass (#1815) 2023-03-17 15:48:48 +08:00
github-actions[bot]
7822cabe32 📝 Update changelog 2023-03-17 05:09:31 +00:00
NanakoOfficial
ca0b17b46a 🍻 publish plugin 随机狗妈 (#1812) 2023-03-17 13:08:17 +08:00
github-actions[bot]
d1404f6004 📝 Update changelog 2023-03-17 03:43:17 +00:00
Windylh
a294f0fbe0 🍻 publish plugin apex信息查询 (#1810) 2023-03-17 11:42:03 +08:00
github-actions[bot]
3cd0066715 📝 Update changelog 2023-03-17 03:10:19 +00:00
Zeta-qixi
faaef1a387 🍻 publish plugin unoconv文件转换 (#1808) 2023-03-17 11:08:56 +08:00
github-actions[bot]
ad4b244701 📝 Update changelog 2023-03-16 09:03:05 +00:00
forchannot
51e7bae8f2 🍻 publish plugin 原神历史卡池 (#1805) 2023-03-16 17:01:51 +08:00
github-actions[bot]
f18b6f609e 📝 Update changelog 2023-03-16 08:22:18 +00:00
MeetWq
dfbb32937e 🍻 publish plugin 括号补全 (#1803) 2023-03-16 16:21:07 +08:00
github-actions[bot]
cf9788ec99 📝 Update changelog 2023-03-16 08:01:14 +00:00
luoyefufeng
6b83d03094 🍻 publish plugin 修仙模拟器 (#1799) 2023-03-16 16:00:08 +08:00
github-actions[bot]
5508c1a4ee 📝 Update changelog 2023-03-16 07:53:03 +00:00
Ju4tCode
3462295562 🐛 fix missing cache in session updater (#1807) 2023-03-16 15:51:48 +08:00
github-actions[bot]
fee16082e0 📝 Update changelog 2023-03-15 11:32:24 +00:00
tkgs0
926b257065 🍻 publish bot 桃桃酱 (#1800) 2023-03-15 19:31:19 +08:00
github-actions[bot]
fca2d074e0 📝 Update changelog 2023-03-14 03:44:27 +00:00
iidamie
1a473f171c 🍻 publish plugin 发6 (#1797) 2023-03-14 11:43:12 +08:00
github-actions[bot]
97eee5a2f7 📝 Update changelog 2023-03-12 03:11:18 +00:00
DMCSWCG
8f1fbd9b36 🍻 publish plugin 群聊自定义表情包 (#1794) 2023-03-12 11:10:13 +08:00
github-actions[bot]
856f0b981f 📝 Update changelog 2023-03-11 12:50:31 +00:00
student_2333
f629fc9309 ✏️ Plugin: 删除 bnhhsh (#1792) 2023-03-11 20:49:17 +08:00
github-actions[bot]
e617bf2762 📝 Update changelog 2023-03-11 12:26:08 +00:00
lgc2333
072c2a2a41 🍻 publish plugin RimoFun (#1790) 2023-03-11 20:24:59 +08:00
github-actions[bot]
3a142033a1 📝 Update changelog 2023-03-10 11:38:15 +00:00
Alpaca4610
2832514f49 🍻 publish plugin ChatPDF文章分析 (#1787) 2023-03-10 19:37:06 +08:00
github-actions[bot]
e7887056b9 📝 Update changelog 2023-03-10 09:38:39 +00:00
TheLZY
500b59905d 🍻 publish plugin 和团子聊天! (#1784) 2023-03-10 17:37:26 +08:00
github-actions[bot]
4d4074ca24 📝 Update changelog 2023-03-10 09:28:30 +00:00
Suxmx
8391de52d9 🍻 publish plugin 多功能的ChatGPT机器人 (#1780) 2023-03-10 17:27:04 +08:00
github-actions[bot]
dd5f3bdea1 📝 Update changelog 2023-03-10 09:12:01 +00:00
Alpaca4610
c4bfe3a823 🍻 publish plugin ChatGPT官方接口版 (#1766) 2023-03-10 17:10:44 +08:00
github-actions[bot]
3ec76454e3 📝 Update changelog 2023-03-09 05:28:00 +00:00
zheuziihau
0e481d96a6 🍻 publish plugin 明日方舟抽卡记录分析 (#1746) 2023-03-09 13:26:51 +08:00
github-actions[bot]
c2b8bbee5f 📝 Update changelog 2023-03-09 05:13:33 +00:00
HCskia
61ad0733de 🍻 publish bot fubot (#1782) 2023-03-09 13:12:14 +08:00
github-actions[bot]
06fa0fb860 📝 Update changelog 2023-03-08 10:44:53 +00:00
allureluoli
e9b1692124 🍻 publish bot LOVE酱 (#1778) 2023-03-08 18:43:35 +08:00
pre-commit-ci[bot]
653902c6a2 ⬆️ auto update by pre-commit hooks (#1777)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-07 13:14:28 +08:00
github-actions[bot]
db0fcc0ceb 📝 Update changelog 2023-03-07 03:34:23 +00:00
Ju4tCode
f0021af6d4 👷 temporarily fix poetry install (#1776) 2023-03-07 11:33:13 +08:00
github-actions[bot]
7e614bb2b7 📝 Update changelog 2023-03-06 11:43:28 +00:00
Hoshinonyaruko
adba6c1890 🍻 publish plugin Sanae (#1774) 2023-03-06 19:42:13 +08:00
github-actions[bot]
6116d394e5 📝 Update changelog 2023-03-05 06:42:16 +00:00
maoxig
31ea5fa306 🍻 publish plugin 小爱课程表 (#1772) 2023-03-05 14:40:57 +08:00
github-actions[bot]
b209b77235 📝 Update changelog 2023-03-05 03:16:05 +00:00
DMCSWCG
81870e0d64 🍻 publish plugin AutoRepeater (#1768) 2023-03-05 11:14:56 +08:00
github-actions[bot]
40bccbc585 📝 Update changelog 2023-03-04 02:46:41 +00:00
zhulinyv
46c2817bba 🍻 publish bot 脑积水 (#1770) 2023-03-04 10:45:35 +08:00
github-actions[bot]
a5302a1872 📝 Update changelog 2023-03-03 03:12:35 +00:00
techotaku39
6642500c1c 🍻 publish plugin 60s日历 (#1751) 2023-03-03 11:11:22 +08:00
github-actions[bot]
f9464171fd 📝 Update changelog 2023-03-03 03:07:19 +00:00
ZM25XC
34d307b881 🍻 publish plugin 青年大学习提交(基础版) (#1763) 2023-03-03 11:06:05 +08:00
github-actions[bot]
8dc36aa630 📝 Update changelog 2023-03-01 13:21:39 +00:00
ZM25XC
f324b62eb2 🍻 publish plugin 青年大学习提交(Web UI) (#1761) 2023-03-01 21:14:05 +08:00
github-actions[bot]
a21b511568 📝 Update changelog 2023-03-01 10:35:31 +00:00
techotaku39
df1c13accd 🍻 publish plugin 网抑云 (#1759) 2023-03-01 18:34:19 +08:00
github-actions[bot]
c141b3eae7 📝 Update changelog 2023-03-01 10:25:48 +00:00
PadorFelice
a2a5af9b5e 🍻 publish plugin nonebot_plugin_eventdone (#1754) 2023-03-01 18:24:31 +08:00
github-actions[bot]
86a4f4043e 📝 Update changelog 2023-03-01 10:23:19 +00:00
Ju4tCode
f3aa8c6aa5 🐛 assert bot when disconnect (#1757) 2023-03-01 18:22:07 +08:00
github-actions[bot]
be81d094b4 📝 Update changelog 2023-02-28 06:47:15 +00:00
Ju4tCode
d0f832c4cd Feature: 添加 get_adapter 类型 overload (#1755)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-28 14:46:09 +08:00
github-actions[bot]
317a2b8c9b 📝 Update changelog 2023-02-26 16:12:34 +00:00
Ju4tCode
433c672130 Feature: 命令匹配支持强制指定空白符 (#1748) 2023-02-27 00:11:24 +08:00
github-actions[bot]
f8c67ebdf6 📝 Update changelog 2023-02-26 13:57:50 +00:00
17TheWord
2a95588421 🍻 publish plugin 爱发电审核 (#1749) 2023-02-26 21:56:35 +08:00
github-actions[bot]
a5fc40f2dc 📝 Update changelog 2023-02-26 13:39:57 +00:00
zzcqie666
e4ccb683cc 🍻 publish plugin 战地一入群审批 (#1744) 2023-02-26 21:38:54 +08:00
github-actions[bot]
34223d6b37 📝 Update changelog 2023-02-26 06:16:24 +00:00
Ju4tCode
04a7c3bc13 add get adapter (#1747) 2023-02-26 14:15:10 +08:00
github-actions[bot]
dd04190ca2 📝 Update changelog 2023-02-25 07:16:56 +00:00
mmxd12
5fd5b2f5b3 🍻 publish plugin wf的wm市场 (#1741) 2023-02-25 15:15:51 +08:00
github-actions[bot]
e8ad79aaf3 📝 Update changelog 2023-02-25 07:10:23 +00:00
Gin2O
ec8bc0424e 🍻 publish plugin 呆呆兽都会用的chatbot接api (#1737) 2023-02-25 15:09:15 +08:00
github-actions[bot]
6063714093 📝 Update changelog 2023-02-25 06:57:00 +00:00
Gin2O
1a0976e834 🍻 publish plugin 呆呆兽都会起来锻炼 H2E (#1736) 2023-02-25 14:55:53 +08:00
Ju4tCode
74743e6176 Develop: 升级 NoneBug 版本 (#1725) 2023-02-22 23:32:48 +08:00
github-actions[bot]
1befd9ffc6 📝 Update changelog 2023-02-22 15:30:13 +00:00
QingMuCat
c688450690 🍻 publish plugin 修仙_2.0 (#1729) 2023-02-22 23:28:44 +08:00
github-actions[bot]
d9e7986f5c 📝 Update changelog 2023-02-22 03:09:12 +00:00
Ikaros-521
d9bdf38a4e 🍻 publish plugin 发病语录 (#1727) 2023-02-22 11:07:55 +08:00
github-actions[bot]
1bd1f15c49 📝 Update changelog 2023-02-21 15:50:13 +00:00
felinae98
b4083ff9f9 🍻 publish plugin 峯驰物流 (#1722) 2023-02-21 23:49:03 +08:00
github-actions[bot]
2e766a86c2 📝 Update changelog 2023-02-21 15:26:52 +00:00
_3yude
93976a4162 📝 Docs: pip 安装指令添加引号 (#1724) 2023-02-21 23:25:43 +08:00
github-actions[bot]
4dc92ffc0b 📝 Update changelog 2023-02-20 16:45:00 +00:00
_3yude
5747397790 📝 Docs: 修正交互模式命令 (#1719)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-02-21 00:43:54 +08:00
github-actions[bot]
17fb3f92eb 📝 Update changelog 2023-02-20 14:26:30 +00:00
Ju4tCode
8f79ba1ccd Feature: 使用 tomllib 读取 toml 配置 (#1720) 2023-02-20 22:25:14 +08:00
github-actions[bot]
e11298d15b 📝 Update changelog 2023-02-20 05:20:25 +00:00
Ju4tCode
728902bfcf 🔊 Feature: 优化插件加载日志 (#1716) 2023-02-20 13:19:05 +08:00
github-actions[bot]
97723a0838 📝 Update changelog 2023-02-19 09:06:25 +00:00
Zhiyu
e42111f31f ✏️ Plugin: 修改链接分享解析器插件名称 (#1715) 2023-02-19 17:05:05 +08:00
github-actions[bot]
8d4eb7faf8 📝 Update changelog 2023-02-18 15:26:34 +00:00
Harry-Jing
9b19bf63b2 🍻 publish plugin Bing Chat (#1713) 2023-02-18 23:25:14 +08:00
github-actions[bot]
6685d88b44 📝 Update changelog 2023-02-18 15:01:46 +00:00
zhiyu1998
694db6c278 🍻 publish plugin 视频、图片解析器 (#1709) 2023-02-18 23:00:39 +08:00
github-actions[bot]
1dc02bfe8e 📝 Update changelog 2023-02-15 13:36:54 +00:00
bridgeL
045ab60699 🍻 publish plugin 你画我猜组队 (#1704) 2023-02-15 21:35:44 +08:00
github-actions[bot]
dc5ebd7a90 📝 Update changelog 2023-02-13 03:13:42 +00:00
student_2333
676e729df8 🔥 Bot: 移除 ShigureBot (#1699) 2023-02-13 11:12:40 +08:00
github-actions[bot]
dda6b97ef8 📝 Update changelog 2023-02-13 02:52:11 +00:00
NumberSir
b3d22ea8c4 🍻 publish plugin 明日方舟工具箱 (#1697) 2023-02-13 10:51:04 +08:00
github-actions[bot]
eac72d2d48 📝 Update changelog 2023-02-12 10:05:26 +00:00
monsterxcn
955ada47ed 🍻 publish plugin 原神深境螺旋数据查询 (#1695) 2023-02-12 18:04:23 +08:00
github-actions[bot]
4c31683231 📝 Update changelog 2023-02-12 03:48:39 +00:00
NCBM
a8f3d83947 🍻 publish plugin 工具拓展 (#1693) 2023-02-12 11:47:39 +08:00
github-actions[bot]
87e1866cf4 📝 Update changelog 2023-02-12 03:33:31 +00:00
j1g5awi
0d39b788bb 🍻 publish plugin OneBot 实现 (#1691) 2023-02-12 11:32:21 +08:00
github-actions[bot]
635668e6d4 📝 Update changelog 2023-02-10 02:19:47 +00:00
uy/sun
6358d07fbd 👷 发布机器人使用 latest 标签 (#1690) 2023-02-10 10:18:38 +08:00
github-actions[bot]
c03e4161c7 📝 Update changelog 2023-02-09 02:25:35 +00:00
scdhh
4c0d4065c5 Use raise from e when load driver error (#1689) 2023-02-09 10:24:27 +08:00
Umamusume-Agnes-Digital
720c736343 🍻 publish plugin 舞萌maimai插件版 (#1684) 2023-02-08 23:09:07 +08:00
github-actions[bot]
6d7435bf36 📝 Update changelog 2023-02-08 06:26:11 +00:00
BigOrangeQWQ
423e055ecd 🍻 publish plugin ACMReminder (#1685) 2023-02-08 14:25:05 +08:00
github-actions[bot]
b952f325d2 📝 Update changelog 2023-02-08 06:18:34 +00:00
KarisAya
3c3331a1ef 🍻 publish plugin 通用指令阻断 (#1682) 2023-02-08 14:17:27 +08:00
github-actions[bot]
4679e7d9cb 📝 Update changelog 2023-02-08 06:07:44 +00:00
CupidsBow
cbcd3987d2 🍻 publish bot koishi (#1680) 2023-02-08 14:06:25 +08:00
github-actions[bot]
d8cc1bd644 📝 Update changelog 2023-02-07 13:28:18 +00:00
Cvandia
09b0b2084d 🍻 publish plugin 今天吃喝什么(图片版) (#1677) 2023-02-07 21:27:03 +08:00
github-actions[bot]
6bc2870ea5 📝 Update changelog 2023-02-07 13:07:03 +00:00
Ju4tCode
f14580e688 ✏️ fix module path error in bilibili live (#1679) 2023-02-07 21:05:53 +08:00
github-actions[bot]
5ae24313f7 📝 Update changelog 2023-02-06 13:09:16 +00:00
cnchens
14088c6c51 🍻 publish bot ChensQBOTv2 (#1675) 2023-02-06 21:08:10 +08:00
Ju4tCode
73126535ef ⬆️ upgrade pre-commit config (#1674)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-05 12:57:31 +08:00
github-actions[bot]
22e27cce5f 📝 Update changelog 2023-02-05 03:46:41 +00:00
Reversedeer
8abab79cfc 🍻 publish plugin Q群消息事件监控 (#1666) 2023-02-05 11:45:45 +08:00
github-actions[bot]
6b419dc929 📝 Update changelog 2023-02-05 03:31:01 +00:00
tkgs0
8bf8b4760b 🍻 publish plugin DickyPK (#1668) 2023-02-05 11:29:54 +08:00
github-actions[bot]
678c1e1532 📝 Update changelog 2023-02-05 03:04:05 +00:00
Rene8028
325d28fbd4 🍻 publish plugin 每日人品2 (#1667) 2023-02-05 11:03:03 +08:00
github-actions[bot]
6bff6e9ad3 📝 Update changelog 2023-02-04 09:51:09 +00:00
KarisAya
d58c1407b5 🍻 publish plugin 娶群友 (#1664) 2023-02-04 17:49:53 +08:00
github-actions[bot]
e8760b6e4a 📝 Update changelog 2023-02-03 02:52:22 +00:00
KarisAya
4c92890265 🍻 publish plugin 我要一张xx涩图 (#1662) 2023-02-03 10:51:10 +08:00
Anh71me
d2f000bb16 Docs: Bump nb-autodoc from 0.x to 1.0.0a5 (#1661) 2023-02-02 16:10:22 +08:00
github-actions[bot]
b534b3b03f 📝 Update changelog 2023-02-02 02:58:33 +00:00
lgc2333
485d6e94b4 🍻 publish plugin AutoReply (#1659) 2023-02-02 10:57:34 +08:00
github-actions[bot]
2f5ba409ec 📝 Update changelog 2023-02-02 02:51:04 +00:00
eya46
e2e9bcc260 🍻 publish plugin B站热搜 (#1657) 2023-02-02 10:49:48 +08:00
github-actions[bot]
6e214efde8 📝 Update changelog 2023-02-01 08:17:45 +00:00
17TheWord
da72d20d0e 🍻 publish plugin MC Ping (#1655) 2023-02-01 16:16:37 +08:00
github-actions[bot]
646ab1002f 📝 Update changelog 2023-01-31 05:54:18 +00:00
Special-Week
1b4e5d05ab 🍻 publish plugin impact淫趴 (#1652) 2023-01-31 13:53:12 +08:00
github-actions[bot]
f323feac1b 📝 Update changelog 2023-01-31 02:31:43 +00:00
KroMiose
cb715c132f 🍻 publish plugin 更人性化的GPT-Ai聊天插件 (#1650) 2023-01-31 10:30:38 +08:00
github-actions[bot]
9e1b439128 📝 Update changelog 2023-01-30 02:55:54 +00:00
ZombieFly
f07e9bf699 🍻 publish plugin uuid生成器 (#1648) 2023-01-30 10:54:53 +08:00
github-actions[bot]
92e410ce0a 📝 Update changelog 2023-01-30 02:45:21 +00:00
Reversedeer
e14709c7f0 🍻 publish plugin 舔狗日记 (#1645) 2023-01-30 10:44:12 +08:00
github-actions[bot]
0533058dc2 📝 Update changelog 2023-01-29 02:36:06 +00:00
ZYKsslm
896e988f0e 🍻 publish plugin 查找轻小说 (#1643) 2023-01-29 10:34:59 +08:00
github-actions[bot]
b39fe9f1a9 📝 Update changelog 2023-01-29 02:23:19 +00:00
longchengguxiao
02301f6098 🍻 publish plugin XDU校园服务 (#1641) 2023-01-29 10:22:19 +08:00
github-actions[bot]
c5a0f0bd6d 📝 Update changelog 2023-01-28 08:40:54 +00:00
Proviasw
f59d5f0826 🍻 publish plugin nonebot-plugin-mcport (#1633) 2023-01-28 16:39:47 +08:00
github-actions[bot]
c369475603 📝 Update changelog 2023-01-27 03:07:16 +00:00
RF-Tar-Railt
18e698f2a0 🍻 publish plugin Alconna 命令工具 (#1638) 2023-01-27 11:06:03 +08:00
github-actions[bot]
33bab04a5a 📝 Update changelog 2023-01-27 03:01:58 +00:00
17TheWord
3cf31998b9 🍻 publish plugin Group_Link_Guild (#1636) 2023-01-27 11:00:57 +08:00
github-actions[bot]
c6d85ac9b0 📝 Update changelog 2023-01-27 02:51:16 +00:00
zhulinyv
27286fdc57 🍻 publish plugin 简易群管女生自用99新 (#1634) 2023-01-27 10:50:15 +08:00
github-actions[bot]
735cf9db6b 📝 Update changelog 2023-01-25 05:00:44 +00:00
17TheWord
d457bece3d 🍻 publish bot 青岚 (#1629) 2023-01-25 12:59:46 +08:00
github-actions[bot]
37b2a100d0 📝 Update changelog 2023-01-25 04:46:17 +00:00
17TheWord
cd4037c4fe 🍻 publish plugin 青岚 (#1628) 2023-01-25 12:45:08 +08:00
github-actions[bot]
13a490de0d 📝 Update changelog 2023-01-25 04:33:48 +00:00
Hiroshi12138
cec09397cc 🍻 publish plugin 对话超管 (#1626) 2023-01-25 12:32:45 +08:00
StarHeart
a11ac82a91 🚸 add constraint for port (#1632) 2023-01-25 12:31:26 +08:00
github-actions[bot]
6410af19ba 📝 Update changelog 2023-01-23 02:59:13 +00:00
kifuan
8af08c6417 🍻 publish plugin 摩尔质量计算器 (#1624) 2023-01-23 10:58:15 +08:00
github-actions[bot]
801d29f66c 📝 Update changelog 2023-01-23 02:51:13 +00:00
longchengguxiao
c518f768d1 🍻 publish plugin 植物大战僵尸小游戏 (#1621) 2023-01-23 10:50:02 +08:00
github-actions[bot]
27149108d9 📝 Update changelog 2023-01-22 10:17:41 +00:00
Jigsaw
5d1582566a 🔥 Docs: 移除商店中的过期插件 2023 (#1610) 2023-01-22 18:16:26 +08:00
406 changed files with 40539 additions and 16681 deletions

View File

@@ -2,7 +2,7 @@
"name": "Default Linux Universal",
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:1": {}
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
},
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
"customizations": {

View File

@@ -0,0 +1,57 @@
name: 发布适配器
title: "Adapter: {name}"
description: 发布适配器到 NoneBot 官方商店
labels: ["Adapter"]
body:
- type: input
id: name
attributes:
label: 适配器名称
description: 适配器名称
validations:
required: true
- type: input
id: description
attributes:
label: 适配器描述
description: 适配器描述
validations:
required: true
- type: input
id: pypi
attributes:
label: PyPI 项目名
description: PyPI 项目名
placeholder: e.g. nonebot-adapter-xxx
validations:
required: true
- type: input
id: module
attributes:
label: 适配器 import 包名
description: 适配器 import 包名
placeholder: e.g. nonebot_adapter_xxx
validations:
required: true
- type: input
id: homepage
attributes:
label: 适配器项目仓库/主页链接
description: 适配器项目仓库/主页链接
placeholder: e.g. https://github.com/xxx/xxx
validations:
required: true
- type: input
id: tags
attributes:
label: 标签
description: 标签
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
value: "[]"
validations:
required: true

37
.github/ISSUE_TEMPLATE/bot_publish.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: 发布机器人
title: "Bot: {name}"
description: 发布机器人到 NoneBot 官方商店
labels: ["Bot"]
body:
- type: input
id: name
attributes:
label: 机器人名称
description: 机器人名称
validations:
required: true
- type: input
id: description
attributes:
label: 机器人描述
description: 机器人描述
validations:
required: true
- type: input
id: homepage
attributes:
label: 机器人项目仓库/主页链接
description: 机器人项目仓库/主页链接
placeholder: e.g. https://github.com/xxx/xxx
- type: input
id: tags
attributes:
label: 标签
description: 标签
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
value: "[]"
validations:
required: true

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a bug report to help us improve
title: 'Bug: Something went wrong'
labels: bug
assignees: ''
---
**描述问题:**
A clear and concise description of what the bug is.
**如何复现?**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**期望的结果**
A clear and concise description of what you expected to happen.
**环境信息:**
- OS: [e.g. Linux]
- Python Version: [e.g. 3.8]
- Nonebot Version: [e.g. 2.0.0]
**协议端信息:**
- 协议端: [e.g. go-cqhttp]
- 协议端版本: [e.g. 1.0.0]
**截图或日志**
If applicable, add screenshots to help explain your problem.

85
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Bug 反馈
title: "Bug: 出现异常"
description: 提交 Bug 反馈以帮助我们改进代码
labels: ["bug"]
body:
- type: dropdown
id: env-os
attributes:
label: 操作系统
description: 选择运行 NoneBot 的系统
options:
- Windows
- MacOS
- Linux
- Other
validations:
required: true
- type: input
id: env-python-ver
attributes:
label: Python 版本
description: 填写运行 NoneBot 的 Python 版本
placeholder: e.g. 3.11.0
validations:
required: true
- type: input
id: env-nb-ver
attributes:
label: NoneBot 版本
description: 填写 NoneBot 版本
placeholder: e.g. 2.0.0
validations:
required: true
- type: input
id: env-adapter
attributes:
label: 适配器
description: 填写使用的适配器以及版本
placeholder: e.g. OneBot v11 2.2.2
validations:
required: true
- type: input
id: env-protocol
attributes:
label: 协议端
description: 填写连接 NoneBot 的协议端及版本
placeholder: e.g. go-cqhttp 1.0.0
validations:
required: true
- type: textarea
id: describe
attributes:
label: 描述问题
description: 清晰简洁地说明问题是什么
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 复现步骤
description: 提供能复现此问题的详细操作步骤
placeholder: |
1. 首先……
2. 然后……
3. 发生……
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望的结果
description: 清晰简洁地描述你期望发生的事情
- type: textarea
id: logs
attributes:
label: 截图或日志
description: 提供有助于诊断问题的任何日志和截图

View File

@@ -1,14 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question
- name: NoneBot 论坛
url: https://discussions.nonebot.dev/
about: Ask questions about nonebot
- name: Plugin Publish
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
about: Publish your adapter to nonebot homepage and nb-cli
- name: Bot Publish
url: https://v2.nonebot.dev/store
about: Publish your bot to nonebot homepage and nb-cli
about: 前往 NoneBot 论坛提问

View File

@@ -1,17 +0,0 @@
---
name: Document improvement
about: Feedback on documentation, including errors and ideas
title: 'Docs: some description'
labels: documentation
assignees: ''
---
**描述问题或主题:**
**需做出的修改:**
* [ ] 一些修改
* [ ] 一些修改
* [ ] 一些修改

18
.github/ISSUE_TEMPLATE/document.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: 文档改进
title: "Docs: 描述"
description: 文档错误及改进意见反馈
labels: ["documentation"]
body:
- type: textarea
id: problem
attributes:
label: 描述问题或主题
validations:
required: true
- type: textarea
id: improve
attributes:
label: 需做出的修改
validations:
required: true

View File

@@ -1,16 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: 'Feature: Something you want'
labels: enhancement
assignees: ''
---
**是否在使用中遇到某些问题而需要新的特性?请描述:**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**描述你所需要的特性:**
A clear and concise description of what you want to happen.

View File

@@ -0,0 +1,20 @@
name: 功能建议
title: "Feature: 功能描述"
description: 提出关于项目新功能的想法
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: 希望能解决的问题
description: 在使用中遇到什么问题而需要新的功能?
validations:
required: true
- type: textarea
id: feature
attributes:
label: 描述所需要的功能
description: 请说明需要的功能或解决方法
validations:
required: true

View File

@@ -0,0 +1,57 @@
name: 发布插件
title: "Plugin: {name}"
description: 发布插件到 NoneBot 官方商店
labels: ["Plugin"]
body:
- type: input
id: name
attributes:
label: 插件名称
description: 插件名称
validations:
required: true
- type: input
id: description
attributes:
label: 插件描述
description: 插件描述
validations:
required: true
- type: input
id: pypi
attributes:
label: PyPI 项目名
description: PyPI 项目名
placeholder: e.g. nonebot-plugin-xxx
validations:
required: true
- type: input
id: module
attributes:
label: 插件 import 包名
description: 插件 import 包名
placeholder: e.g. nonebot_plugin_xxx
validations:
required: true
- type: input
id: homepage
attributes:
label: 插件项目仓库/主页链接
description: 插件项目仓库/主页链接
placeholder: e.g. https://github.com/xxx/xxx
validations:
required: true
- type: input
id: tags
attributes:
label: 标签
description: 标签
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
value: "[]"
validations:
required: true

View File

@@ -5,7 +5,10 @@ runs:
using: "composite"
steps:
- run: |
poetry run nb-autodoc nonebot
poetry run nb-autodoc nonebot \
-s nonebot.plugins \
-u nonebot.internal \
-u nonebot.internal.*
cp -r ./build/nonebot/* ./website/docs/api/
yarn prettier
shell: bash

View File

@@ -1,4 +1,4 @@
name: NoneBot2 Publish Bot
name: NoneFlow
on:
issues:
@@ -7,6 +7,8 @@ on:
types: [closed]
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
@@ -16,7 +18,23 @@ jobs:
plugin_test:
runs-on: ubuntu-latest
name: nonebot2 plugin test
if: github.event_name != 'issue_comment' || !github.event.issue.pull_request
if: |
!(
(
github.event.pull_request &&
(
github.event.pull_request.head.repo.fork ||
!(
contains(github.event.pull_request.labels.*.name, 'Plugin') ||
contains(github.event.pull_request.labels.*.name, 'Adapter') ||
contains(github.event.pull_request.labels.*.name, 'Bot')
)
)
) ||
(
github.event_name == 'issue_comment' && github.event.issue.pull_request
)
)
permissions:
issues: read
outputs:
@@ -26,28 +44,36 @@ jobs:
- name: Install Poetry
if: ${{ !startsWith(github.event_name, 'pull_request') }}
run: pipx install poetry
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Test Plugin
id: plugin-test
run: |
curl -sSL https://raw.githubusercontent.com/nonebot/nonebot2-publish-bot/main/plugin_test.py -o plugin_test.py
python plugin_test.py
publish_bot:
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
noneflow:
runs-on: ubuntu-latest
name: nonebot2 publish bot
name: noneflow
needs: plugin_test
steps:
- name: Generate token
id: generate-token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_KEY }}
- name: Checkout Code
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
- name: NoneBot2 Publish Bot
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
token: ${{ steps.generate-token.outputs.token }}
- name: NoneFlow
uses: docker://ghcr.io/nonebot/noneflow:latest
with:
token: ${{ secrets.GH_TOKEN }}
config: >
{
"base": "master",
@@ -58,3 +84,5 @@ jobs:
env:
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
APP_ID: ${{ secrets.APP_ID }}
PRIVATE_KEY: ${{ secrets.APP_KEY }}

View File

@@ -18,9 +18,16 @@ jobs:
group: pull-request-changelog
cancel-in-progress: true
steps:
- name: Generate token
id: generate-token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Node Environment
uses: ./.github/actions/setup-node
@@ -43,8 +50,8 @@ jobs:
- 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 config user.name noneflow[bot]
git config user.email 129742071+noneflow[bot]@users.noreply.github.com
git add .
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
git push

View File

@@ -6,12 +6,17 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate token
id: generate-token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_KEY }}
- uses: actions/checkout@v3
with:
ref: master
token: ${{ secrets.GH_TOKEN }}
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Python Environment
uses: ./.github/actions/setup-python
@@ -39,8 +44,8 @@ jobs:
- name: Push Tag
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git config user.name noneflow[bot]
git config user.email 129742071+noneflow[bot]@users.noreply.github.com
git add .
git commit -m ":bookmark: Release $(poetry version -s)"
git tag ${{ env.TAG_NAME }}

View File

@@ -7,32 +7,32 @@ ci:
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.2
rev: v2.1.3
hooks:
- id: pycln
args: [--config, pyproject.toml]
- repo: https://github.com/pycqa/isort
rev: 5.11.4
rev: 5.12.0
hooks:
- id: isort
stages: [commit]
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.3.0
hooks:
- id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.4
rev: v3.0.0-alpha.9-for-vscode
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
stages: [commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.3
rev: v0.1.4
hooks:
- id: nonemoji
stages: [prepare-commit-msg]

View File

@@ -1,3 +1,3 @@
# Changelog
See [changelog.md](./website/src/pages/changelog.md) or <https://v2.nonebot.dev/changelog>
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>

View File

@@ -66,16 +66,17 @@ yarn start
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你“Hello, World !”。`
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>
以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件
如果你需要编辑器检查 Markdown 规范,可以 VSCode 中安装 markdownlint 扩展
### 参与开发
@@ -83,7 +84,7 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
## 为社区做贡献
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://nonebot.dev/docs/developer/plugin-publishing) 一节。
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。

View File

@@ -1,6 +1,6 @@
<!-- markdownlint-disable MD033 MD041 -->
<p align="center">
<a href="https://v2.nonebot.dev/"><img src="https://v2.nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
<a href="https://nonebot.dev/"><img src="https://nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
</p>
<div align="center">
@@ -49,9 +49,9 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
</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/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 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>
<br />
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
@@ -69,18 +69,16 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
</p>
<p align="center">
<a href="https://v2.nonebot.dev/">文档</a>
<a href="https://nonebot.dev/">文档</a>
·
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a>
·
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
<a href="https://nonebot.dev/docs/quick-start">快速上手</a>
·
<a href="#插件">文档打不开?</a>
</p>
<p align="center">
<a href="https://asciinema.org/a/464654">
<img src="https://v2.nonebot.dev/img/setup.svg">
<a href="https://asciinema.org/a/569440">
<img src="https://nonebot.dev/img/setup.svg">
</a>
</p>
@@ -92,36 +90,37 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
- 生而可靠100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
- 生而可靠100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://nonebot.dev/docs/editor-support))
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
| 协议名称 | 状态 | 注释 |
| :-----------------------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
| [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/) | ↗️ | 由社区贡献 |
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 由社区贡献 |
| [MineCraft (Spigot)](https://github.com/17TheWord/nonebot-adapter-spigot) | ↗️ | 由社区贡献 |
| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
| OneBot[仓库](https://github.com/nonebot/adapter-onebot)[协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
| Telegram[仓库](https://github.com/nonebot/adapter-telegram)[协议](https://core.telegram.org/bots/api) | ✅ | |
| 飞书[仓库](https://github.com/nonebot/adapter-feishu)[协议](https://open.feishu.cn/document/home/index) | ✅ | |
| GitHub[仓库](https://github.com/nonebot/adapter-github)[协议](https://docs.github.com/en/apps) | ✅ | GitHub APP & OAuth APP |
| QQ 频道[仓库](https://github.com/nonebot/adapter-qqguild)[协议](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
| 钉钉[仓库](https://github.com/nonebot/adapter-ding)[协议](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer(暂不可用) |
| Console[仓库](https://github.com/nonebot/adapter-console) | ✅ | 控制台交互 |
| 开黑啦[仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)[协议](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
| Mirai[仓库](https://github.com/ieew/nonebot_adapter_mirai2)[协议](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | QQ 协议,由社区贡献 |
| Ntchat[仓库](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 微信协议,由社区贡献 |
| MineCraft[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft) | ↗️ | 由社区贡献 |
| BiliBili Live[仓库](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
| Walle-Q[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq) | ↗️ | QQ 协议,由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
| 驱动框架 | 类型 |
| :--------------------------------------------------------: | :----: |
| :-----------------------------------------------------------------: | :----: |
| [FastAPI](https://fastapi.tiangolo.com/) | 服务端 |
| [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask) | 服务端 |
| [Quart](https://quart.palletsprojects.com/en/latest/)异步 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/)
更多:[概览](https://nonebot.dev/docs/)
## 什么不是 NoneBot2
@@ -133,7 +132,7 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
## 即刻开始
~~完整~~文档可以在 [这里](https://v2.nonebot.dev/) 查看。
~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。
懒得看文档?下面是快速安装指南:
@@ -190,7 +189,7 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
- [文档镜像(中国境内)](https://nb2.baka.icu)
- [文档镜像(Vercel)](https://nonebot2-vercel-mirror.vercel.app)
- 其他插件请查看 [商店](https://v2.nonebot.dev/store)
- 其他插件请查看 [商店](https://nonebot.dev/store)
## 许可证

View File

@@ -38,17 +38,16 @@ FrontMatter:
"""
import os
import importlib
from importlib.metadata import version
from typing import Any, Dict, Type, Optional
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
import loguru
from pydantic.env_settings import DotenvType
from nonebot.adapters import Bot
from nonebot.utils import escape_tag
from nonebot.config import Env, Config
from nonebot.log import logger as logger
from nonebot.adapters import Bot, Adapter
from nonebot.utils import escape_tag, resolve_dot_notation
from nonebot.drivers import Driver, ReverseDriver, combine_driver
try:
@@ -56,6 +55,8 @@ try:
except Exception: # pragma: no cover
__version__ = None
A = TypeVar("A", bound=Adapter)
_driver: Optional[Driver] = None
@@ -80,6 +81,56 @@ def get_driver() -> Driver:
return _driver
@overload
def get_adapter(name: str) -> Adapter:
...
@overload
def get_adapter(name: Type[A]) -> A:
...
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
返回:
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
异常:
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
from nonebot.adapters.console import Adapter
adapter = nonebot.get_adapter(Adapter)
```
"""
adapters = get_adapters()
target = name if isinstance(name, str) else name.get_name()
if target not in adapters:
raise ValueError(f"Adapter {target} not registered.")
return adapters[target]
def get_adapters() -> Dict[str, Adapter]:
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
返回:
所有 {ref}`nonebot.adapters.Adapter` 实例字典
异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
用法:
```python
adapters = nonebot.get_adapters()
```
"""
return get_driver()._adapters.copy()
def get_app() -> Any:
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
@@ -175,31 +226,16 @@ def get_bots() -> Dict[str, Bot]:
return get_driver().bots
def _resolve_dot_notation(
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
) -> Any:
modulename, _, cls = obj_str.partition(":")
if default_prefix is not None and modulename.startswith("~"):
modulename = default_prefix + modulename[1:]
module = importlib.import_module(modulename)
if not cls:
return getattr(module, default_attr)
instance = module
for attr_str in cls.split("."):
instance = getattr(instance, attr_str)
return instance
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
drivers = obj_str.split("+")
DriverClass = _resolve_dot_notation(
DriverClass = resolve_dot_notation(
drivers[0], "Driver", default_prefix="nonebot.drivers."
)
if len(drivers) == 1:
logger.trace(f"Detected driver {DriverClass} with no mixins.")
return DriverClass
mixins = [
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
for mixin in drivers[1:]
]
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")

View File

@@ -19,6 +19,11 @@ __autodoc__ = {
"Event": True,
"Adapter": True,
"Message": True,
"Message.__getitem__": True,
"Message.__contains__": True,
"Message._construct": True,
"MessageSegment": True,
"MessageSegment.__str__": True,
"MessageSegment.__add__": True,
"MessageTemplate": True,
}

View File

@@ -14,7 +14,7 @@ from ipaddress import IPv4Address
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
from pydantic.utils import deep_update
from pydantic import Extra, BaseSettings, IPvAnyAddress
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
from pydantic.env_settings import (
DotenvType,
SettingsError,
@@ -158,7 +158,7 @@ class Config(BaseConfig):
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
"""
_env_file: DotenvType = ".env", ".env.prod"
@@ -173,7 +173,7 @@ class Config(BaseConfig):
"""
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
port: int = 8080
port: int = Field(default=8080, ge=1, le=65535)
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
log_level: Union[int, str] = "INFO"
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称

View File

@@ -4,6 +4,8 @@ FrontMatter:
sidebar_position: 9
description: nonebot.consts 模块
"""
import os
import sys
from typing import Literal
# used by Matcher
@@ -30,6 +32,8 @@ CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
"""命令参数存储 key"""
CMD_START_KEY: Literal["command_start"] = "command_start"
"""命令开头存储 key"""
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
"""命令与参数间空白符存储 key"""
SHELL_ARGS: Literal["_args"] = "_args"
"""shell 命令 parse 后参数字典存储 key"""
@@ -38,12 +42,6 @@ SHELL_ARGV: Literal["_argv"] = "_argv"
REGEX_MATCHED: Literal["_matched"] = "_matched"
"""正则匹配结果存储 key"""
REGEX_STR: Literal["_matched_str"] = "_matched_str"
"""正则匹配文本存储 key"""
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
"""正则匹配 group 元组存储 key"""
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
"""正则匹配 group 字典存储 key"""
STARTSWITH_KEY: Literal["_startswith"] = "_startswith"
"""响应触发前缀 key"""
ENDSWITH_KEY: Literal["_endswith"] = "_endswith"
@@ -52,3 +50,5 @@ FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch"
"""响应触发完整消息 key"""
KEYWORD_KEY: Literal["_keyword"] = "_keyword"
"""响应触发关键字 key"""
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")

View File

@@ -0,0 +1,45 @@
from typing import Any, List, Union, Callable, Awaitable, cast
from nonebot.utils import run_sync, is_coroutine_callable
SYNC_LIFESPAN_FUNC = Callable[[], Any]
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]]
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
class Lifespan:
def __init__(self) -> None:
self._startup_funcs: List[LIFESPAN_FUNC] = []
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._startup_funcs.append(func)
return func
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
self._shutdown_funcs.append(func)
return func
@staticmethod
async def _run_lifespan_func(
funcs: List[LIFESPAN_FUNC],
) -> None:
for func in funcs:
if is_coroutine_callable(func):
await cast(ASYNC_LIFESPAN_FUNC, func)()
else:
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
async def startup(self) -> None:
if self._startup_funcs:
await self._run_lifespan_func(self._startup_funcs)
async def shutdown(self) -> None:
if self._shutdown_funcs:
await self._run_lifespan_func(self._shutdown_funcs)
async def __aenter__(self) -> None:
await self.startup()
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.shutdown()

View File

@@ -27,10 +27,10 @@ from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_dr
try:
import aiohttp
except ImportError: # pragma: no cover
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
) from None
) from e
class Mixin(ForwardMixin):

View File

@@ -19,7 +19,7 @@ FrontMatter:
import logging
import contextlib
from functools import wraps
from typing import Any, Dict, List, Tuple, Union, Callable, Optional
from typing import Any, Dict, List, Tuple, Union, Optional
from pydantic import BaseSettings
@@ -32,15 +32,17 @@ from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
from ._lifespan import LIFESPAN_FUNC, Lifespan
try:
import uvicorn
from fastapi.responses import Response
from fastapi import FastAPI, Request, UploadFile, status
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
except ImportError: # pragma: no cover
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
) from None
) from e
def catch_closed(func):
@@ -92,7 +94,10 @@ class Driver(ReverseDriver):
self.fastapi_config: Config = Config(**config.dict())
self._lifespan = Lifespan()
self._server_app = FastAPI(
lifespan=self._lifespan_manager,
openapi_url=self.fastapi_config.fastapi_openapi_url,
docs_url=self.fastapi_config.fastapi_docs_url,
redoc_url=self.fastapi_config.fastapi_redoc_url,
@@ -148,14 +153,20 @@ class Driver(ReverseDriver):
)
@overrides(ReverseDriver)
def on_startup(self, func: Callable) -> Callable:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
return self.server_app.on_event("startup")(func)
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_startup(func)
@overrides(ReverseDriver)
def on_shutdown(self, func: Callable) -> Callable:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_"""
return self.server_app.on_event("shutdown")(func)
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
return self._lifespan.on_shutdown(func)
@contextlib.asynccontextmanager
async def _lifespan_manager(self, app: FastAPI):
await self._lifespan.startup()
try:
yield
finally:
await self._lifespan.shutdown()
@overrides(ReverseDriver)
def run(

View File

@@ -31,10 +31,10 @@ from nonebot.drivers import (
try:
import httpx
except ImportError: # pragma: no cover
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install httpx by using `pip install nonebot2[httpx]`"
) from None
) from e
class Mixin(ForwardMixin):

View File

@@ -13,19 +13,21 @@ FrontMatter:
import signal
import asyncio
import threading
from typing import Set, Union, Callable, Awaitable, cast
from nonebot.log import logger
from nonebot.consts import WINDOWS
from nonebot.typing import overrides
from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver
from nonebot.utils import run_sync, is_coroutine_callable
HOOK_FUNC = Union[Callable[[], None], Callable[[], Awaitable[None]]]
from ._lifespan import LIFESPAN_FUNC, Lifespan
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
)
if WINDOWS: # pragma: py-win32
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
class Driver(BaseDriver):
@@ -33,8 +35,9 @@ class Driver(BaseDriver):
def __init__(self, env: Env, config: Config):
super().__init__(env, config)
self.startup_funcs: Set[HOOK_FUNC] = set()
self.shutdown_funcs: Set[HOOK_FUNC] = set()
self._lifespan = Lifespan()
self.should_exit: asyncio.Event = asyncio.Event()
self.force_exit: bool = False
@@ -51,20 +54,14 @@ class Driver(BaseDriver):
return logger
@overrides(BaseDriver)
def on_startup(self, func: HOOK_FUNC) -> HOOK_FUNC:
"""
注册一个启动时执行的函数
"""
self.startup_funcs.add(func)
return func
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个启动时执行的函数"""
return self._lifespan.on_startup(func)
@overrides(BaseDriver)
def on_shutdown(self, func: HOOK_FUNC) -> HOOK_FUNC:
"""
注册一个停止时执行的函数
"""
self.shutdown_funcs.add(func)
return func
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""注册一个停止时执行的函数"""
return self._lifespan.on_shutdown(func)
@overrides(BaseDriver)
def run(self, *args, **kwargs):
@@ -82,16 +79,8 @@ class Driver(BaseDriver):
await self._shutdown()
async def _startup(self):
# run startup
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)
await self._lifespan.startup()
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running startup function. "
@@ -107,16 +96,9 @@ class Driver(BaseDriver):
logger.info("Shutting down")
logger.info("Waiting for application shutdown.")
# run shutdown
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)
await self._lifespan.shutdown()
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running shutdown function. "
@@ -160,7 +142,15 @@ class Driver(BaseDriver):
signal.signal(sig, self._handle_exit)
def _handle_exit(self, sig, frame):
if self.should_exit.is_set():
self.force_exit = True
else:
self.exit(force=self.should_exit.is_set())
def exit(self, force: bool = False):
"""退出 none driver
参数:
force: 强制退出
"""
if not self.should_exit.is_set():
self.should_exit.set()
if force:
self.force_exit = True

View File

@@ -37,10 +37,10 @@ try:
from quart import Quart, Request, Response
from quart.datastructures import FileStorage
from quart import Websocket as QuartWebSocket
except ImportError: # pragma: no cover
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install Quart by using `pip install nonebot2[quart]`"
) from None
) from e
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])

View File

@@ -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: # pragma: no cover
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install websockets by using `pip install nonebot2[websockets]`"
) from None
) from e
logger = logging.Logger("websockets.client", "INFO")
logger.addHandler(LoguruHandler())

View File

@@ -66,8 +66,9 @@ class Adapter(abc.ABC):
参数:
bot: {ref}`nonebot.adapters.Bot` 实例
"""
if self.bots.pop(bot.self_id, None) is None:
raise RuntimeError(f"{bot} not found in adapter {self.get_name()}")
self.driver._bot_disconnect(bot)
self.bots.pop(bot.self_id, None)
def setup_http_server(self, setup: HTTPServerSetup):
"""设置一个 HTTP 服务器路由配置"""

View File

@@ -47,7 +47,7 @@ class Event(abc.ABC, BaseModel):
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
异常:
NoLogException
NoLogException:
"""
return f"[{self.get_event_name()}]: {self.get_event_description()}"

View File

@@ -1,5 +1,6 @@
import abc
from copy import deepcopy
from typing_extensions import Self
from dataclasses import field, asdict, dataclass
from typing import (
Any,
@@ -12,6 +13,7 @@ from typing import (
TypeVar,
Iterable,
Optional,
SupportsIndex,
overload,
)
@@ -19,7 +21,6 @@ from pydantic import parse_obj_as
from .template import MessageTemplate
T = TypeVar("T")
TMS = TypeVar("TMS", bound="MessageSegment")
TM = TypeVar("TM", bound="Message")
@@ -47,7 +48,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
def __len__(self) -> int:
return len(str(self))
def __ne__(self: T, other: T) -> bool:
def __ne__(self, other: Self) -> bool:
return not self == other
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
@@ -61,7 +62,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
yield cls._validate
@classmethod
def _validate(cls, value):
def _validate(cls, value) -> Self:
if isinstance(value, cls):
return value
if not isinstance(value, dict):
@@ -84,7 +85,10 @@ class MessageSegment(abc.ABC, Generic[TM]):
def items(self):
return asdict(self).items()
def copy(self: T) -> T:
def join(self: TMS, iterable: Iterable[Union[TMS, TM]]) -> TM:
return self.get_message_class()(self).join(iterable)
def copy(self) -> Self:
return deepcopy(self)
@abc.abstractmethod
@@ -117,7 +121,7 @@ class Message(List[TMS], abc.ABC):
self.extend(self._construct(message)) # pragma: no cover
@classmethod
def template(cls: Type[TM], format_string: Union[str, TM]) -> MessageTemplate[TM]:
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
"""创建消息模板。
用法和 `str.format` 大致相同, 但是可以输出消息对象, 并且支持以 `Message` 对象作为消息模板
@@ -146,7 +150,7 @@ class Message(List[TMS], abc.ABC):
yield cls._validate
@classmethod
def _validate(cls, value):
def _validate(cls, value) -> Self:
if isinstance(value, cls):
return value
elif isinstance(value, Message):
@@ -169,16 +173,16 @@ class Message(List[TMS], abc.ABC):
"""构造消息数组"""
raise NotImplementedError
def __add__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
def __add__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
result = self.copy()
result += other
return result
def __radd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
def __radd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
result = self.__class__(other)
return result + self
def __iadd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
def __iadd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
if isinstance(other, str):
self.extend(self._construct(other))
elif isinstance(other, MessageSegment):
@@ -190,57 +194,62 @@ class Message(List[TMS], abc.ABC):
return self
@overload
def __getitem__(self: TM, __args: str) -> TM:
"""
def __getitem__(self, args: str) -> Self:
"""获取仅包含指定消息段类型的消息
参数:
__args: 消息段类型
args: 消息段类型
返回:
所有类型为 `__args` 的消息段
所有类型为 `args` 的消息段
"""
@overload
def __getitem__(self, __args: Tuple[str, int]) -> TMS:
"""
def __getitem__(self, args: Tuple[str, int]) -> TMS:
"""索引指定类型的消息段
参数:
__args: 消息段类型和索引
args: 消息段类型和索引
返回:
类型为 `__args[0]` 的消息段第 `__args[1]` 个
类型为 `args[0]` 的消息段第 `args[1]` 个
"""
@overload
def __getitem__(self: TM, __args: Tuple[str, slice]) -> TM:
"""
def __getitem__(self, args: Tuple[str, slice]) -> Self:
"""切片指定类型的消息段
参数:
__args: 消息段类型和切片
args: 消息段类型和切片
返回:
类型为 `__args[0]` 的消息段切片 `__args[1]`
类型为 `args[0]` 的消息段切片 `args[1]`
"""
@overload
def __getitem__(self, __args: int) -> TMS:
"""
def __getitem__(self, args: int) -> TMS:
"""索引消息段
参数:
__args: 索引
args: 索引
返回:
第 `__args` 个消息段
第 `args` 个消息段
"""
@overload
def __getitem__(self: TM, __args: slice) -> TM:
"""
def __getitem__(self, args: slice) -> Self:
"""切片消息段
参数:
__args: 切片
args: 切片
返回:
消息切片 `__args`
消息切片 `args`
"""
def __getitem__(
self: TM,
self,
args: Union[
str,
Tuple[str, int],
@@ -248,7 +257,7 @@ class Message(List[TMS], abc.ABC):
int,
slice,
],
) -> Union[TMS, TM]:
) -> Union[TMS, Self]:
arg1, arg2 = args if isinstance(args, tuple) else (args, None)
if isinstance(arg1, int) and arg2 is None:
return super().__getitem__(arg1)
@@ -263,15 +272,52 @@ class Message(List[TMS], abc.ABC):
else:
raise ValueError("Incorrect arguments to slice") # pragma: no cover
def index(self, value: Union[TMS, str], *args) -> int:
def __contains__(self, value: Union[TMS, str]) -> bool:
"""检查消息段是否存在
参数:
value: 消息段或消息段类型
返回:
消息内是否存在给定消息段或给定类型的消息段
"""
if isinstance(value, str):
return bool(next((seg for seg in self if seg.type == value), None))
return super().__contains__(value)
def has(self, value: Union[TMS, str]) -> bool:
"""{ref}``__contains__` <nonebot.adapters.Message.__contains__>` 相同"""
return value in self
def index(self, value: Union[TMS, str], *args: SupportsIndex) -> int:
"""索引消息段
参数:
value: 消息段或者消息段类型
arg: start 与 end
返回:
索引 index
异常:
ValueError: 消息段不存在
"""
if isinstance(value, str):
first_segment = next((seg for seg in self if seg.type == value), None)
if first_segment is None:
raise ValueError(f"Segment with type {value} is not in message")
raise ValueError(f"Segment with type {value!r} is not in message")
return super().index(first_segment, *args)
return super().index(value, *args)
def get(self: TM, type_: str, count: Optional[int] = None) -> TM:
def get(self, type_: str, count: Optional[int] = None) -> Self:
"""获取指定类型的消息段
参数:
type_: 消息段类型
count: 获取个数
返回:
构建的新消息
"""
if count is None:
return self[type_]
@@ -286,9 +332,30 @@ class Message(List[TMS], abc.ABC):
return filtered
def count(self, value: Union[TMS, str]) -> int:
"""计算指定消息段的个数
参数:
value: 消息段或消息段类型
返回:
个数
"""
return len(self[value]) if isinstance(value, str) else super().count(value)
def append(self: TM, obj: Union[str, TMS]) -> TM:
def only(self, value: Union[TMS, str]) -> bool:
"""检查消息中是否仅包含指定消息段
参数:
value: 指定消息段或消息段类型
返回:
是否仅包含指定消息段
"""
if isinstance(value, str):
return all(seg.type == value for seg in self)
return all(seg == value for seg in self)
def append(self, obj: Union[str, TMS]) -> Self:
"""添加一个消息段到消息数组末尾。
参数:
@@ -302,7 +369,7 @@ class Message(List[TMS], abc.ABC):
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
return self
def extend(self: TM, obj: Union[TM, Iterable[TMS]]) -> TM:
def extend(self, obj: Union[Self, Iterable[TMS]]) -> Self:
"""拼接一个消息数组或多个消息段到消息数组末尾。
参数:
@@ -312,18 +379,52 @@ class Message(List[TMS], abc.ABC):
self.append(segment)
return self
def copy(self: TM) -> TM:
def join(self, iterable: Iterable[Union[TMS, Self]]) -> Self:
"""将多个消息连接并将自身作为分割
参数:
iterable: 要连接的消息
返回:
连接后的消息
"""
ret = self.__class__()
for index, msg in enumerate(iterable):
if index != 0:
ret.extend(self)
if isinstance(msg, MessageSegment):
ret.append(msg.copy())
else:
ret.extend(msg.copy())
return ret
def copy(self) -> Self:
"""深拷贝消息"""
return deepcopy(self)
def include(self, *types: str) -> Self:
"""过滤消息
参数:
types: 包含的消息段类型
返回:
新构造的消息
"""
return self.__class__(seg for seg in self if seg.type in types)
def exclude(self, *types: str) -> Self:
"""过滤消息
参数:
types: 不包含的消息段类型
返回:
新构造的消息
"""
return self.__class__(seg for seg in self if seg.type not in types)
def extract_plain_text(self) -> str:
"""提取消息内纯文本消息"""
return "".join(str(seg) for seg in self if seg.is_text())
__autodoc__ = {
"MessageSegment.__str__": True,
"MessageSegment.__add__": True,
"Message.__getitem__": True,
"Message._construct": True,
}

View File

@@ -119,10 +119,9 @@ class MessageTemplate(Formatter, Generic[TF]):
) -> Tuple[TF, int]:
results: List[Any] = [self.factory()]
for (literal_text, field_name, format_spec, conversion) in self.parse(
for literal_text, field_name, format_spec, conversion in self.parse(
format_string
):
# output the literal text
if literal_text:
results.append(literal_text)

View File

@@ -1,6 +1,6 @@
import abc
import asyncio
from contextlib import asynccontextmanager
from contextlib import AsyncExitStack, asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from nonebot.log import logger
@@ -8,8 +8,12 @@ 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
from nonebot.typing import (
T_DependencyCache,
T_BotConnectionHook,
T_BotDisconnectionHook,
)
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
@@ -135,13 +139,15 @@ class Driver(abc.ABC):
self._bots[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(
map(
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
self._bot_connection_hook,
dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
if coros := [
run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
)
if coros:
for hook in self._bot_connection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:
@@ -158,13 +164,15 @@ class Driver(abc.ABC):
del self._bots[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(
map(
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
self._bot_disconnection_hook,
dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack:
if coros := [
run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
)
)
if coros:
for hook in self._bot_disconnection_hook
]:
try:
await asyncio.gather(*coros)
except Exception as e:

View File

@@ -1,5 +1,6 @@
from types import ModuleType
from contextvars import ContextVar
from typing_extensions import Self
from datetime import datetime, timedelta
from contextlib import AsyncExitStack, contextmanager
from typing import (
@@ -10,6 +11,7 @@ from typing import (
Union,
TypeVar,
Callable,
ClassVar,
Iterable,
NoReturn,
Optional,
@@ -19,7 +21,7 @@ from typing import (
from nonebot.log import logger
from nonebot.internal.rule import Rule
from nonebot.dependencies import Dependent
from nonebot.internal.permission import USER, User, Permission
from nonebot.internal.permission import User, Permission
from nonebot.internal.adapter import (
Bot,
Event,
@@ -80,7 +82,7 @@ class MatcherMeta(type):
def __repr__(self) -> str:
return (
f"Matcher(type={self.type!r}"
f"{self.__name__}(type={self.type!r}"
+ (f", module={self.module_name}" if self.module_name else "")
+ ")"
)
@@ -89,38 +91,38 @@ class MatcherMeta(type):
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
plugin: Optional["Plugin"] = None
plugin: ClassVar[Optional["Plugin"]] = None
"""事件响应器所在插件"""
module: Optional[ModuleType] = None
module: ClassVar[Optional[ModuleType]] = None
"""事件响应器所在插件模块"""
plugin_name: Optional[str] = None
plugin_name: ClassVar[Optional[str]] = None
"""事件响应器所在插件名"""
module_name: Optional[str] = None
module_name: ClassVar[Optional[str]] = None
"""事件响应器所在点分割插件模块路径"""
type: str = ""
type: ClassVar[str] = ""
"""事件响应器类型"""
rule: Rule = Rule()
rule: ClassVar[Rule] = Rule()
"""事件响应器匹配规则"""
permission: Permission = Permission()
permission: ClassVar[Permission] = Permission()
"""事件响应器触发权限"""
handlers: List[Dependent[Any]] = []
"""事件响应器拥有的事件处理函数列表"""
priority: int = 1
priority: ClassVar[int] = 1
"""事件响应器优先级"""
block: bool = False
"""事件响应器是否阻止事件传播"""
temp: bool = False
temp: ClassVar[bool] = False
"""事件响应器是否为临时"""
expire_time: Optional[datetime] = None
expire_time: ClassVar[Optional[datetime]] = None
"""事件响应器过期时间点"""
_default_state: T_State = {}
_default_state: ClassVar[T_State] = {}
"""事件响应器默认状态"""
_default_type_updater: Optional[Dependent[str]] = None
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
"""事件响应器类型更新函数"""
_default_permission_updater: Optional[Dependent[Permission]] = None
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES = (
@@ -139,7 +141,7 @@ class Matcher(metaclass=MatcherMeta):
def __repr__(self) -> str:
return (
f"Matcher(type={self.type!r}"
f"{self.__class__.__name__}(type={self.type!r}"
+ (f", module={self.module_name}" if self.module_name else "")
+ ")"
)
@@ -163,7 +165,7 @@ class Matcher(metaclass=MatcherMeta):
default_permission_updater: Optional[
Union[T_PermissionUpdater, Dependent[Permission]]
] = None,
) -> Type["Matcher"]:
) -> Type[Self]:
"""
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
@@ -184,8 +186,8 @@ class Matcher(metaclass=MatcherMeta):
Type[Matcher]: 新的事件响应器类
"""
NewMatcher = type(
"Matcher",
(Matcher,),
cls.__name__,
(cls,),
{
"plugin": plugin,
"module": module,
@@ -377,7 +379,6 @@ class Matcher(metaclass=MatcherMeta):
_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]
new_handler = Dependent(
@@ -425,7 +426,6 @@ class Matcher(metaclass=MatcherMeta):
_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]
new_handler = Dependent(
@@ -645,23 +645,44 @@ class Matcher(metaclass=MatcherMeta):
"""阻止事件传播"""
self.block = True
async def update_type(self, bot: Bot, event: Event) -> str:
async def update_type(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> str:
updater = self.__class__._default_type_updater
return (
await updater(bot=bot, event=event, state=self.state, matcher=self)
await updater(
bot=bot,
event=event,
state=self.state,
matcher=self,
stack=stack,
dependency_cache=dependency_cache,
)
if updater
else "message"
)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
async def update_permission(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> 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)
return await updater(
bot=bot,
event=event,
state=self.state,
matcher=self,
stack=stack,
dependency_cache=dependency_cache,
)
return Permission(User.from_event(event, perm=self.permission))
async def resolve_reject(self):
handler = current_handler.get()
@@ -733,10 +754,12 @@ class Matcher(metaclass=MatcherMeta):
except RejectedException:
await self.resolve_reject()
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(
bot, event, stack, dependency_cache
)
Matcher.new(
self.new(
type_,
Rule(),
permission,
@@ -752,10 +775,12 @@ class Matcher(metaclass=MatcherMeta):
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(
bot, event, stack, dependency_cache
)
Matcher.new(
self.new(
type_,
Rule(),
permission,

View File

@@ -1,8 +1,10 @@
import asyncio
import inspect
from typing_extensions import Annotated
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
from pydantic.typing import get_args, get_origin
from pydantic.fields import Required, Undefined, ModelField
from nonebot.dependencies.utils import check_field_type
@@ -69,7 +71,12 @@ def Depends(
class DependParam(Param):
"""子依赖参数"""
"""子依赖注入参数
本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。
本注入应该具有最高优先级,因此应该在其他参数之前检查。
"""
def __repr__(self) -> str:
return f"Depends({self.extra['dependent']})"
@@ -78,20 +85,32 @@ class DependParam(Param):
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
type_annotation, depends_inner = param.annotation, None
if get_origin(param.annotation) is Annotated:
type_annotation, *extra_args = get_args(param.annotation)
depends_inner = next(
(x for x in extra_args if isinstance(x, DependsInner)), None
)
depends_inner = (
param.default if isinstance(param.default, DependsInner) else depends_inner
)
if depends_inner is None:
return
dependency: T_Handler
if param.default.dependency is None:
assert param.annotation is not param.empty, "Dependency cannot be empty"
dependency = param.annotation
if depends_inner.dependency is None:
assert (
type_annotation is not inspect.Signature.empty
), "Dependency cannot be empty"
dependency = type_annotation
else:
dependency = param.default.dependency
dependency = depends_inner.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=allow_types,
)
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
return cls(Required, use_cache=depends_inner.use_cache, dependent=sub_dependent)
@classmethod
def _check_parameterless(
@@ -154,7 +173,12 @@ class DependParam(Param):
class BotParam(Param):
"""{ref}`nonebot.adapters.Bot` 参数"""
"""{ref}`nonebot.adapters.Bot` 注入参数
本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Bot` 及其子类或 `None` 的参数。
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
"""
def __repr__(self) -> str:
return (
@@ -173,7 +197,7 @@ class BotParam(Param):
) -> Optional["BotParam"]:
from nonebot.adapters import Bot
if param.default == param.empty:
# param type is Bot(s) or subclass(es) of Bot or None
if generic_check_issubclass(param.annotation, Bot):
checker: Optional[ModelField] = None
if param.annotation is not Bot:
@@ -186,6 +210,7 @@ class BotParam(Param):
required=True,
)
return cls(Required, checker=checker)
# legacy: param is named "bot" and has no type annotation
elif param.annotation == param.empty and param.name == "bot":
return cls(Required)
@@ -198,7 +223,12 @@ class BotParam(Param):
class EventParam(Param):
"""{ref}`nonebot.adapters.Event` 参数"""
"""{ref}`nonebot.adapters.Event` 注入参数
本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Event` 及其子类或 `None` 的参数。
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
"""
def __repr__(self) -> str:
return (
@@ -217,7 +247,7 @@ class EventParam(Param):
) -> Optional["EventParam"]:
from nonebot.adapters import Event
if param.default == param.empty:
# param type is Event(s) or subclass(es) of Event or None
if generic_check_issubclass(param.annotation, Event):
checker: Optional[ModelField] = None
if param.annotation is not Event:
@@ -230,6 +260,7 @@ class EventParam(Param):
required=True,
)
return cls(Required, checker=checker)
# legacy: param is named "event" and has no type annotation
elif param.annotation == param.empty and param.name == "event":
return cls(Required)
@@ -242,7 +273,12 @@ class EventParam(Param):
class StateParam(Param):
"""事件处理状态参数"""
"""事件处理状态注入参数
本注入解析所有类型为 `T_State` 的参数。
为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。
"""
def __repr__(self) -> str:
return "StateParam()"
@@ -251,9 +287,10 @@ class StateParam(Param):
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["StateParam"]:
if param.default == param.empty:
# param type is T_State
if param.annotation is T_State:
return cls(Required)
# legacy: param is named "state" and has no type annotation
elif param.annotation == param.empty and param.name == "state":
return cls(Required)
@@ -262,7 +299,12 @@ class StateParam(Param):
class MatcherParam(Param):
"""事件响应器实例参数"""
"""事件响应器实例注入参数
本注入解析所有类型为且仅为 {ref}`nonebot.matcher.Matcher` 及其子类或 `None` 的参数。
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
"""
def __repr__(self) -> str:
return "MatcherParam()"
@@ -273,9 +315,11 @@ class MatcherParam(Param):
) -> Optional["MatcherParam"]:
from nonebot.matcher import Matcher
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and param.name == "matcher"
):
# param type is Matcher(s) or subclass(es) of Matcher or None
if generic_check_issubclass(param.annotation, Matcher):
return cls(Required)
# legacy: param is named "matcher" and has no type annotation
elif param.annotation == param.empty and param.name == "matcher":
return cls(Required)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
@@ -294,22 +338,28 @@ class ArgInner:
def Arg(key: Optional[str] = None) -> Any:
"""`got` 的 Arg 参数消息"""
"""Arg 参数消息"""
return ArgInner(key, "message")
def ArgStr(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息文本"""
"""Arg 参数消息文本"""
return ArgInner(key, "str") # type: ignore
def ArgPlainText(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息纯文本"""
"""Arg 参数消息纯文本"""
return ArgInner(key, "plaintext") # type: ignore
class ArgParam(Param):
"""`got` 的 Arg 参数"""
"""Arg 注入参数
本注入解析事件响应器操作 `got` 所获取的参数。
可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数,
留空则会根据参数名称获取。
"""
def __repr__(self) -> str:
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
@@ -324,7 +374,8 @@ class ArgParam(Param):
)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
key: str = self.extra["key"]
message = matcher.get_arg(key)
if message is None:
return message
if self.extra["type"] == "message":
@@ -336,7 +387,12 @@ class ArgParam(Param):
class ExceptionParam(Param):
"""`run_postprocessor` 的异常参数"""
"""{ref}`nonebot.message.run_postprocessor` 的异常注入参数
本注入解析所有类型为 `Exception` 或 `None` 的参数。
为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。
"""
def __repr__(self) -> str:
return "ExceptionParam()"
@@ -345,9 +401,11 @@ class ExceptionParam(Param):
def _check_param(
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and param.name == "exception"
):
# param type is Exception(s) or subclass(es) of Exception or None
if generic_check_issubclass(param.annotation, Exception):
return cls(Required)
# legacy: param is named "exception" and has no type annotation
elif param.annotation == param.empty and param.name == "exception":
return cls(Required)
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
@@ -355,7 +413,12 @@ class ExceptionParam(Param):
class DefaultParam(Param):
"""默认值参数"""
"""默认值注入参数
本注入解析所有剩余未能解析且具有默认值的参数。
本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。
"""
def __repr__(self) -> str:
return f"DefaultParam(default={self.default!r})"

View File

@@ -1,4 +1,5 @@
import asyncio
from typing_extensions import Self
from contextlib import AsyncExitStack
from typing import Set, Tuple, Union, NoReturn, Optional
@@ -57,7 +58,7 @@ class Permission:
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足某个权限
"""检查是否满足某个权限
参数:
bot: Bot 对象
@@ -109,7 +110,7 @@ class Permission:
class User:
"""检查当前事件是否属于指定会话
"""检查当前事件是否属于指定会话
参数:
users: 会话 ID 元组
@@ -140,13 +141,47 @@ class User:
session in self.users and (self.perm is None or await self.perm(bot, event))
)
@classmethod
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
if len(perm.checkers) == 1 and isinstance(
user_perm := tuple(perm.checkers)[0].call, cls
):
return user_perm.perm
return perm
@classmethod
def from_event(cls, event: Event, perm: Optional[Permission] = None) -> Self:
"""从事件中获取会话 ID。
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
参数:
event: Event 对象
perm: 需同时满足的权限
"""
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
@classmethod
def from_permission(cls, *users: str, perm: Optional[Permission] = None) -> Self:
"""指定会话与权限。
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
参数:
users: 会话白名单
perm: 需同时满足的权限
"""
return cls(users, perm=perm and cls._clean_permission(perm))
def USER(*users: str, perm: Optional[Permission] = None):
"""匹配当前事件属于指定会话
"""匹配当前事件属于指定会话
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
参数:
user: 会话白名单
perm: 需要同时满足的权限
"""
return Permission(User(users, perm))
return Permission(User.from_permission(*users, perm=perm))

View File

@@ -2,7 +2,7 @@
NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/tutorial/custom-logger)
自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)
以及 [`loguru`][loguru] 文档。
[loguru]: https://github.com/Delgan/loguru

View File

@@ -80,7 +80,10 @@ RUN_POSTPCS_PARAMS = (
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
"""事件预处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。"""
"""事件预处理。
装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。
"""
_event_preprocessors.add(
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
)
@@ -88,7 +91,10 @@ def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
"""事件后处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。"""
"""事件后处理。
装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。
"""
_event_postprocessors.add(
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
)
@@ -96,7 +102,10 @@ def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
"""运行预处理。装饰一个函数,使它在每次事件响应器运行前执行。"""
"""运行预处理。
装饰一个函数,使它在每次事件响应器运行前执行。
"""
_run_preprocessors.add(
Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
)
@@ -104,55 +113,149 @@ def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
"""运行后处理。装饰一个函数,使它在每次事件响应器运行后执行。"""
"""运行后处理。
装饰一个函数,使它在每次事件响应器运行后执行。
"""
_run_postprocessors.add(
Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
)
return func
async def _check_matcher(
Matcher: Type[Matcher],
async def _apply_event_preprocessors(
bot: "Bot",
event: "Event",
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> None:
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
with contextlib.suppress(Exception):
Matcher.destroy()
return
show_log: bool = True,
) -> bool:
"""运行事件预处理。
参数:
bot: Bot 对象
event: Event 对象
state: 会话状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
show_log: 是否显示日志
返回:
是否继续处理事件
"""
if not _event_preprocessors:
return True
if show_log:
logger.debug("Running PreProcessors...")
try:
if not await Matcher.check_perm(
bot, event, stack, dependency_cache
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
return
await asyncio.gather(
*(
run_coro_with_catch(
proc(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _event_preprocessors
)
)
except IgnoredException as e:
logger.opt(colors=True).info(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
)
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
)
return
return False
if Matcher.temp:
with contextlib.suppress(Exception):
Matcher.destroy()
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
return True
async def _run_matcher(
Matcher: Type[Matcher],
async def _apply_event_postprocessors(
bot: "Bot",
event: "Event",
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
show_log: bool = True,
) -> None:
logger.info(f"Event will be handled by {Matcher}")
"""运行事件后处理。
matcher = Matcher()
if coros := [
参数:
bot: Bot 对象
event: Event 对象
state: 会话状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
show_log: 是否显示日志
"""
if not _event_postprocessors:
return
if show_log:
logger.debug("Running PostProcessors...")
try:
await asyncio.gather(
*(
run_coro_with_catch(
proc(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _event_postprocessors
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
async def _apply_run_preprocessors(
bot: "Bot",
event: "Event",
state: T_State,
matcher: Matcher,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""运行事件响应器运行前处理。
参数:
bot: Bot 对象
event: Event 对象
state: 会话状态
matcher: 事件响应器
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否继续处理事件
"""
if not _run_preprocessors:
return True
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try:
await asyncio.gather(
*(
run_coro_with_catch(
proc(
matcher=matcher,
@@ -165,33 +268,46 @@ async def _run_matcher(
(SkippedException,),
)
for proc in _run_preprocessors
]:
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try:
await asyncio.gather(*coros)
)
)
except IgnoredException:
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
return
return False
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 False
return True
async def _apply_run_postprocessors(
bot: "Bot",
event: "Event",
matcher: Matcher,
exception: Optional[Exception] = None,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> None:
"""运行事件响应器运行后处理。
Args:
bot: Bot 对象
event: Event 对象
matcher: 事件响应器
exception: 事件响应器运行异常
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not _run_postprocessors:
return
exception = None
with matcher.ensure_context(bot, event):
try:
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} failed.</bg #f8bbd0></r>"
)
exception = e
if coros := [
await asyncio.gather(
*(
run_coro_with_catch(
proc(
matcher=matcher,
@@ -205,20 +321,158 @@ async def _run_matcher(
(SkippedException,),
)
for proc in _run_postprocessors
]:
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try:
await asyncio.gather(*coros)
)
)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
)
async def _check_matcher(
Matcher: Type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查事件响应器是否符合运行条件。
请注意,过时的事件响应器将被**销毁**。对于未过时的事件响应器,将会一次检查其响应类型、权限和规则。
参数:
Matcher: 要检查的事件响应器
bot: Bot 对象
event: Event 对象
state: 会话状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
bool: 是否符合运行条件
"""
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
with contextlib.suppress(Exception):
Matcher.destroy()
return False
try:
if not await Matcher.check_perm(
bot, event, stack, dependency_cache
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
return False
except Exception as e:
logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
)
return False
return True
async def _run_matcher(
Matcher: Type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> None:
"""运行事件响应器。
临时事件响应器将在运行前被**销毁**。
参数:
Matcher: 事件响应器
bot: Bot 对象
event: Event 对象
state: 会话状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
异常:
StopPropagation: 阻止事件继续传播
"""
logger.info(f"Event will be handled by {Matcher}")
if Matcher.temp:
with contextlib.suppress(Exception):
Matcher.destroy()
matcher = Matcher()
if not await _apply_run_preprocessors(
bot=bot,
event=event,
state=state,
matcher=matcher,
stack=stack,
dependency_cache=dependency_cache,
):
return
exception = None
try:
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} failed.</bg #f8bbd0></r>"
)
exception = e
await _apply_run_postprocessors(
bot=bot,
event=event,
matcher=matcher,
exception=exception,
stack=stack,
dependency_cache=dependency_cache,
)
if matcher.block:
raise StopPropagation
async def check_and_run_matcher(
Matcher: Type[Matcher],
bot: "Bot",
event: "Event",
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> None:
"""检查并运行事件响应器。
参数:
Matcher: 事件响应器
bot: Bot 对象
event: Event 对象
state: 会话状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not await _check_matcher(
Matcher=Matcher,
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
):
return
await _run_matcher(
Matcher=Matcher,
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
async def handle_event(bot: "Bot", event: "Event") -> None:
"""处理一个事件。调用该函数以实现分发事件。
@@ -245,34 +499,15 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
state: Dict[Any, Any] = {}
dependency_cache: T_DependencyCache = {}
# create event scope context
async with AsyncExitStack() as stack:
if coros := [
run_coro_with_catch(
proc(
if not await _apply_event_preprocessors(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _event_preprocessors
]:
try:
if show_log:
logger.debug("Running PreProcessors...")
await asyncio.gather(*coros)
except IgnoredException as e:
logger.opt(colors=True).info(
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
)
return
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>"
)
):
return
# Trie Match
@@ -284,6 +519,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
)
break_flag = False
# iterate through all priority until stop propagation
for priority in sorted(matchers.keys()):
if break_flag:
break
@@ -292,14 +528,12 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
logger.debug(f"Checking for matchers in priority {priority}...")
pending_tasks = [
_check_matcher(
check_and_run_matcher(
matcher, bot, event, state.copy(), stack, dependency_cache
)
for matcher in matchers[priority]
]
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
for result in results:
if not isinstance(result, Exception):
continue
@@ -314,24 +548,4 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
if show_log:
logger.debug("Checking for matchers completed")
if coros := [
run_coro_with_catch(
proc(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
),
(SkippedException,),
)
for proc in _event_postprocessors
]:
try:
if show_log:
logger.debug("Running PostProcessors...")
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
await _apply_event_postprocessors(bot, event, state, stack, dependency_cache)

View File

@@ -5,8 +5,7 @@ FrontMatter:
description: nonebot.params 模块
"""
import warnings
from typing import Any, Dict, List, Tuple, Union, Optional
from typing import Any, Dict, List, Match, Tuple, Union, Optional
from nonebot.typing import T_State
from nonebot.matcher import Matcher
@@ -25,20 +24,18 @@ from nonebot.internal.params import MatcherParam as MatcherParam
from nonebot.internal.params import ExceptionParam as ExceptionParam
from nonebot.consts import (
CMD_KEY,
REGEX_STR,
PREFIX_KEY,
REGEX_DICT,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
)
@@ -114,6 +111,15 @@ def CommandStart() -> str:
return Depends(_command_start)
def _command_whitespace(state: T_State) -> str:
return state[PREFIX_KEY][CMD_WHITESPACE_KEY]
def CommandWhitespace() -> str:
"""消息命令与参数之间的空白"""
return Depends(_command_whitespace)
def _shell_command_args(state: T_State) -> Any:
return state[SHELL_ARGS] # Namespace or ParserExit
@@ -132,23 +138,17 @@ def ShellCommandArgv() -> Any:
return Depends(_shell_command_argv, use_cache=False)
def _regex_matched(state: T_State) -> str:
def _regex_matched(state: T_State) -> Match[str]:
return state[REGEX_MATCHED]
def RegexMatched() -> str:
def RegexMatched() -> Match[str]:
"""正则匹配结果"""
warnings.warn(
'"RegexMatched()" will be changed to "re.Match" object, '
'use "RegexStr()" instead. '
"See https://github.com/nonebot/nonebot2/pull/1453 .",
DeprecationWarning,
)
return Depends(_regex_matched, use_cache=False)
def _regex_str(state: T_State) -> str:
return state[REGEX_STR]
return _regex_matched(state).group()
def RegexStr() -> str:
@@ -157,7 +157,7 @@ def RegexStr() -> str:
def _regex_group(state: T_State) -> Tuple[Any, ...]:
return state[REGEX_GROUP]
return _regex_matched(state).groups()
def RegexGroup() -> Tuple[Any, ...]:
@@ -166,7 +166,7 @@ def RegexGroup() -> Tuple[Any, ...]:
def _regex_dict(state: T_State) -> Dict[str, Any]:
return state[REGEX_DICT]
return _regex_matched(state).groupdict()
def RegexDict() -> Dict[str, Any]:

View File

@@ -9,13 +9,16 @@ from pathlib import Path
from types import ModuleType
from typing import Set, Union, Iterable, Optional
import tomlkit
from nonebot.utils import path_to_module_name
from .plugin import Plugin
from .manager import PluginManager
from . import _managers, get_plugin, _module_name_to_plugin_name
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
try: # pragma: py-gte-311
import tomllib # pyright: ignore[reportMissingImports]
except ModuleNotFoundError: # pragma: py-lt-311
import tomli as tomllib
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
@@ -108,7 +111,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
```
"""
with open(file_path, "r", encoding=encoding) as f:
data = tomlkit.parse(f.read()) # type: ignore
data = tomllib.loads(f.read())
nonebot_data = data.get("tool", {}).get("nonebot")
if nonebot_data is None:
@@ -158,11 +161,19 @@ def require(name: str) -> ModuleType:
RuntimeError: 插件无法加载
"""
plugin = get_plugin(_module_name_to_plugin_name(name))
# if plugin not loaded
if not plugin:
# plugin already declared
if manager := _find_manager_by_name(name):
plugin = manager.load_plugin(name)
# plugin not declared, try to declare and load it
else:
# clear current plugin chain, ensure plugin loaded in a new context
_t = _current_plugin_chain.set(())
try:
plugin = load_plugin(name)
finally:
_current_plugin_chain.reset(_t)
if not plugin:
raise RuntimeError(f'Cannot load plugin "{name}"!')
return plugin.module

View File

@@ -148,14 +148,21 @@ class PluginManager:
else:
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
logger.opt(colors=True).success(
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
)
if (plugin := getattr(module, "__plugin__", None)) is None:
if (
plugin := getattr(module, "__plugin__", None)
) is None or not isinstance(plugin, Plugin):
raise RuntimeError(
f"Module {module.__name__} is not loaded as a plugin! "
"Make sure not to import it before loading."
)
logger.opt(colors=True).success(
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
+ (
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
if plugin.module_name != plugin.name
else ""
)
)
return plugin
except Exception as e:
logger.opt(colors=True, exception=e).error(
@@ -221,6 +228,7 @@ class PluginLoader(SourceFileLoader):
# detect parent plugin before entering current plugin context
parent_plugins = _current_plugin_chain.get()
for pre_plugin in reversed(parent_plugins):
# ensure parent plugin is declared before current plugin
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
plugin.parent_plugin = pre_plugin
pre_plugin.sub_plugins.add(plugin)

View File

@@ -28,16 +28,44 @@ from nonebot.rule import (
shell_command,
)
from .plugin import Plugin
from . import get_plugin_by_module_name
from .manager import _current_plugin_chain
def _store_matcher(matcher: Type[Matcher]) -> None:
# only store the matcher defined in the plugin
if plugins := _current_plugin_chain.get():
plugins[-1].matcher.add(matcher)
def store_matcher(matcher: Type[Matcher]) -> None:
"""存储一个事件响应器到插件。
参数:
matcher: 事件响应器
"""
# only store the matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
plugin_chain[-1].matcher.add(matcher)
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
"""获取事件响应器定义所在插件。
参数:
depth: 调用栈深度
"""
# matcher defined when plugin loading
if plugin_chain := _current_plugin_chain.get():
return plugin_chain[-1]
# matcher defined when plugin running
if module := get_matcher_module(depth + 1):
if plugin := get_plugin_by_module_name(module.__name__):
return plugin
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
"""获取事件响应器定义所在模块。
参数:
depth: 调用栈深度
"""
current_frame = inspect.currentframe()
if current_frame is None:
return None
@@ -71,7 +99,6 @@ def on(
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
plugin_chain = _current_plugin_chain.get()
matcher = Matcher.new(
type,
Rule() & rule,
@@ -81,29 +108,20 @@ def on(
priority=priority,
block=block,
handlers=handlers,
plugin=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
plugin=get_matcher_plugin(_depth + 1),
module=get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
store_matcher(matcher)
return matcher
def on_metaevent(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
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]:
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
"""注册一个元事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -111,36 +129,10 @@ def on_metaevent(
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=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
def on_message(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
*,
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]:
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
"""注册一个消息事件响应器。
参数:
@@ -153,39 +145,16 @@ def on_message(
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=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
kwargs.setdefault("block", True)
return on("message", *args, **kwargs, _depth=_depth + 1)
def on_notice(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
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]:
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
"""注册一个通知事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -193,39 +162,15 @@ def on_notice(
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=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
return on("notice", *args, **kwargs, _depth=_depth + 1)
def on_request(
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
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]:
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
"""注册一个请求事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -233,22 +178,7 @@ def on_request(
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=plugin_chain[-1] if plugin_chain else None,
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
return on("request", *args, **kwargs, _depth=_depth + 1)
def on_startswith(
@@ -349,6 +279,7 @@ def on_command(
cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
@@ -360,6 +291,7 @@ def on_command(
cmd: 指定命令内容
rule: 事件响应规则
aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
@@ -370,9 +302,11 @@ def on_command(
"""
commands = {cmd} | (aliases or set())
block = kwargs.pop("block", False)
kwargs.setdefault("block", False)
return on_message(
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
command(*commands, force_whitespace=force_whitespace) & rule,
**kwargs,
_depth=_depth + 1,
)
@@ -518,6 +452,7 @@ class CommandGroup(_Group):
参数:
cmd: 指定命令内容
aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
@@ -587,6 +522,7 @@ class MatcherGroup(_Group):
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -622,6 +558,7 @@ class MatcherGroup(_Group):
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -639,6 +576,7 @@ class MatcherGroup(_Group):
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
@@ -736,6 +674,7 @@ class MatcherGroup(_Group):
self,
cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
**kwargs,
) -> Type[Matcher]:
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
@@ -745,6 +684,7 @@ class MatcherGroup(_Group):
参数:
cmd: 指定命令内容
aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
@@ -755,7 +695,9 @@ class MatcherGroup(_Group):
state: 默认 state
"""
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_command(cmd, aliases=aliases, **final_kwargs)
matcher = on_command(
cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs
)
self.matchers.append(matcher)
return matcher

View File

@@ -1,4 +1,5 @@
import re
from types import ModuleType
from datetime import datetime, timedelta
from typing import Set, List, Type, Tuple, Union, Optional
@@ -9,6 +10,11 @@ from nonebot.dependencies import Dependent
from nonebot.rule import Rule, ArgumentParser
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
from .plugin import Plugin
def store_matcher(matcher: Type[Matcher]) -> None: ...
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ...
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ...
def on(
type: str = "",
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
@@ -23,6 +29,7 @@ def on(
) -> Type[Matcher]: ...
def on_metaevent(
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
@@ -44,6 +51,7 @@ def on_message(
) -> Type[Matcher]: ...
def on_notice(
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
@@ -54,6 +62,7 @@ def on_notice(
) -> Type[Matcher]: ...
def on_request(
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
@@ -117,6 +126,7 @@ def on_command(
cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
force_whitespace: Optional[Union[str, bool]] = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
@@ -186,6 +196,7 @@ class CommandGroup:
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
force_whitespace: Optional[Union[str, bool]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
@@ -241,6 +252,7 @@ class MatcherGroup:
self,
*,
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]] = ...,
@@ -264,6 +276,7 @@ class MatcherGroup:
self,
*,
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]] = ...,
@@ -275,6 +288,7 @@ class MatcherGroup:
self,
*,
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]] = ...,
@@ -341,6 +355,7 @@ class MatcherGroup:
self,
cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
force_whitespace: Optional[Union[str, bool]] = ...,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,

View File

@@ -1,9 +1,11 @@
"""本模块定义插件对象
"""本模块定义插件相关信息
FrontMatter:
sidebar_position: 3
description: nonebot.plugin.plugin 模块
"""
import contextlib
from types import ModuleType
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
@@ -11,11 +13,11 @@ from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
from pydantic import BaseModel
from nonebot.matcher import Matcher
# FIXME: backport for nonebug
from . import _plugins as plugins # nopycln: import
from nonebot.utils import resolve_dot_notation
if TYPE_CHECKING:
from nonebot.adapters import Adapter
from .manager import PluginManager
@@ -24,14 +26,39 @@ class PluginMetadata:
"""插件元信息,由插件编写者提供"""
name: str
"""插件可阅读名称"""
"""插件名称"""
description: str
"""插件功能介绍"""
usage: str
"""插件使用方法"""
type: Optional[str] = None
"""插件类型,用于商店分类"""
homepage: Optional[str] = None
"""插件主页"""
config: Optional[Type[BaseModel]] = None
"""插件配置项"""
supported_adapters: Optional[Set[str]] = None
"""插件支持的适配器模块路径
格式为 `<module>[:<Adapter>]``~` 为 `nonebot.adapters.` 的缩写。
`None` 表示支持**所有适配器**。
"""
extra: Dict[Any, Any] = field(default_factory=dict)
"""插件额外信息,可由插件编写者自由扩展定义"""
def get_supported_adapters(self) -> Optional[Set[Type["Adapter"]]]:
"""获取当前已安装的插件支持适配器类列表"""
if self.supported_adapters is None:
return None
adapters = set()
for adapter in self.supported_adapters:
with contextlib.suppress(ModuleNotFoundError, AttributeError):
adapters.add(
resolve_dot_notation(adapter, "Adapter", "nonebot.adapters.")
)
return adapters
@dataclass(eq=False)
@@ -47,7 +74,7 @@ class Plugin:
manager: "PluginManager"
"""导入该插件的插件管理器"""
matcher: Set[Type[Matcher]] = field(default_factory=set)
"""插件定义的 `Matcher`"""
"""插件加载时定义的 `Matcher`"""
parent_plugin: Optional["Plugin"] = None
"""父插件"""
sub_plugins: Set["Plugin"] = field(default_factory=set)

View File

@@ -11,6 +11,7 @@ FrontMatter:
import re
import shlex
from argparse import Action
from gettext import gettext
from argparse import ArgumentError
from contextvars import ContextVar
from itertools import chain, product
@@ -39,24 +40,22 @@ 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, CommandArg, CommandWhitespace
from nonebot.consts import (
CMD_KEY,
REGEX_STR,
PREFIX_KEY,
REGEX_DICT,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
)
T = TypeVar("T")
@@ -66,8 +65,9 @@ CMD_RESULT = TypedDict(
{
"command": Optional[Tuple[str, ...]],
"raw_command": Optional[str],
"command_arg": Optional[Message[MessageSegment]],
"command_arg": Optional[Message],
"command_start": Optional[str],
"command_whitespace": Optional[str],
},
)
@@ -91,7 +91,11 @@ 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, command_start=None
command=None,
raw_command=None,
command_arg=None,
command_start=None,
command_whitespace=None,
)
state[PREFIX_KEY] = prefix
if event.get_type() != "message":
@@ -106,9 +110,23 @@ class TrieRule:
prefix[RAW_CMD_KEY] = 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())
# check whitespace
arg_str = segment_text[len(pf.key) :]
arg_str_stripped = arg_str.lstrip()
has_arg = arg_str_stripped or msg
if (
has_arg
and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0
):
prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len]
# construct command arg
if arg_str_stripped:
new_message = msg.__class__(arg_str_stripped)
for new_segment in reversed(new_message):
msg.insert(0, new_segment)
prefix[CMD_ARG_KEY] = msg
@@ -144,8 +162,6 @@ class StartswithRule:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
@@ -201,8 +217,6 @@ class EndswithRule:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
@@ -258,8 +272,6 @@ class FullmatchRule:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
@@ -310,8 +322,6 @@ class KeywordsRule:
return hash(frozenset(self.keywords))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
try:
text = event.get_plaintext()
except Exception:
@@ -339,12 +349,18 @@ class CommandRule:
参数:
cmds: 指定命令元组列表
force_whitespace: 是否强制命令后必须有指定空白符
"""
__slots__ = ("cmds",)
__slots__ = ("cmds", "force_whitespace")
def __init__(self, cmds: List[Tuple[str, ...]]):
def __init__(
self,
cmds: List[Tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]] = None,
):
self.cmds = tuple(cmds)
self.force_whitespace = force_whitespace
def __repr__(self) -> str:
return f"Command(cmds={self.cmds})"
@@ -357,11 +373,25 @@ class CommandRule:
def __hash__(self) -> int:
return hash((frozenset(self.cmds),))
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
return cmd in self.cmds
async def __call__(
self,
cmd: Optional[Tuple[str, ...]] = Command(),
cmd_arg: Optional[Message] = CommandArg(),
cmd_whitespace: Optional[str] = CommandWhitespace(),
) -> bool:
if cmd not in self.cmds:
return False
if self.force_whitespace is None or not cmd_arg:
return True
if isinstance(self.force_whitespace, str):
return self.force_whitespace == cmd_whitespace
return self.force_whitespace == (cmd_whitespace is not None)
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
def command(
*cmds: Union[str, Tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]] = None,
) -> Rule:
"""匹配消息命令。
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
@@ -373,6 +403,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
参数:
cmds: 命令文本或命令元组
force_whitespace: 是否强制命令后必须有指定空白符
用法:
使用默认 `command_start`, `command_sep` 配置
@@ -404,7 +435,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
)
return Rule(CommandRule(commands))
return Rule(CommandRule(commands, force_whitespace))
class ArgumentParser(ArgParser):
@@ -418,16 +449,39 @@ class ArgumentParser(ArgParser):
if TYPE_CHECKING:
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]] = ...
) -> Namespace:
def parse_known_args(
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: None = None,
) -> Tuple[Namespace, List[Union[str, MessageSegment]]]:
...
@overload
def parse_known_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
) -> Tuple[T, List[Union[str, MessageSegment]]]:
...
@overload
def parse_known_args(
self, *, namespace: T
) -> Tuple[T, List[Union[str, MessageSegment]]]:
...
def parse_known_args(
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: Optional[T] = None,
) -> Tuple[Union[Namespace, T], List[Union[str, MessageSegment]]]:
...
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: None
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: None = None,
) -> Namespace:
... # type: ignore[misc]
...
@overload
def parse_args(
@@ -435,12 +489,20 @@ class ArgumentParser(ArgParser):
) -> T:
...
@overload
def parse_args(self, *, namespace: T) -> T:
...
def parse_args(
self,
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: Optional[T] = None,
) -> Union[Namespace, T]:
...
result, argv = self.parse_known_args(args, namespace)
if argv:
msg = gettext("unrecognized arguments: %s")
self.error(msg % " ".join(map(str, argv)))
return cast(Union[Namespace, T], result)
def _parse_optional(
self, arg_string: Union[str, MessageSegment]
@@ -509,7 +571,7 @@ class ShellCommandRule:
try:
args = self.parser.parse_args(state[SHELL_ARGV])
state[SHELL_ARGS] = args
except ArgumentError as e:
except ArgumentError as e: # pragma: py-gte-39
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
except ParserExit as e:
state[SHELL_ARGS] = e
@@ -609,17 +671,12 @@ class RegexRule:
return hash((self.regex, self.flags))
async def __call__(self, event: Event, state: T_State) -> bool:
if event.get_type() != "message":
return False
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_STR] = matched.group()
state[REGEX_GROUP] = matched.groups()
state[REGEX_DICT] = matched.groupdict()
state[REGEX_MATCHED] = matched
return True
else:
return False

View File

@@ -9,8 +9,10 @@ import re
import json
import asyncio
import inspect
import importlib
import dataclasses
from pathlib import Path
from contextvars import copy_context
from functools import wraps, partial
from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, get_args, get_origin
@@ -57,7 +59,7 @@ def generic_check_issubclass(
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
特别的,如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
则会检查其中的类型是否是 class_or_tuple 中一个类型子类。(None 会被忽略)
则会检查其中的所有类型是否是 class_or_tuple 中一个类型子类None
"""
try:
return issubclass(cls, class_or_tuple)
@@ -110,7 +112,8 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
loop = asyncio.get_running_loop()
pfunc = partial(call, *args, **kwargs)
result = await loop.run_in_executor(None, pfunc)
context = copy_context()
result = await loop.run_in_executor(None, partial(context.run, pfunc))
return result
return _wrapper
@@ -135,6 +138,7 @@ async def run_sync_ctx_manager(
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: None = None,
) -> Union[T, None]:
...
@@ -167,15 +171,32 @@ def get_name(obj: Any) -> str:
def path_to_module_name(path: Path) -> str:
rel_path = path.resolve().relative_to(Path(".").resolve())
"""转换路径为模块名"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == "__init__":
return ".".join(rel_path.parts[:-1])
else:
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
def resolve_dot_notation(
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
) -> Any:
"""解析并导入点分表示法的对象"""
modulename, _, cls = obj_str.partition(":")
if default_prefix is not None and modulename.startswith("~"):
modulename = default_prefix + modulename[1:]
module = importlib.import_module(modulename)
if not cls:
return getattr(module, default_attr)
instance = module
for attr_str in cls.split("."):
instance = getattr(instance, attr_str)
return instance
class DataclassEncoder(json.JSONEncoder):
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
"""在JSON序列化 {ref}`nonebot.adapters.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
@overrides(json.JSONEncoder)
def default(self, o):

View File

@@ -1,5 +1,5 @@
<p align="center">
<a href="https://v2.nonebot.dev/"><img src="https://raw.githubusercontent.com/nonebot/nonebot2/master/docs/.vuepress/public/logo.png" width="200" height="200" alt="nonebot"></a>
<a href="https://nonebot.dev/"><img src="https://nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
</p>
<div align="center">

1465
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
[tool.poetry]
name = "nonebot2"
version = "2.0.0rc3"
version = "2.0.0"
description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
readme = "README.md"
homepage = "https://v2.nonebot.dev/"
homepage = "https://nonebot.dev/"
repository = "https://github.com/nonebot/nonebot2"
documentation = "https://v2.nonebot.dev/"
documentation = "https://nonebot.dev/"
keywords = ["bot", "qq", "qqbot", "mirai", "coolq"]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -21,18 +21,23 @@ packages = [
]
include = ["nonebot/py.typed"]
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
"Changelog" = "https://nonebot.dev/changelog"
"Funding" = "https://afdian.net/@nonebot"
[tool.poetry.dependencies]
python = "^3.8"
yarl = "^1.7.2"
loguru = "^0.6.0"
pygtrie = "^2.4.1"
tomlkit = ">=0.10.0,<1.0.0"
typing-extensions = ">=3.10.0,<5.0.0"
loguru = ">=0.6.0,<1.0.0"
typing-extensions = ">=4.0.0,<5.0.0"
tomli = { version = "^2.0.1", python = "<3.11" }
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
websockets = { version = "^10.0", optional = true }
websockets = { version = ">=10.0", optional = true }
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
fastapi = { version = ">=0.87.0,!=0.89.0,<1.0.0", optional = true }
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
@@ -40,18 +45,19 @@ uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true
[tool.poetry.group.dev.dependencies]
pycln = "^2.1.2"
isort = "^5.10.1"
black = "^22.1.0"
black = "^23.1.0"
nonemoji = "^0.1.2"
pre-commit = "^2.16.0"
pre-commit = "^3.0.0"
[tool.poetry.group.test.dependencies]
nonebug = "^0.3.0"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.20.0"
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
pytest-asyncio = "^0.21.0"
coverage-conditional-plugin = "^0.8.0"
[tool.poetry.group.docs.dependencies]
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
nb-autodoc = "^1.0.0a5"
[tool.poetry.extras]
httpx = ["httpx"]
@@ -63,7 +69,7 @@ all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "--cov=nonebot --cov-report=term-missing"
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
filterwarnings = [
"error",
"ignore::DeprecationWarning",

View File

@@ -1,9 +1,25 @@
[run]
plugins =
coverage_conditional_plugin
[report]
exclude_lines =
pragma: no cover
def __repr__
def __str__
pragma: no cover
if TYPE_CHECKING:
@(typing\.)?overload
if (typing\.)?TYPE_CHECKING( is True)?:
@(abc\.)?abstractmethod
raise NotImplementedError
\.\.\.
pass
if __name__ == .__main__.:
[coverage_conditional_plugin]
rules =
"sys_platform != 'win32'": py-win32
"sys_platform != 'linux'": py-linux
"sys_platform != 'darwin'": py-darwin
"sys_version_info < (3, 9)": py-gte-39
"sys_version_info < (3, 11)": py-gte-311
"sys_version_info >= (3, 11)": py-lt-311

View File

@@ -1,21 +1,30 @@
import os
from pathlib import Path
from typing import TYPE_CHECKING, Set
import pytest
from nonebug import NONEBOT_INIT_KWARGS
import nonebot
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
os.environ["CONFIG_OVERRIDE"] = "new"
if TYPE_CHECKING:
from nonebot.plugin import Plugin
@pytest.fixture
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
import nonebot
def pytest_configure(config: pytest.Config) -> None:
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
@pytest.fixture(scope="session", autouse=True)
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
# preload global plugins
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
@pytest.fixture
def load_example(nonebug_init: None) -> Set["Plugin"]:
import nonebot
return nonebot.load_plugins(str(Path(__file__).parent / "examples"))
@pytest.fixture(scope="session", autouse=True)
def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]:
# preload builtin plugins
return nonebot.load_builtin_plugins("echo", "single_session")

0
tests/dynamic/path.py Normal file
View File

View File

View File

0
tests/dynamic/simple.py Normal file
View File

View File

@@ -1,29 +0,0 @@
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.matcher import Matcher
from nonebot.adapters import Message
from nonebot.params import Arg, CommandArg, ArgPlainText
weather = on_command("weather", rule=to_me(), aliases={"天气", "天气预报"}, priority=5)
@weather.handle()
async def handle_first_receive(matcher: Matcher, args: Message = CommandArg()):
plain_text = args.extract_plain_text() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
if plain_text:
matcher.set_arg("city", args) # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(city: Message = Arg(), city_name: str = ArgPlainText("city")):
if city_name not in ["北京", "上海"]: # 如果参数不符合要求,则提示用户重新输入
# 可以使用平台的 Message 类直接构造模板消息
await weather.reject(city.template("你想查询的城市 {city} 暂不支持,请重新输入!"))
city_weather = await get_weather(city_name)
await weather.finish(city_weather)
# 在这里编写获取天气信息的函数
async def get_weather(city: str) -> str:
return f"{city}的天气是..."

View File

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

View File

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

View File

@@ -90,3 +90,6 @@ async def overload(event: FakeEvent):
@test_overload.handle()
async def finish():
await test_overload.finish()
test_destroy = on_message()

View File

@@ -1,5 +1,6 @@
from pydantic import BaseModel
from nonebot.adapters import Adapter
from nonebot.plugin import PluginMetadata
@@ -7,10 +8,17 @@ class Config(BaseModel):
custom: str = ""
class FakeAdapter(Adapter):
...
__plugin_meta__ = PluginMetadata(
name="测试插件",
description="测试插件元信息",
usage="无法使用",
type="application",
homepage="https://nonebot.dev",
config=Config,
supported_adapters={"~onebot.v11", "plugins.metadata:FakeAdapter"},
extra={"author": "NoneBot"},
)

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing_extensions import Annotated
from nonebot import on_message
from nonebot.params import Depends
@@ -47,3 +48,17 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
async def class_depend(c: ClassDependency = Depends()):
return c
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
return x
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
return c
async def annotated_prior_depend(
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
):
return x

View File

@@ -1,4 +1,4 @@
from typing import List, Tuple
from typing import List, Match, Tuple
from nonebot.typing import T_State
from nonebot.adapters import Message
@@ -17,6 +17,7 @@ from nonebot.params import (
RegexMatched,
ShellCommandArgs,
ShellCommandArgv,
CommandWhitespace,
)
@@ -48,6 +49,10 @@ async def command_start(start: str = CommandStart()) -> str:
return start
async def command_whitespace(whitespace: str = CommandWhitespace()) -> str:
return whitespace
async def shell_command_args(
shell_command_args: dict = ShellCommandArgs(),
) -> dict:
@@ -68,12 +73,12 @@ async def regex_group(regex_group: Tuple = RegexGroup()) -> Tuple:
return regex_group
async def regex_matched(regex_matched: str = RegexMatched()) -> str:
async def regex_matched(regex_matched: Match[str] = RegexMatched()) -> Match[str]:
return regex_matched
async def regex_str(regex_matched: str = RegexStr()) -> str:
return regex_matched
async def regex_str(regex_str: str = RegexStr()) -> str:
return regex_str
async def startswith(startswith: str = Startswith()) -> str:

View File

@@ -0,0 +1,23 @@
from typing import Optional
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import Arg, Depends
from nonebot.adapters import Bot, Event, Message
def dependency():
return 1
async def complex_priority(
sub: int = Depends(dependency),
bot: Optional[Bot] = None,
event: Optional[Event] = None,
state: T_State = {},
matcher: Optional[Matcher] = None,
arg: Message = Arg(),
exception: Optional[Exception] = None,
default: int = 1,
):
...

View File

@@ -1,6 +1,8 @@
from typing import Type
from datetime import datetime, timezone
from nonebot.adapters import Event
from nonebot.matcher import Matcher
from nonebot import (
CommandGroup,
MatcherGroup,
@@ -50,6 +52,20 @@ matcher_on = on(
)
def matcher_on_factory() -> Type[Matcher]:
return 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],

View File

@@ -41,6 +41,26 @@ def test_segment_validate():
parse_obj_as(MessageSegment, {"data": {}})
def test_segment_join():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
seg = MessageSegment.text("test")
iterable = [
MessageSegment.text("first"),
Message([MessageSegment.text("second"), MessageSegment.text("third")]),
]
assert seg.join(iterable) == Message(
[
MessageSegment.text("first"),
MessageSegment.text("test"),
MessageSegment.text("second"),
MessageSegment.text("third"),
]
)
def test_segment():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
@@ -87,7 +107,6 @@ def test_message_add():
def test_message_getitem():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
@@ -102,7 +121,7 @@ def test_message_getitem():
assert message[0] == MessageSegment.text("test")
assert message[0:2] == Message(
assert message[:2] == Message(
[MessageSegment.text("test"), MessageSegment.image("test2")]
)
@@ -147,3 +166,124 @@ def test_message_validate():
with pytest.raises(ValidationError):
parse_obj_as(Message, object())
def test_message_contains():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
message = Message(
[
MessageSegment.text("test"),
MessageSegment.image("test2"),
MessageSegment.image("test3"),
MessageSegment.text("test4"),
]
)
assert message.has(MessageSegment.text("test")) is True
assert MessageSegment.text("test") in message
assert message.has("image") is True
assert "image" in message
assert message.has(MessageSegment.text("foo")) is False
assert MessageSegment.text("foo") not in message
assert message.has("foo") is False
assert "foo" not in message
def test_message_only():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
message = Message(
[
MessageSegment.text("test"),
MessageSegment.text("test2"),
]
)
assert message.only("text") is True
assert message.only(MessageSegment.text("test")) is False
message = Message(
[
MessageSegment.text("test"),
MessageSegment.image("test2"),
MessageSegment.image("test3"),
MessageSegment.text("test4"),
]
)
assert message.only("text") is False
message = Message(
[
MessageSegment.text("test"),
MessageSegment.text("test"),
]
)
assert message.only(MessageSegment.text("test")) is True
def test_message_join():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
msg = Message([MessageSegment.text("test")])
iterable = [
MessageSegment.text("first"),
Message([MessageSegment.text("second"), MessageSegment.text("third")]),
]
assert msg.join(iterable) == Message(
[
MessageSegment.text("first"),
MessageSegment.text("test"),
MessageSegment.text("second"),
MessageSegment.text("third"),
]
)
def test_message_include():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
message = Message(
[
MessageSegment.text("test"),
MessageSegment.image("test2"),
MessageSegment.image("test3"),
MessageSegment.text("test4"),
]
)
assert message.include("text") == Message(
[
MessageSegment.text("test"),
MessageSegment.text("test4"),
]
)
def test_message_exclude():
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
message = Message(
[
MessageSegment.text("test"),
MessageSegment.image("test2"),
MessageSegment.image("test3"),
MessageSegment.text("test4"),
]
)
assert message.exclude("image") == Message(
[
MessageSegment.text("test"),
MessageSegment.text("test4"),
]
)

View File

@@ -1,9 +1,8 @@
from nonebot.adapters import MessageTemplate
from utils import escape_text, make_fake_message
def test_template_basis():
from nonebot.adapters import MessageTemplate
template = MessageTemplate("{key:.3%}")
formatted = template.format(key=0.123456789)
assert formatted == "12.346%"

388
tests/test_broadcast.py Normal file
View File

@@ -0,0 +1,388 @@
import sys
from typing import Optional
import pytest
from nonebug import App
from nonebot import on_message
import nonebot.message as message
from utils import make_fake_event
from nonebot.params import Depends
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
from nonebot.exception import IgnoredException
from nonebot.log import logger, default_filter, default_format
from nonebot.message import (
run_preprocessor,
run_postprocessor,
event_preprocessor,
event_postprocessor,
)
async def _dependency() -> int:
return 1
@pytest.mark.asyncio
async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set())
runned = False
@event_preprocessor
async def test_preprocessor(
bot: Bot,
event: Event,
state: T_State,
sub: int = Depends(_dependency),
default: int = 1,
):
nonlocal runned
runned = True
assert test_preprocessor in {
dependent.call for dependent in message._event_preprocessors
}
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
assert runned, "event_preprocessor should runned"
@pytest.mark.asyncio
async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set())
@event_preprocessor
async def test_preprocessor():
raise IgnoredException("pass")
assert test_preprocessor in {
dependent.call for dependent in message._event_preprocessors
}
runned = False
async def handler():
nonlocal runned
runned = True
with app.provider.context({}):
matcher = on_message(handlers=[handler])
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
assert not runned, "matcher should not runned"
@pytest.mark.asyncio
async def test_event_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
with monkeypatch.context() as m:
m.setattr(message, "_event_preprocessors", set())
@event_preprocessor
async def test_preprocessor():
raise RuntimeError("test")
assert test_preprocessor in {
dependent.call for dependent in message._event_preprocessors
}
runned = False
async def handler():
nonlocal runned
runned = True
handler_id = logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=default_format,
)
try:
with app.provider.context({}):
matcher = on_message(handlers=[handler])
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
finally:
logger.remove(handler_id)
assert not runned, "matcher should not runned"
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_event_postprocessors", set())
runned = False
@event_postprocessor
async def test_postprocessor(
bot: Bot,
event: Event,
state: T_State,
sub: int = Depends(_dependency),
default: int = 1,
):
nonlocal runned
runned = True
assert test_postprocessor in {
dependent.call for dependent in message._event_postprocessors
}
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
assert runned, "event_postprocessor should runned"
@pytest.mark.asyncio
async def test_event_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
with monkeypatch.context() as m:
m.setattr(message, "_event_postprocessors", set())
@event_postprocessor
async def test_postprocessor():
raise RuntimeError("test")
assert test_postprocessor in {
dependent.call for dependent in message._event_postprocessors
}
handler_id = logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=default_format,
)
try:
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
finally:
logger.remove(handler_id)
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set())
runned = False
@run_preprocessor
async def test_preprocessor(
bot: Bot,
event: Event,
state: T_State,
matcher: Matcher,
sub: int = Depends(_dependency),
default: int = 1,
):
nonlocal runned
runned = True
await matcher.send("test")
assert test_preprocessor in {
dependent.call for dependent in message._run_preprocessors
}
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True, bot)
assert runned, "run_preprocessor should runned"
@pytest.mark.asyncio
async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set())
@run_preprocessor
async def test_preprocessor():
raise IgnoredException("pass")
assert test_preprocessor in {
dependent.call for dependent in message._run_preprocessors
}
runned = False
async def handler():
nonlocal runned
runned = True
with app.provider.context({}):
matcher = on_message(handlers=[handler])
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
assert not runned, "matcher should not runned"
@pytest.mark.asyncio
async def test_run_preprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
with monkeypatch.context() as m:
m.setattr(message, "_run_preprocessors", set())
@run_preprocessor
async def test_preprocessor():
raise RuntimeError("test")
assert test_preprocessor in {
dependent.call for dependent in message._run_preprocessors
}
runned = False
async def handler():
nonlocal runned
runned = True
handler_id = logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=default_format,
)
try:
with app.provider.context({}):
matcher = on_message(handlers=[handler])
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
finally:
logger.remove(handler_id)
assert not runned, "matcher should not runned"
assert "RuntimeError: test" in capsys.readouterr().out
@pytest.mark.asyncio
async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(message, "_run_postprocessors", set())
runned = False
@run_postprocessor
async def test_postprocessor(
bot: Bot,
event: Event,
state: T_State,
matcher: Matcher,
exception: Optional[Exception],
sub: int = Depends(_dependency),
default: int = 1,
):
nonlocal runned
runned = True
await matcher.send("test")
assert test_postprocessor in {
dependent.call for dependent in message._run_postprocessors
}
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True, bot)
assert runned, "run_postprocessor should runned"
@pytest.mark.asyncio
async def test_run_postprocessor_exception(
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
with monkeypatch.context() as m:
m.setattr(message, "_run_postprocessors", set())
@run_postprocessor
async def test_postprocessor():
raise RuntimeError("test")
assert test_postprocessor in {
dependent.call for dependent in message._run_postprocessors
}
handler_id = logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=default_format,
)
try:
with app.provider.context({}):
matcher = on_message()
async with app.test_matcher(matcher) as ctx:
bot = ctx.create_bot()
event = make_fake_event()()
ctx.receive_event(bot, event)
finally:
logger.remove(handler_id)
assert "RuntimeError: test" in capsys.readouterr().out

View File

@@ -1,34 +1,86 @@
import json
import asyncio
from typing import cast
from typing import Any, Set, cast
import pytest
from nonebug import App
@pytest.mark.asyncio
@pytest.mark.parametrize(
"nonebug_init",
[
pytest.param({"driver": "nonebot.drivers.fastapi:Driver"}, id="fastapi"),
pytest.param({"driver": "nonebot.drivers.quart:Driver"}, id="quart"),
],
indirect=True,
)
async def test_reverse_driver(app: App):
import nonebot
from nonebot.exception import WebSocketClosed
from nonebot.drivers import (
import nonebot
from nonebot.config import Env
from nonebot.adapters import Bot
from nonebot.params import Depends
from nonebot import _resolve_combine_expr
from nonebot.dependencies import Dependent
from nonebot.exception import WebSocketClosed
from nonebot.drivers._lifespan import Lifespan
from nonebot.drivers import (
URL,
Driver,
Request,
Response,
WebSocket,
ForwardDriver,
ReverseDriver,
HTTPServerSetup,
WebSocketServerSetup,
)
)
driver = cast(ReverseDriver, nonebot.get_driver())
@pytest.fixture(name="driver")
def load_driver(request: pytest.FixtureRequest) -> Driver:
driver_name = getattr(request, "param", None)
global_driver = nonebot.get_driver()
if driver_name is None:
return global_driver
DriverClass = _resolve_combine_expr(driver_name)
return DriverClass(Env(environment=global_driver.env), global_driver.config)
@pytest.mark.asyncio
async def test_lifespan():
lifespan = Lifespan()
start_log = []
shutdown_log = []
@lifespan.on_startup
async def _startup1():
assert start_log == []
start_log.append(1)
@lifespan.on_startup
async def _startup2():
assert start_log == [1]
start_log.append(2)
@lifespan.on_shutdown
async def _shutdown1():
assert shutdown_log == []
shutdown_log.append(1)
@lifespan.on_shutdown
async def _shutdown2():
assert shutdown_log == [1]
shutdown_log.append(2)
async with lifespan:
assert start_log == [1, 2]
assert shutdown_log == [1, 2]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver",
[
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
],
indirect=True,
)
async def test_reverse_driver(app: App, driver: Driver):
driver = cast(ReverseDriver, driver)
async def _handle_http(request: Request) -> Response:
assert request.content in (b"test", "test")
@@ -61,7 +113,7 @@ async def test_reverse_driver(app: App):
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
driver.setup_websocket_server(ws_setup)
async with app.test_server() as ctx:
async with app.test_server(driver.asgi) as ctx:
client = ctx.get_client()
response = await client.post("/http_test", data="test")
assert response.status_code == 200
@@ -86,18 +138,15 @@ async def test_reverse_driver(app: App):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"nonebug_init",
"driver",
[
pytest.param({"driver": "nonebot.drivers.httpx:Driver"}, id="httpx"),
pytest.param({"driver": "nonebot.drivers.aiohttp:Driver"}, id="aiohttp"),
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
],
indirect=True,
)
async def test_http_driver(app: App):
import nonebot
from nonebot.drivers import Request, ForwardDriver
driver = cast(ForwardDriver, nonebot.get_driver())
async def test_http_driver(driver: Driver):
driver = cast(ForwardDriver, driver)
request = Request(
"POST",
@@ -140,23 +189,86 @@ async def test_http_driver(app: App):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"nonebug_init, driver_type",
"driver, driver_type",
[
pytest.param(
{"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"},
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
"fastapi+aiohttp",
id="fastapi+aiohttp",
),
pytest.param(
{"driver": "~httpx:Driver+~websockets"},
"~httpx:Driver+~websockets",
"none+httpx+websockets",
id="httpx+websockets",
),
],
indirect=["nonebug_init"],
indirect=["driver"],
)
async def test_combine_driver(app: App, driver_type: str):
import nonebot
driver = nonebot.get_driver()
async def test_combine_driver(driver: Driver, driver_type: str):
assert driver.type == driver_type
@pytest.mark.asyncio
async def test_bot_connect_hook(app: App, driver: Driver):
with pytest.MonkeyPatch.context() as m:
conn_hooks: Set[Dependent[Any]] = set()
disconn_hooks: Set[Dependent[Any]] = set()
m.setattr(Driver, "_bot_connection_hook", conn_hooks)
m.setattr(Driver, "_bot_disconnection_hook", disconn_hooks)
conn_should_be_called = False
disconn_should_be_called = False
dependency_should_be_run = False
dependency_should_be_cleaned = False
async def dependency():
nonlocal dependency_should_be_run, dependency_should_be_cleaned
dependency_should_be_run = True
try:
yield 1
finally:
dependency_should_be_cleaned = True
@driver.on_bot_connect
async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):
nonlocal conn_should_be_called
conn_should_be_called = True
if foo is not bot:
pytest.fail("on_bot_connect hook called with wrong bot")
if dep != 1:
pytest.fail("on_bot_connect hook called with wrong dependency")
if default != 1:
pytest.fail("on_bot_connect hook called with wrong default value")
@driver.on_bot_disconnect
async def disconn_hook(
foo: Bot, dep: int = Depends(dependency), default: int = 1
):
nonlocal disconn_should_be_called
disconn_should_be_called = True
if foo is not bot:
pytest.fail("on_bot_disconnect hook called with wrong bot")
if dep != 1:
pytest.fail("on_bot_connect hook called with wrong dependency")
if default != 1:
pytest.fail("on_bot_connect hook called with wrong default value")
if conn_hook not in {hook.call for hook in conn_hooks}:
pytest.fail("on_bot_connect hook not registered")
if disconn_hook not in {hook.call for hook in disconn_hooks}:
pytest.fail("on_bot_disconnect hook not registered")
async with app.test_api() as ctx:
bot = ctx.create_bot()
await asyncio.sleep(1)
if not conn_should_be_called:
pytest.fail("on_bot_connect hook not called")
if not disconn_should_be_called:
pytest.fail("on_bot_disconnect hook not called")
if not dependency_should_be_run:
pytest.fail("dependency not run")
if not dependency_should_be_cleaned:
pytest.fail("dependency not cleaned")

View File

@@ -1,72 +0,0 @@
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_weather(app: App, load_example):
from examples.weather import weather
from utils import make_fake_event, make_fake_message
# 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型
Message = make_fake_message()
async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()
msg = Message("/天气 上海")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "上海的天气是...", True)
ctx.should_finished()
async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()
msg = Message("/天气 南京")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()
ctx.receive_event(bot, event)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("南京"),
True,
)
ctx.should_rejected()
msg = Message("北京")
event = make_fake_event(_message=msg)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "北京的天气是...", True)
ctx.should_finished()
async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()
msg = Message("/天气")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "你想查询哪个城市的天气呢?", True)
msg = Message("杭州")
event = make_fake_event(_message=msg)()
ctx.receive_event(bot, event)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("杭州"),
True,
)
ctx.should_rejected()
msg = Message("北京")
event = make_fake_event(_message=msg)()
ctx.receive_event(bot, event)
ctx.should_call_send(event, "北京的天气是...", True)
ctx.should_finished()

View File

@@ -1,30 +1,25 @@
import os
import pytest
from nonebug import App
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
os.environ["CONFIG_OVERRIDE"] = "new"
import nonebot
from nonebot.drivers import Driver, ReverseDriver
from nonebot import (
get_app,
get_bot,
get_asgi,
get_bots,
get_driver,
get_adapter,
get_adapters,
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"nonebug_init",
[
{
"config_from_init": "init",
"driver": "~fastapi+~httpx+~websockets",
},
{"config_from_init": "init", "driver": "~fastapi+~aiohttp"},
],
indirect=True,
)
async def test_init(nonebug_init):
from nonebot import get_driver
env = get_driver().env
async def test_init():
env = nonebot.get_driver().env
assert env == "test"
config = get_driver().config
config = nonebot.get_driver().config
assert config.config_from_env == {"test": "test"}
assert config.config_override == "new"
assert config.config_from_init == "init"
@@ -36,21 +31,29 @@ async def test_init(nonebug_init):
@pytest.mark.asyncio
async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
import nonebot
from nonebot.drivers import ForwardDriver, ReverseDriver
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
with monkeypatch.context() as m:
m.setattr(nonebot, "_driver", None)
with pytest.raises(ValueError):
get_driver()
nonebot.init(driver="nonebot.drivers.fastapi")
driver = get_driver()
assert isinstance(driver, ReverseDriver)
assert get_asgi() == driver.asgi
assert get_app() == driver.server_app
async with app.test_api() as ctx:
adapter = ctx.create_adapter()
adapter_name = adapter.get_name()
with monkeypatch.context() as m:
m.setattr(Driver, "_adapters", {adapter_name: adapter})
assert get_adapters() == {adapter_name: adapter}
assert get_adapter(adapter_name) is adapter
assert get_adapter(adapter.__class__) is adapter
with pytest.raises(ValueError):
get_adapter("not exist")
runned = False
def mock_run(*args, **kwargs):

View File

@@ -1,11 +1,14 @@
import pytest
from nonebug import App
from nonebot.permission import User
from nonebot.matcher import Matcher, matchers
from nonebot.message import check_and_run_matcher
from utils import make_fake_event, make_fake_message
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugin):
async def test_matcher(app: App):
from plugins.matcher.matcher_process import (
test_got,
test_handle,
@@ -77,7 +80,21 @@ async def test_matcher(app: App, load_plugin):
@pytest.mark.asyncio
async def test_type_updater(app: App, load_plugin):
async def test_matcher_destroy(app: App):
from plugins.matcher.matcher_process import test_destroy
async with app.test_matcher(test_destroy) as ctx:
assert len(matchers) == 1
assert len(matchers[test_destroy.priority]) == 1
assert matchers[test_destroy.priority][0] is test_destroy
test_destroy.destroy()
assert len(matchers[test_destroy.priority]) == 0
@pytest.mark.asyncio
async def test_type_updater(app: App):
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
event = make_fake_event()()
@@ -98,8 +115,7 @@ async def test_type_updater(app: App, load_plugin):
@pytest.mark.asyncio
async def test_permission_updater(app: App, load_plugin):
from nonebot.permission import User
async def test_permission_updater(app: App):
from plugins.matcher.matcher_permission import (
default_permission,
test_custom_updater,
@@ -143,8 +159,7 @@ async def test_permission_updater(app: App, load_plugin):
@pytest.mark.asyncio
async def test_run(app: App):
from nonebot.matcher import Matcher, matchers
with app.provider.context({}):
assert not matchers
event = make_fake_event()()
@@ -174,9 +189,7 @@ async def test_run(app: App):
@pytest.mark.asyncio
async def test_expire(app: App, load_plugin):
from nonebot.matcher import matchers
from nonebot.message import _check_matcher
async def test_expire(app: App):
from plugins.matcher.matcher_expire import (
test_temp_matcher,
test_datetime_matcher,
@@ -187,19 +200,19 @@ async def test_expire(app: App, load_plugin):
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, bot, event, {})
await check_and_run_matcher(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, bot, event, {})
await check_and_run_matcher(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, bot, event, {})
await check_and_run_matcher(test_timedelta_matcher, bot, event, {})
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]

View File

@@ -1,11 +1,14 @@
import pytest
from nonebug import App
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
@pytest.mark.asyncio
async def test_manager(app: App, load_plugin):
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
async def test_manager(app: App):
try:
default_provider = matchers.provider
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
assert matchers.provider == default_provider
assert default_provider == matchers.provider
finally:
matchers.provider = app.provider

View File

@@ -1,18 +1,50 @@
import re
import pytest
from nonebug import App
from nonebot.matcher import Matcher
from nonebot.dependencies import Dependent
from nonebot.exception import TypeMisMatch
from utils import make_fake_event, make_fake_message
from nonebot.params import (
ArgParam,
BotParam,
EventParam,
StateParam,
DependParam,
DefaultParam,
MatcherParam,
ExceptionParam,
)
from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
)
@pytest.mark.asyncio
async def test_depend(app: App, load_plugin):
from nonebot.params import DependParam
async def test_depend(app: App):
from plugins.param.param_depend import (
ClassDependency,
runned,
depends,
class_depend,
test_depends,
annotated_depend,
annotated_class_depend,
annotated_prior_depend,
)
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
@@ -29,14 +61,28 @@ async def test_depend(app: App, load_plugin):
assert len(runned) == 2 and runned[0] == runned[1] == 1
runned.clear()
async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx:
ctx.should_return(ClassDependency(x=1, y=2))
async with app.test_dependent(annotated_depend, allow_types=[DependParam]) as ctx:
ctx.should_return(1)
async with app.test_dependent(
annotated_prior_depend, allow_types=[DependParam]
) as ctx:
ctx.should_return(1)
assert runned == [1, 1]
async with app.test_dependent(
annotated_class_depend, allow_types=[DependParam]
) as ctx:
ctx.should_return(ClassDependency(x=1, y=2))
@pytest.mark.asyncio
async def test_bot(app: App, load_plugin):
from nonebot.params import BotParam
from nonebot.exception import TypeMisMatch
async def test_bot(app: App):
from plugins.param.param_bot import (
FooBot,
get_bot,
@@ -82,9 +128,7 @@ async def test_bot(app: App, load_plugin):
@pytest.mark.asyncio
async def test_event(app: App, load_plugin):
from nonebot.exception import TypeMisMatch
from nonebot.params import EventParam, DependParam
async def test_event(app: App):
from plugins.param.param_event import (
FooEvent,
event,
@@ -159,25 +203,7 @@ async def test_event(app: App, load_plugin):
@pytest.mark.asyncio
async def test_state(app: App, load_plugin):
from nonebot.params import StateParam, DependParam
from nonebot.consts import (
CMD_KEY,
REGEX_STR,
PREFIX_KEY,
REGEX_DICT,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY,
REGEX_GROUP,
ENDSWITH_KEY,
CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
)
async def test_state(app: App):
from plugins.param.param_state import (
state,
command,
@@ -194,24 +220,24 @@ async def test_state(app: App, load_plugin):
command_start,
regex_matched,
not_legacy_state,
command_whitespace,
shell_command_args,
shell_command_argv,
)
fake_message = make_fake_message()("text")
fake_matched = re.match(r"\[cq:(?P<type>.*?),(?P<arg>.*?)\]", "[cq:test,arg=value]")
fake_state = {
PREFIX_KEY: {
CMD_KEY: ("cmd",),
RAW_CMD_KEY: "/cmd",
CMD_START_KEY: "/",
CMD_ARG_KEY: fake_message,
CMD_WHITESPACE_KEY: " ",
},
SHELL_ARGV: ["-h"],
SHELL_ARGS: {"help": True},
REGEX_MATCHED: "[cq:test,arg=value]",
REGEX_STR: "[cq:test,arg=value]",
REGEX_GROUP: ("test", "arg=value"),
REGEX_DICT: {"type": "test", "arg": "value"},
REGEX_MATCHED: fake_matched,
STARTSWITH_KEY: "startswith",
ENDSWITH_KEY: "endswith",
FULLMATCH_KEY: "fullmatch",
@@ -256,6 +282,12 @@ async def test_state(app: App, load_plugin):
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])
async with app.test_dependent(
command_whitespace, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[PREFIX_KEY][CMD_WHITESPACE_KEY])
async with app.test_dependent(
shell_command_argv, allow_types=[StateParam, DependParam]
) as ctx:
@@ -278,19 +310,19 @@ async def test_state(app: App, load_plugin):
regex_str, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[REGEX_STR])
ctx.should_return("[cq:test,arg=value]")
async with app.test_dependent(
regex_group, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[REGEX_GROUP])
ctx.should_return(("test", "arg=value"))
async with app.test_dependent(
regex_dict, allow_types=[StateParam, DependParam]
) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state[REGEX_DICT])
ctx.should_return({"type": "test", "arg": "arg=value"})
async with app.test_dependent(
startswith, allow_types=[StateParam, DependParam]
@@ -318,9 +350,7 @@ async def test_state(app: App, load_plugin):
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugin):
from nonebot.matcher import Matcher
from nonebot.params import DependParam, MatcherParam
async def test_matcher(app: App):
from plugins.param.param_matcher import matcher, receive, last_receive
fake_matcher = Matcher()
@@ -348,9 +378,7 @@ async def test_matcher(app: App, load_plugin):
@pytest.mark.asyncio
async def test_arg(app: App, load_plugin):
from nonebot.matcher import Matcher
from nonebot.params import ArgParam
async def test_arg(app: App):
from plugins.param.param_arg import arg, arg_str, arg_plain_text
matcher = Matcher()
@@ -371,8 +399,7 @@ async def test_arg(app: App, load_plugin):
@pytest.mark.asyncio
async def test_exception(app: App, load_plugin):
from nonebot.params import ExceptionParam
async def test_exception(app: App):
from plugins.param.param_exception import exc
exception = ValueError("test")
@@ -382,9 +409,46 @@ async def test_exception(app: App, load_plugin):
@pytest.mark.asyncio
async def test_default(app: App, load_plugin):
from nonebot.params import DefaultParam
async def test_default(app: App):
from plugins.param.param_default import default
async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx:
ctx.should_return(1)
@pytest.mark.asyncio
async def test_priority():
from plugins.param.priority import complex_priority
dependent = Dependent.parse(
call=complex_priority,
allow_types=[
DependParam,
BotParam,
EventParam,
StateParam,
MatcherParam,
ArgParam,
ExceptionParam,
DefaultParam,
],
)
for param in dependent.params:
if param.name == "sub":
assert isinstance(param.field_info, DependParam)
elif param.name == "bot":
assert isinstance(param.field_info, BotParam)
elif param.name == "event":
assert isinstance(param.field_info, EventParam)
elif param.name == "state":
assert isinstance(param.field_info, StateParam)
elif param.name == "matcher":
assert isinstance(param.field_info, MatcherParam)
elif param.name == "arg":
assert isinstance(param.field_info, ArgParam)
elif param.name == "exception":
assert isinstance(param.field_info, ExceptionParam)
elif param.name == "default":
assert isinstance(param.field_info, DefaultParam)
else:
raise ValueError(f"unknown param {param.name}")

View File

@@ -4,13 +4,26 @@ import pytest
from nonebug import App
from utils import make_fake_event
from nonebot.exception import SkippedException
from nonebot.permission import (
USER,
NOTICE,
MESSAGE,
REQUEST,
METAEVENT,
SUPERUSER,
User,
Notice,
Message,
Request,
MetaEvent,
SuperUser,
Permission,
)
@pytest.mark.asyncio
async def test_permission(app: App):
from nonebot.permission import Permission
from nonebot.exception import SkippedException
async def falsy():
return False
@@ -42,20 +55,8 @@ async def test_permission(app: App):
@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
@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)])
async def test_message(type: str, expected: bool):
dependent = list(MESSAGE.checkers)[0]
checker = dependent.call
@@ -66,20 +67,8 @@ async def test_message(
@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
@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)])
async def test_notice(type: str, expected: bool):
dependent = list(NOTICE.checkers)[0]
checker = dependent.call
@@ -90,20 +79,8 @@ async def test_notice(
@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
@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)])
async def test_request(type: str, expected: bool):
dependent = list(REQUEST.checkers)[0]
checker = dependent.call
@@ -114,20 +91,8 @@ async def test_request(
@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
@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)])
async def test_metaevent(type: str, expected: bool):
dependent = list(METAEVENT.checkers)[0]
checker = dependent.call
@@ -139,7 +104,7 @@ async def test_metaevent(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"type,user_id,expected",
"type, user_id, expected",
[
("message", "test", True),
("message", "foo", False),
@@ -148,14 +113,7 @@ async def test_metaevent(
("notice", "test", True),
],
)
async def test_superuser(
app: App,
type: str,
user_id: str,
expected: bool,
):
from nonebot.permission import SUPERUSER, SuperUser
async def test_superuser(app: App, type: str, user_id: str, expected: bool):
dependent = list(SUPERUSER.checkers)[0]
checker = dependent.call
@@ -170,7 +128,7 @@ async def test_superuser(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"session_ids,session_id,expected",
"session_ids, session_id, expected",
[
(("user", "foo"), "user", True),
(("user", "foo"), "bar", False),
@@ -180,8 +138,6 @@ async def test_superuser(
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

View File

@@ -1,16 +1,11 @@
from typing import TYPE_CHECKING, Set
import pytest
from nonebug import App
if TYPE_CHECKING:
from nonebot.plugin import Plugin
import nonebot
from nonebot.plugin import PluginManager, _managers
@pytest.mark.asyncio
async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
import nonebot
async def test_get_plugin():
# check simple plugin
plugin = nonebot.get_plugin("export")
assert plugin
@@ -28,12 +23,15 @@ async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
@pytest.mark.asyncio
async def test_get_available_plugin(app: App):
import nonebot
from nonebot.plugin import PluginManager, _managers
async def test_get_available_plugin():
old_managers = _managers.copy()
_managers.clear()
try:
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
# check get available plugins
plugin_names = nonebot.get_available_plugin_names()
assert plugin_names == {"export", "require"}
finally:
_managers.clear()
_managers.extend(old_managers)

View File

@@ -1,38 +1,32 @@
import sys
from typing import Set
from pathlib import Path
from dataclasses import asdict
from typing import TYPE_CHECKING, Set
import pytest
from nonebug import App
if TYPE_CHECKING:
from nonebot.plugin import Plugin
import nonebot
from nonebot.plugin import Plugin, PluginManager, _managers
@pytest.mark.asyncio
async def test_load_plugin(app: App):
import nonebot
async def test_load_plugin():
# check regular
assert nonebot.load_plugin("plugins.metadata")
assert nonebot.load_plugin("dynamic.simple")
# check path
assert nonebot.load_plugin(Path("plugins/export"))
assert nonebot.load_plugin(Path("dynamic/path.py"))
# 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
async def test_load_plugins(load_plugin: Set[Plugin], load_builtin_plugin: Set[Plugin]):
loaded_plugins = {
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
}
assert loaded_plugins == load_plugin
assert loaded_plugins >= load_plugin | load_builtin_plugin
# check simple plugin
assert "plugins.export" in sys.modules
@@ -51,9 +45,7 @@ async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
@pytest.mark.asyncio
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
import nonebot
async def test_load_nested_plugin():
parent_plugin = nonebot.get_plugin("nested")
sub_plugin = nonebot.get_plugin("nested_subplugin")
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
@@ -64,9 +56,7 @@ async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
@pytest.mark.asyncio
async def test_load_json(app: App):
import nonebot
async def test_load_json():
nonebot.load_from_json("./plugins.json")
with pytest.raises(TypeError):
@@ -74,9 +64,7 @@ async def test_load_json(app: App):
@pytest.mark.asyncio
async def test_load_toml(app: App):
import nonebot
async def test_load_toml():
nonebot.load_from_toml("./plugins.toml")
with pytest.raises(ValueError):
@@ -87,35 +75,27 @@ async def test_load_toml(app: App):
@pytest.mark.asyncio
async def test_bad_plugin(app: App):
import nonebot
async def test_bad_plugin():
nonebot.load_plugins("bad_plugins")
assert nonebot.get_plugin("bad_plugins") is None
@pytest.mark.asyncio
async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot
async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
def _patched_find(name: str):
assert False
pytest.fail("require existing plugin should not call find_manager_by_name")
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
nonebot.load_plugin("plugins.export")
nonebot.require("plugins.export")
@pytest.mark.asyncio
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot
from nonebot.plugin import PluginManager, _managers
m = PluginManager(["plugins.export"])
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
m = PluginManager(["dynamic.require_not_loaded"])
_managers.append(m)
num_managers = len(_managers)
origin_load = PluginManager.load_plugin
@@ -125,34 +105,30 @@ async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
nonebot.require("plugins.export")
nonebot.require("dynamic.require_not_loaded")
assert len(_managers) == 1
assert len(_managers) == num_managers
@pytest.mark.asyncio
async def test_require_not_declared(app: App):
import nonebot
from nonebot.plugin import _managers
async def test_require_not_declared():
num_managers = len(_managers)
nonebot.require("plugins.export")
nonebot.require("dynamic.require_not_declared")
assert len(_managers) == 1
assert _managers[-1].plugins == {"plugins.export"}
assert len(_managers) == num_managers + 1
assert _managers[-1].plugins == {"dynamic.require_not_declared"}
@pytest.mark.asyncio
async def test_require_not_found(app: App):
import nonebot
async def test_require_not_found():
with pytest.raises(RuntimeError):
nonebot.require("some_plugin_not_exist")
@pytest.mark.asyncio
async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
import nonebot
from plugins.metadata import Config
async def test_plugin_metadata():
from plugins.metadata import Config, FakeAdapter
plugin = nonebot.get_plugin("metadata")
assert plugin
@@ -161,6 +137,11 @@ async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
"name": "测试插件",
"description": "测试插件元信息",
"usage": "无法使用",
"type": "application",
"homepage": "https://nonebot.dev",
"config": Config,
"supported_adapters": {"~onebot.v11", "plugins.metadata:FakeAdapter"},
"extra": {"author": "NoneBot"},
}
assert plugin.metadata.get_supported_adapters() == {FakeAdapter}

View File

@@ -1,12 +1,11 @@
import pytest
from nonebug import App
from nonebot.plugin import PluginManager
@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")
async def test_load_plugin_name():
m = PluginManager(plugins=["dynamic.manager"])
module1 = m.load_plugin("manager")
module2 = m.load_plugin("dynamic.manager")
assert module1 is module2

View File

@@ -1,16 +1,12 @@
from typing import Type, Optional
from typing import Type, Callable, 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 (
import nonebot
from nonebot.adapters import Event
from nonebot.typing import T_RuleChecker
from nonebot.matcher import Matcher, matchers
from nonebot.rule import (
RegexRule,
IsTypeRule,
CommandRule,
@@ -19,52 +15,97 @@ async def test_on(app: App, load_plugin):
FullmatchRule,
StartswithRule,
ShellCommandRule,
)
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"matcher_name, pre_rule_factory, has_permission",
[
pytest.param("matcher_on", None, True),
pytest.param("matcher_on_metaevent", None, False),
pytest.param("matcher_on_message", None, True),
pytest.param("matcher_on_notice", None, False),
pytest.param("matcher_on_request", None, False),
pytest.param(
"matcher_on_startswith", lambda e: StartswithRule(("test",)), True
),
pytest.param("matcher_on_endswith", lambda e: EndswithRule(("test",)), True),
pytest.param("matcher_on_fullmatch", lambda e: FullmatchRule(("test",)), True),
pytest.param("matcher_on_keyword", lambda e: KeywordsRule("test"), True),
pytest.param("matcher_on_command", lambda e: CommandRule([("test",)]), True),
pytest.param(
"matcher_on_shell_command",
lambda e: ShellCommandRule([("test",)], None),
True,
),
pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
pytest.param(
"matcher_sub_shell_cmd",
lambda e: ShellCommandRule([("test", "sub")], None),
True,
),
pytest.param("matcher_group_on", None, True),
pytest.param("matcher_group_on_metaevent", None, False),
pytest.param("matcher_group_on_message", None, True),
pytest.param("matcher_group_on_notice", None, False),
pytest.param("matcher_group_on_request", None, False),
pytest.param(
"matcher_group_on_startswith",
lambda e: StartswithRule(("test",)),
True,
),
pytest.param(
"matcher_group_on_endswith",
lambda e: EndswithRule(("test",)),
True,
),
pytest.param(
"matcher_group_on_fullmatch",
lambda e: FullmatchRule(("test",)),
True,
),
pytest.param("matcher_group_on_keyword", lambda e: KeywordsRule("test"), True),
pytest.param(
"matcher_group_on_command",
lambda e: CommandRule([("test",)]),
True,
),
pytest.param(
"matcher_group_on_shell_command",
lambda e: ShellCommandRule([("test",)], None),
True,
),
pytest.param("matcher_group_on_regex", lambda e: RegexRule("test"), True),
pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
],
)
async def test_on(
matcher_name: str,
pre_rule_factory: Optional[Callable[[Type[Event]], T_RuleChecker]],
has_permission: bool,
):
import plugins.plugin.matchers as module
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")
matcher = getattr(module, matcher_name)
assert issubclass(matcher, Matcher), f"{matcher_name} should be a Matcher"
pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None
plugin = nonebot.get_plugin("plugin")
assert plugin, "plugin should be loaded"
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}
)
@@ -82,35 +123,27 @@ async def test_on(app: App, load_plugin):
assert matcher._default_state == state
assert matcher.plugin is plugin
assert matcher in plugin.matcher
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)
@pytest.mark.asyncio
async def test_runtime_on():
import plugins.plugin.matchers as module
from plugins.plugin.matchers import matcher_on_factory
matcher = matcher_on_factory()
plugin = nonebot.get_plugin("plugin")
assert plugin, "plugin should be loaded"
try:
assert matcher.plugin is plugin
assert matcher not in plugin.matcher
assert matcher.module is module
assert matcher.plugin_name == "plugin"
assert matcher.module_name == "plugins.plugin.matchers"
finally:
matcher.destroy()

View File

@@ -1,17 +1,56 @@
import re
import sys
from typing import Dict, Tuple, Union, Optional
from typing import Match, Tuple, Union, Optional
import pytest
from nonebug import App
from nonebot.typing import T_State
from utils import make_fake_event, make_fake_message
from nonebot.exception import ParserExit, SkippedException
from nonebot.consts import (
CMD_KEY,
PREFIX_KEY,
SHELL_ARGS,
SHELL_ARGV,
CMD_ARG_KEY,
KEYWORD_KEY,
ENDSWITH_KEY,
FULLMATCH_KEY,
REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
)
from nonebot.rule import (
CMD_RESULT,
TRIE_VALUE,
Rule,
ToMeRule,
TrieRule,
Namespace,
RegexRule,
IsTypeRule,
CommandRule,
EndswithRule,
KeywordsRule,
FullmatchRule,
ArgumentParser,
StartswithRule,
ShellCommandRule,
regex,
to_me,
command,
is_type,
keyword,
endswith,
fullmatch,
startswith,
shell_command,
)
@pytest.mark.asyncio
async def test_rule(app: App):
from nonebot.rule import Rule
from nonebot.exception import SkippedException
async def falsy():
return False
@@ -42,9 +81,47 @@ async def test_rule(app: App):
assert await Rule(truthy, skipped)(bot, event, {}) == False
@pytest.mark.asyncio
async def test_trie(app: App):
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
Message = make_fake_message()
MessageSegment = Message.get_segment_class()
async with app.test_api() as ctx:
bot = ctx.create_bot()
message = Message("/fake-prefix some args")
event = make_fake_event(_message=message)()
state = {}
TrieRule.get_value(bot, event, state)
assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",),
raw_command="/fake-prefix",
command_arg=Message("some args"),
command_start="/",
command_whitespace=" ",
)
message = MessageSegment.text("/fake-prefix ") + MessageSegment.image(
"fake url"
)
event = make_fake_event(_message=message)()
state = {}
TrieRule.get_value(bot, event, state)
assert state[PREFIX_KEY] == CMD_RESULT(
command=("fake-prefix",),
raw_command="/fake-prefix",
command_arg=Message(MessageSegment.image("fake url")),
command_start="/",
command_whitespace=" ",
)
del TrieRule.prefix["/fake-prefix"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"msg,ignorecase,type,text,expected",
"msg, ignorecase, type, text, expected",
[
("prefix", False, "message", "prefix_", True),
("prefix", False, "message", "Prefix_", False),
@@ -54,20 +131,17 @@ async def test_rule(app: App):
("prefix", False, "message", "fooprefix", False),
("prefix", False, "message", None, False),
(("prefix", "foo"), False, "message", "fooprefix", True),
("prefix", False, "notice", "prefix", True),
("prefix", False, "notice", "foo", False),
],
)
async def test_startswith(
app: App,
msg: Union[str, Tuple[str, ...]],
ignorecase: bool,
type: str,
text: Optional[str],
expected: bool,
):
from nonebot.consts import STARTSWITH_KEY
from nonebot.rule import StartswithRule, startswith
test_startswith = startswith(msg, ignorecase)
dependent = list(test_startswith.checkers)[0]
checker = dependent.call
@@ -87,7 +161,7 @@ async def test_startswith(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"msg,ignorecase,type,text,expected",
"msg, ignorecase, type, text, expected",
[
("suffix", False, "message", "_suffix", True),
("suffix", False, "message", "_Suffix", False),
@@ -97,20 +171,17 @@ async def test_startswith(
("suffix", False, "message", "suffixfoo", False),
("suffix", False, "message", None, False),
(("suffix", "foo"), False, "message", "suffixfoo", True),
("suffix", False, "notice", "suffix", True),
("suffix", False, "notice", "foo", False),
],
)
async def test_endswith(
app: App,
msg: Union[str, Tuple[str, ...]],
ignorecase: bool,
type: str,
text: Optional[str],
expected: bool,
):
from nonebot.consts import ENDSWITH_KEY
from nonebot.rule import EndswithRule, endswith
test_endswith = endswith(msg, ignorecase)
dependent = list(test_endswith.checkers)[0]
checker = dependent.call
@@ -130,7 +201,7 @@ async def test_endswith(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"msg,ignorecase,type,text,expected",
"msg, ignorecase, type, text, expected",
[
("fullmatch", False, "message", "fullmatch", True),
("fullmatch", False, "message", "Fullmatch", False),
@@ -140,20 +211,17 @@ async def test_endswith(
("fullmatch", False, "message", "_fullmatch_", False),
("fullmatch", False, "message", None, False),
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
("fullmatch", False, "notice", "fullmatch", True),
("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.consts import FULLMATCH_KEY
from nonebot.rule import FullmatchRule, fullmatch
test_fullmatch = fullmatch(msg, ignorecase)
dependent = list(test_fullmatch.checkers)[0]
checker = dependent.call
@@ -173,25 +241,22 @@ async def test_fullmatch(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"kws,type,text,expected",
"kws, type, text, expected",
[
(("key",), "message", "_key_", True),
(("key", "foo"), "message", "_foo_", True),
(("key",), "message", None, False),
(("key",), "notice", "foo", False),
(("key",), "message", "foo", False),
(("key",), "notice", "_key_", True),
(("key",), "notice", "foo", False),
],
)
async def test_keyword(
app: App,
kws: Tuple[str, ...],
type: str,
text: Optional[str],
expected: bool,
):
from nonebot.consts import KEYWORD_KEY
from nonebot.rule import KeywordsRule, keyword
test_keyword = keyword(*kws)
dependent = list(test_keyword.checkers)[0]
checker = dependent.call
@@ -208,31 +273,52 @@ async def test_keyword(
@pytest.mark.asyncio
@pytest.mark.parametrize(
"cmds", [(("help",),), (("help", "foo"),), (("help",), ("foo",))]
"cmds, force_whitespace, cmd, whitespace, arg_text, expected",
[
# command tests
[(("help",),), None, ("help",), None, None, True],
[(("help",),), None, ("foo",), None, None, False],
[(("help", "foo"),), None, ("help", "foo"), None, None, True],
[(("help", "foo"),), None, ("help", "bar"), None, None, False],
[(("help",), ("foo",)), None, ("help",), None, None, True],
[(("help",), ("foo",)), None, ("bar",), None, None, False],
# whitespace tests
[(("help",),), True, ("help",), " ", "arg", True],
[(("help",),), True, ("help",), None, "arg", False],
[(("help",),), True, ("help",), None, None, True],
[(("help",),), False, ("help",), " ", "arg", False],
[(("help",),), False, ("help",), None, "arg", True],
[(("help",),), False, ("help",), None, None, True],
[(("help",),), " ", ("help",), " ", "arg", True],
[(("help",),), " ", ("help",), "\n", "arg", False],
[(("help",),), " ", ("help",), None, "arg", False],
[(("help",),), " ", ("help",), None, None, True],
],
)
async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
from nonebot.rule import CommandRule, command
from nonebot.consts import CMD_KEY, PREFIX_KEY
test_command = command(*cmds)
async def test_command(
cmds: Tuple[Tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]],
cmd: Tuple[str, ...],
whitespace: Optional[str],
arg_text: Optional[str],
expected: bool,
):
test_command = command(*cmds, force_whitespace=force_whitespace)
dependent = list(test_command.checkers)[0]
checker = dependent.call
assert isinstance(checker, CommandRule)
assert checker.cmds == cmds
for cmd in cmds:
state = {PREFIX_KEY: {CMD_KEY: cmd}}
assert await dependent(state=state)
arg = arg_text if arg_text is None else make_fake_message()(arg_text)
state = {
PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg}
}
assert await dependent(state=state) == expected
@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
async def test_shell_command():
state: T_State
CMD = ("test",)
Message = make_fake_message()
@@ -298,6 +384,19 @@ async def test_shell_command(app: App):
assert state[SHELL_ARGS].status != 0
assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:")
test_parser_remain_args = shell_command(CMD, parser=parser)
dependent = list(test_parser_remain_args.checkers)[0]
checker = dependent.call
assert isinstance(checker, ShellCommandRule)
message = MessageSegment.text("-a 1 2") + 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", "1", "2", MessageSegment.image("test")]
assert isinstance(state[SHELL_ARGS], ParserExit)
assert state[SHELL_ARGS].status != 0
assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:")
test_message_parser = shell_command(CMD, parser=parser)
dependent = list(test_message_parser.checkers)[0]
checker = dependent.call
@@ -328,37 +427,27 @@ async def test_shell_command(app: App):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"pattern,type,text,expected,matched,string,group,dict",
"pattern, type, text, expected, matched",
[
(
r"(?P<key>key\d)",
"message",
"_key1_",
True,
"key1",
"key1",
("key1",),
{"key": "key1"},
re.search(r"(?P<key>key\d)", "_key1_"),
),
(r"foo", "message", None, False, None, None, None, None),
(r"foo", "notice", "foo", False, None, None, None, None),
(r"foo", "message", None, False, None),
(r"foo", "notice", "foo", True, re.search(r"foo", "foo")),
(r"foo", "notice", "bar", False, None),
],
)
async def test_regex(
app: App,
pattern: str,
type: str,
text: Optional[str],
expected: bool,
matched: Optional[str],
string: Optional[str],
group: Optional[Tuple[str, ...]],
dict: Optional[Dict[str, str]],
matched: Optional[Match[str]],
):
from nonebot.typing import T_State
from nonebot.rule import RegexRule, regex
from nonebot.consts import REGEX_STR, REGEX_DICT, REGEX_GROUP, REGEX_MATCHED
test_regex = regex(pattern)
dependent = list(test_regex.checkers)[0]
checker = dependent.call
@@ -370,17 +459,18 @@ async def test_regex(
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_STR) == string
assert state.get(REGEX_GROUP) == group
assert state.get(REGEX_DICT) == dict
result: Optional[Match[str]] = state.get(REGEX_MATCHED)
if matched is None:
assert result is None
else:
assert isinstance(result, Match)
assert result.group() == matched.group()
assert result.span() == matched.span()
@pytest.mark.asyncio
@pytest.mark.parametrize("expected", [True, False])
async def test_to_me(app: App, expected: bool):
from nonebot.rule import ToMeRule, to_me
async def test_to_me(expected: bool):
test_to_me = to_me()
dependent = list(test_to_me.checkers)[0]
checker = dependent.call
@@ -392,9 +482,7 @@ async def test_to_me(app: App, expected: bool):
@pytest.mark.asyncio
async def test_is_type(app: App):
from nonebot.rule import IsTypeRule, is_type
async def test_is_type():
Event1 = make_fake_event()
Event2 = make_fake_event()
Event3 = make_fake_event()

View File

@@ -1,11 +1,10 @@
import json
from utils import make_fake_message
from nonebot.utils import DataclassEncoder
def test_dataclass_encoder():
from nonebot.utils import DataclassEncoder
simple = json.dumps("123", cls=DataclassEncoder)
assert simple == '"123"'

View File

@@ -1,9 +1,8 @@
from typing import TYPE_CHECKING, Type, Union, Mapping, Iterable, Optional
from typing import Type, Union, Mapping, Iterable, Optional
from pydantic import create_model
if TYPE_CHECKING:
from nonebot.adapters import Event, Message
from nonebot.adapters import Event, Message, MessageSegment
def escape_text(s: str, *, escape_comma: bool = True) -> str:
@@ -14,9 +13,7 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
def make_fake_message():
from nonebot.adapters import Message, MessageSegment
class FakeMessageSegment(MessageSegment):
class FakeMessageSegment(MessageSegment["FakeMessage"]):
@classmethod
def get_message_class(cls):
return FakeMessage
@@ -39,7 +36,7 @@ def make_fake_message():
def is_text(self) -> bool:
return self.type == "text"
class FakeMessage(Message):
class FakeMessage(Message[FakeMessageSegment]):
@classmethod
def get_segment_class(cls):
return FakeMessageSegment
@@ -53,7 +50,9 @@ def make_fake_message():
yield FakeMessageSegment(**seg)
return
def __add__(self, other):
def __add__(
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
):
other = escape_text(other) if isinstance(other, str) else other
return super().__add__(other)
@@ -61,18 +60,16 @@ def make_fake_message():
def make_fake_event(
_base: Optional[Type["Event"]] = None,
_base: Optional[Type[Event]] = None,
_type: str = "message",
_name: str = "test",
_description: str = "test",
_user_id: Optional[str] = "test",
_session_id: Optional[str] = "test",
_message: Optional["Message"] = None,
_message: Optional[Message] = None,
_to_me: bool = True,
**fields,
) -> Type["Event"]:
from nonebot.adapters import Event
) -> Type[Event]:
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
class FakeEvent(_Fake):

View File

@@ -6,44 +6,44 @@ slug: /
# 概览
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot,它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。
需要注意的是NoneBot2 仅支持 **Python 3.8 以上版本**
需要注意的是NoneBot 仅支持 **Python 3.8 以上版本**
## 特色
### 异步优先
NoneBot2 基于 Python [asyncio](https://docs.python.org/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
### 完整的类型注解
NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./start/editor-support))。
NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 PyrightPylance 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。
### 开箱即用
NoneBot2 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得初次接触 NoneBot2 时更容易上手。详细使用方法请参考各文档章节以及[使用脚手架](./start/nb-cli)。
NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。
### 插件系统
插件系统是 NoneBot2 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
### 依赖注入系统
NoneBot2 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
#### 什么是依赖注入
[**依赖注入**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**依赖**。
[**依赖注入**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**依赖**。
系统(在这里是指 NoneBot2)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**注入**依赖性)
系统(在这里是指 NoneBot将负责做任何需要的事情为你的代码提供这些必要依赖即**注入**依赖性)
这在你有以下情形的需求时非常有用:
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
- 共享数据库以及网络请求连接会话
- 比如 `httpx.AsyncClient``aiohttp.ClientSession``sqlalchemy.Session`
- 用户权限检查以及认证
- 机器人用户权限检查以及认证
- 还有更多...
它在完成上述工作的同时,还能尽量减少代码的耦合和重复

View File

@@ -1,207 +0,0 @@
---
id: index
sidebar_position: 0
description: 深入了解 NoneBot2 运行机制
slug: /advanced/
options:
menu:
weight: 10
category: advanced
---
# 深入
:::danger 警告
进阶部分尚未更新完成
:::
## 它如何工作?
如同[概览](../README.md)所言:
> NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的事件进行解析和处理,并以插件化的形式,按优先级分发给事件所对应的事件响应器,来完成具体的功能。
NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体功能的机器人框架,在这里,我们将简要讲述它的工作内容。
**便捷起见,以下内容对 NoneBot2 会被称为 NoneBot与 NoneBot2 交互的机器人实现会被称为协议端**
在实际应用中NoneBot 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 http、websocket 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 NoneBotNoneBot 会处理数据并返回响应给协议端另一方面NoneBot 可以主动推送数据给协议端。而 NoneBot 便是围绕双向通信进行工作的。
在开始工作之前NoneBot 需要进行准备工作:
1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 NoneBot 和后端驱动 `Driver` 对象。
2. **注册协议适配器 `Adapter`**
3. **加载插件**
准备工作完成后NoneBot 会利用 uvicorn 启动,并运行 `on_startup` 钩子函数。
随后,倘若一个协议端与 NoneBot 进行了连接NoneBot 的后端驱动 `Driver` 就会将数据交给 `Adapter`,然后会实例化 `Bot`NoneBot 便会利用 `Bot` 开始工作,它的工作内容分为两个方面:
1. **事件处理**`Bot` 会将协议端上报的数据转化为 `Event`(事件),之后 NoneBot 会根据一套既定流程来处理事件。
2. **调用 `API`**,在**事件处理**的过程中NoneBot 可以通过 `Bot` 调用协议端指定的 `API` 来获取更多数据或者反馈响应给协议端NoneBot 也可以通过调用 `API` 向协议端主动请求数据或者主动推送数据。
在**指南**模块,我们已经叙述了[如何配置 NoneBot](../tutorial/configuration.md)、[如何注册协议适配器](../tutorial/register-adapter.md)以及[如何加载插件](../tutorial/plugin/load-plugin.md),这里便不再赘述。
下面,我们将对**事件处理****调用 API** 进行说明。
## 事件处理
我们可以先看事件处理的流程图:
![handle-event](./images/Handle-Event.png)
在流程图里我们可以看到NoneBot 会有三个阶段来处理事件:
1. **Driver 接收上报数据**
2. **Adapter 处理原始数据**
3. **NoneBot 处理 Event**
我们将顺序说明这三个阶段。其中,会将第三个阶段拆分成**概念解释****处理 Event****特殊异常处理**三个部分来说明。
### Driver 接收上报数据
1. 协议端会通过 websocket 或 http 等方式与 NoneBot 的后端驱动 `Driver` 连接,协议端上报数据后,`Driver` 会将原始数据交给 `Adapter` 处理。
:::warning
连接之前必须要注册 `Adapter`
:::
### Adapter 处理原始数据
1. `Adapter` 检查授权许可,并获取 `self-id` 作为唯一识别 id 。
:::tip
如果协议端通过 websocket 上报数据,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 http 方式连接时,会在协议端每次上报数据时都进行这个步骤。
:::
:::warning
`self-id` 是帐号的唯一识别 ID ,这意味着不能出现相同的 `self-id`
:::
2. 根据 `self-id` 实例化 `Adapter` 相应的 `Bot`
3. 根据 `Event Model` 将原始数据转化为 NoneBot 可以处理的 `Event` 对象。
:::tip
`Adapter` 在转换数据格式的同时可以进行一系列的特殊操作,例如 OneBot 适配器会对 reply 信息进行提取。
:::
4. `Bot``Event` 交由 NoneBot 进一步处理。
### NoneBot 处理 Event
在讲述这个阶段之前,我们需要先对几个概念进行解释。
#### 概念解释
1. **hook** ,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook``运行预处理 hook``运行后处理 hook``事件后处理 hook`
:::tip
关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md)。
:::
2. **Matcher****matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher``matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。
3. **handler**,或者说**事件处理函数**,它们可以认为是 NoneBot 处理 `Event` 的最小单位。在不考虑 `hook` 的情况下,**运行 matcher 就是顺序运行 matcher.handlers**,这句话换种表达方式就是,`handler` 只有添加到 `matcher.handlers` 时,才可以参与到 NoneBot 的工作中来。
:::tip
如何让 `handler` 添加到 `matcher.handlers`
一方面,我们可以参照[这里](../tutorial/plugin/create-handler.md)利用装饰器来添加;另一方面,我们在用 `on()` 或者 `on_*()` 注册事件响应器时,可以添加 `handlers=[handler1, handler2, ...]` 这样的关键词参数来添加。
:::
#### 处理 Event
1. **执行事件预处理 hook** NoneBot 接收到 `Event` 后,会传入到 `事件预处理 hook` 中进行处理。
:::warning
需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并发运行**的。这个原则同样适用于其他的 `hook`
:::
2. **按优先级升序选出同一优先级的 Matcher**NoneBot 提供了一个全局字典 `matchers`,这个字典的 `key` 是优先级 `priority``value` 是一个 `list`,里面存放着同一优先级的 `Matcher`。在注册 `Matcher` 时,它和优先级 `priority` 会添加到里面。
在执行 `事件预处理 hook`NoneBot 会对 `matchers``key` 升序排序并选择出当前最小优先级的 `Matcher`
3. **根据 Matcher 定义的 Rule、Permission 判断是否运行**,在选出 `Matcher`NoneBot 会将 `bot``Event` 传入到 `Matcher.check_rule``Matcher.check_perm` 两个函数中,两个函数分别对 Matcher 定义的 `Rule``Permission` 进行 check当 check 通过后,这个 `Matcher` 就会响应事件。当同一个优先级的所有 `Matcher` 均没有响应时NoneBot 会返回到上一个步骤,选择出下一优先级的 `Matcher`
4. **实例化 matcher 并执行运行预处理 hook**,当 `Matcher` 响应事件后,它便会实例化为 `matcher`,并执行 `运行预处理 hook`
5. **顺序运行 matcher 的所有 handlers**`运行预处理 hook` 执行完毕后,便会运行 `matcher`,也就是**顺序运行**它的 `handlers`
:::tip
`matcher` 运行 `handlers` 的顺序是:先运行该 `matcher` 的类 `Matcher` 注册时添加的 `handlers`(如果有的话),再按照装饰器装饰顺序运行装饰的 `handlers`
:::
6. **执行运行后处理 hook**`matcher``handlers` 运行完毕后,会执行 `运行后处理 hook`
7. **判断是否停止事件传播**NoneBot 会根据当前优先级所有 `matcher``block` 参数或者 `StopPropagation` 异常判断是否停止传播 `Event`如果事件没有停止传播NoneBot 便会返回到第 2 步, 选择出下一优先级的 `Matcher`
8. **执行事件后处理 hook**,在 `Event` 停止传播或执行完所有响应的 `Matcher`NoneBot 会执行 `事件后处理 hook`
`事件后处理 hook` 执行完毕后,当前 `Event` 的处理周期就顺利结束了。
#### 特殊异常处理
在这个阶段NoneBot 规定了几个特殊的异常,当 NoneBot 捕获到它们时,会用特定的行为来处理它们。
1. **IgnoredException**
这个异常可以在 `事件预处理 hook``运行预处理 hook` 抛出。
`事件预处理 hook` 抛出它时NoneBot 会忽略当前的 `Event`,不进行处理。
`运行预处理 hook` 抛出它时NoneBot 会忽略当前的 `matcher`,结束当前 `matcher` 的运行。
:::warning
`hook` 需要抛出这个异常时,要写明原因。
:::
2. **PausedException**
这个异常可以在 `handler` 中由 `Matcher.pause` 抛出。
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler`
3. **RejectedException**
这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler`
4. **FinishedException**
这个异常可以在 `handler` 中由 `Matcher.finish` 抛出。
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行。
5. **StopPropagation**
这个异常一般会在执行 `运行后处理 hook` 后抛出。
当 NoneBot 捕获到它时, 会停止传播当前 `Event` ,不再寻找下一优先级的 `Matcher` ,直接执行 `事件后处理 hook`
## 调用 API
NoneBot 可以通过 `bot` 来调用 `API``API` 可以向协议端发送数据,也可以向协议端请求更多的数据。
NoneBot 调用 `API` 会有如下过程:
1. 调用 `calling_api_hook` 预处理钩子。
2. `adapter` 将信息处理为原始数据,并转交 `driver``driver` 交给协议端处理。
3. `driver` 接收协议端的结果,交给`adapter` 处理之后将结果反馈给 NoneBot 。
4. 调用 `called_api_hook` 后处理钩子。
在调用 `API` 时同样规定了特殊的异常,叫做 `MockApiException` 。该异常会由预处理钩子和后处理钩子触发当预处理钩子触发时NoneBot 会跳过之后的调用过程,直接执行后处理钩子。
:::tip
不同 `adapter` 规定了不同的 API对应的 API 列表请参照协议规范。
:::
一般来说,我们可以用 `bot.*` 来调用 `API`\*是 `API``action` 或者 `endpoint`)。
对于发送消息而言,一方面可以调用既有的 `API` ;另一方面 NoneBot 实现了两个便捷方法,`bot.send(event, message, **kwargs)` 方法和可以在 `handler` 中使用的 `Matcher.send(message, **kwargs)` 方法,来向事件主体发送消息。

View File

@@ -0,0 +1,161 @@
---
sidebar_position: 1
description: 注册适配器与指定平台交互
options:
menu:
weight: 20
category: advanced
---
# 使用适配器
适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。
## 适配器功能与组成
适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。
为了实现这两种功能,适配器通常由四个部分组成:
- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。
- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。
- **Event**:负责定义事件内容,以及事件主体对象。
- **Message**:负责正确序列化消息,以便机器人插件处理。
## 注册适配器
在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器:
```python {2,5} title=bot.py
import nonebot
from nonebot.adapters.console import Adapter
driver = nonebot.get_driver()
driver.register_adapter(Adapter)
```
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。
## 获取已注册的适配器
NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例:
```python
import nonebot
from nonebot.adapters.console import Adapter
adapters = nonebot.get_adapters()
console_adapter = nonebot.get_adapter(Adapter)
console_adapter = nonebot.get_adapter(Adapter.get_name())
```
## 获取 Bot 对象
当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典:
```python
import nonebot
bots = nonebot.get_bots()
```
我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个:
```python
import nonebot
bot = nonebot.get_bot("bot_id")
```
如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典:
```python
import nonebot
from nonebot.adapters.console import Adapter
console_adapter = nonebot.get_adapter(Adapter)
bots = console_adapter.bots
```
Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID由适配器填写通常为机器人的帐号 ID 或者 APP ID。
## 获取事件通用信息
适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息:
### 事件类型
事件类型通常为 `meta_event`、`message`、`notice`、`request`。
```python
type: str = event.get_type()
```
### 事件名称
事件名称由适配器定义,通常用于日志记录。
```python
name: str = event.get_event_name()
```
### 事件描述
事件描述由适配器定义,通常用于日志记录。
```python
description: str = event.get_event_description()
```
### 事件日志字符串
事件日志字符串由事件名称和事件描述组成,用于日志记录。
```python
log: str = event.get_log_string()
```
### 事件主体 ID
事件主体 ID 通常为机器人用户 ID。
```python
user_id: str = event.get_user_id()
```
### 事件会话 ID
事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。
```python
session_id: str = event.get_session_id()
```
### 事件消息
如果事件包含消息,则可以通过该方法获取,否则会产生异常。
```python
message: Message = event.get_message()
```
### 事件纯文本消息
通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。
```python
text: str = event.get_plaintext()
```
### 事件是否与机器人有关
由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。
```python
is_tome: bool = event.is_tome()
```
## 更多
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,243 +0,0 @@
---
sidebar_position: 1
description: 依赖注入简介
options:
menu:
weight: 60
category: advanced
---
# 简介
受 [FastAPI](https://fastapi.tiangolo.com/tutorial/dependencies/) 启发NoneBot 同样编写了一个简易的依赖注入模块,使得开发者可以通过事件处理函数参数的类型标注来自动注入依赖。
## 什么是依赖注入?
[依赖注入](https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5)
> 在软件工程中,**依赖注入**dependency injection的意思为给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖。 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。
依赖注入往往起到了分离依赖和调用方的作用,这样一方面能让代码更为整洁可读,一方面可以提升代码的复用性。
## 使用依赖注入
以下通过一个简单的例子来说明依赖注入的使用方法:
```python {2,7-8,11}
from nonebot import on_command
from nonebot.params import Depends # 1.引用 Depends
from nonebot.adapters.onebot.v11 import MessageEvent
test = on_command("123")
async def depend(event: MessageEvent): # 2.编写依赖函数
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
@test.handle()
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
print(x["uid"], x["nickname"])
```
如注释所言,可以用三步来说明依赖注入的使用过程:
1. 引用 `Depends` 。
2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 `bot`, `event`, `state` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。
在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注,返回一个新的字典,包括 `uid` 和 `nickname` 两个键值。
3. 在事件处理函数中声明依赖项。依赖项必须要 `Depends` 包裹依赖函数作为默认值。
:::tip
请注意,参数 `x` 的类型标注将会影响到事件处理函数的运行,与类型标注不符的值将会导致事件处理函数被跳过。
:::
:::tip
事实上bot、event、state 它们本身只是依赖注入的一个特例,它们无需声明这是依赖即可注入。
:::
虽然声明依赖项的方式和其他参数如 `bot`, `event` 并无二样,但他的参数有一些限制,必须是**可调用对象**,函数自然是可调用对象,类和生成器也是,我们会在接下来的小节说明。
一般来说,当接收到事件时,`NoneBot2` 会进行以下处理:
1. 准备依赖函数所需要的参数。
2. 调用依赖函数并获得返回值。
3. 将返回值作为事件处理函数中的参数值传入。
## 依赖缓存
在使用 `Depends` 包裹依赖函数时,有一个参数 `use_cache` ,它默认为 `True` ,这个参数会决定 `Nonebot2` 在依赖注入的处理中是否使用缓存。
```python {11}
import random
from nonebot import on_command
from nonebot.params import Depends
test = on_command("123")
async def always_run():
return random.randint(1, 100)
@test.handle()
async def _(x: int = Depends(always_run, use_cache=False)):
print(x)
```
:::tip
缓存是针对单次事件处理来说的,在事件处理中 `Depends` 第一次被调用时,结果存入缓存,在之后都会直接返回缓存中的值,在事件处理结束后缓存就会被清除。
:::
当使用缓存时,依赖注入会这样处理:
1. 查询缓存,如果缓存中有相应的值,则直接返回。
2. 准备依赖函数所需要的参数。
3. 调用依赖函数并获得返回值。
4. 将返回值存入缓存。
5. 将返回值作为事件处理函数中的参数值传入。
## 同步支持
我们在编写依赖函数时,可以简单地用同步函数,`NoneBot2` 的内部流程会进行处理:
```python {2,8-9,12}
from nonebot.log import logger
from nonebot.params import Depends # 1.引用 Depends
from nonebot import on_command, on_message
from nonebot.adapters.onebot.v11 import MessageEvent
test = on_command("123")
def depend(event: MessageEvent): # 2.编写同步依赖函数
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
@test.handle()
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
print(x["uid"], x["nickname"])
```
## Class 作为依赖
我们可以看下面的代码段:
```python
class A:
def __init__(self):
pass
a = A()
```
在我们实例化类 `A` 的时候,其实我们就在**调用**它,类本身也是一个**可调用对象**,所以类可以被 `Depends` 包裹成为依赖项。
因此我们对第一节的代码段做一下改造:
```python {2,7-10,13}
from nonebot import on_command
from nonebot.params import Depends # 1.引用 Depends
from nonebot.adapters.onebot.v11 import MessageEvent
test = on_command("123")
class DependClass: # 2.编写依赖类
def __init__(self, event: MessageEvent):
self.uid = event.get_user_id()
self.nickname = event.sender.nickname
@test.handle()
async def _(x: DependClass = Depends(DependClass)): # 3.在事件处理函数里声明依赖项
print(x.uid, x.nickname)
```
依然可以用三步说明如何用类作为依赖项:
1. 引用 `Depends` 。
2. 编写依赖类。类的 `__init__` 函数可以接收 `bot`, `event`, `state` 等参数,在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注。
3. 在事件处理函数中声明依赖项。当用类作为依赖项时,它会是一个对应的实例,在这里 `x` 就是 `DependClass` 实例。
### 另一种依赖项声明方式
当使用类作为依赖项时,`Depends` 的参数可以为空,`NoneBot2` 会根据参数的类型标注进行推断并进行依赖注入。
```python
@test.handle()
async def _(x: DependClass = Depends()): # 在事件处理函数里声明依赖项
print(x.uid, x.nickname)
```
## 生成器作为依赖
:::warning
`yield` 语句只能写一次,否则会引发异常。
如果对此有疑问并想探究原因,可以看 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档,实际上,`Nonebot2` 的内部就使用了这两个装饰器。
:::
:::tips
生成器是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
:::
与 `FastAPI` 一样,`NoneBot2` 的依赖注入支持依赖项在事件处理结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。
要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。
我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO
```python {3,7-10,13}
import httpx
from nonebot import on_command
from nonebot.params import Depends # 1.引用 Depends
test = on_command("123")
async def get_client(): # 2.编写异步生成器函数
async with httpx.AsyncClient() as client:
yield client
print("调用结束")
@test.handle()
async def _(x: httpx.AsyncClient = Depends(get_client)): # 3.在事件处理函数里声明依赖项
resp = await x.get("https://v2.nonebot.dev")
# do something
```
我们用 `yield` 代码段作为生成器函数的“返回”,在事件处理函数里用返回出来的 `client` 做自己需要的工作。在 `NoneBot2` 结束事件处理时,会执行 `yield` 之后的代码。
## 创造可调用对象作为依赖
:::tips
魔法方法 `__call__` 是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
:::
在 `Python` 的里,类的 `__call__` 方法会让类的实例变成**可调用对象**,我们可以利用这个魔法方法做一个简单的尝试:
```python{3,9-14,16,19}
from typing import Type
from nonebot.log import logger
from nonebot.params import Depends # 1.引用 Depends
from nonebot import on_command
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent
test = on_command("123")
class EventChecker: # 2.编写需要的类
def __init__(self, EventClass: Type[MessageEvent]):
self.event_class = EventClass
def __call__(self, event: MessageEvent) -> bool:
return isinstance(event, self.event_class)
checker = EventChecker(GroupMessageEvent) # 3.将类实例化
@test.handle()
async def _(x: bool = Depends(checker)): # 4.在事件处理函数里声明依赖项
if x:
print("这是群聊消息")
else:
print("这不是群聊消息")
```
这是判断 `onebot` 的消息事件是不是群聊消息事件的一个例子,我们可以用四步来说明这个例子:
1. 引用 `Depends` 。
2. 编写需要的类。类的 `__init__` 函数接收参数 `EventClass`,它将接收事件类本身。类的 `__call__` 函数将接受消息事件对象,并返回一个 `bool` 类型的判定结果。
3. 将类实例化。我们传入群聊消息事件作为参数实例化 `checker` 。
4. 在事件处理函数里声明依赖项。`NoneBot2` 将会调用 `checker` 的 `__call__` 方法,返回给参数 `x` 相应的判断结果。

View File

@@ -1,76 +0,0 @@
---
sidebar_position: 2
description: 重载事件处理函数
options:
menu:
weight: 61
category: advanced
---
# 事件处理函数重载
当我们在编写 NoneBot2 应用时,常常会遇到这样一个问题:该怎么让同一类型的不同事件执行不同的响应逻辑?又或者如何让不同的 `bot` 针对同一类型的事件作出不同响应?
针对这个问题, NoneBot2 提供一个便捷而高效的解决方案:事件处理函数重载机制。简单地说,`handler`(事件处理函数)会根据其参数的 `type hints`[PEP484 类型标注](https://www.python.org/dev/peps/pep-0484/))来对相对应的 `bot``event` 进行响应,并且会忽略不符合其参数类型标注的情况。
<!-- 必须要注意的是,该机制利用了 `inspect` 标准库获取到了事件处理函数的 `signature`(签名),进一步获取到参数名称和类型标注。故而,我们在编写 `handler` 时,参数的名称和类型标注必须要符合 `T_Handler` 规定,详情可以参看**指南**中的[事件处理](../../guide/creating-a-handler)。 -->
:::tip 提示
如果想了解更多关于 `inspect` 标准库的信息,可以查看[官方文档](https://docs.python.org/zh-cn/3.9/library/inspect.html)。
:::
下面,我们会以 OneBot 适配器中的群聊消息事件和私聊消息事件为例,对该机制的应用进行简单的介绍。
## 一个例子
首先,我们需要导入需要的方法、类型。
```python
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, PrivateMessageEvent
```
之后,我们可以注册一个 `Matcher` 来响应消息事件。
```python
matcher = on_command("test_overload")
```
最后,我们编写不同的 `handler` 并编写不同的类型标注来实现事件处理函数重载:
```python
@matcher.handle()
async def _(bot: Bot, event: GroupMessageEvent):
await matcher.send("群聊消息事件响应成功!")
@matcher.handle()
async def _(bot: Bot, event: PrivateMessageEvent):
await matcher.send("私聊消息事件响应成功!")
```
此时,我们可以在群聊或私聊中对我们的机器人发送 `test_overload`,它会在不同的场景做出不同的应答。
这样一个简单的事件处理函数重载就完成了。
## 进阶
事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。例如:
```python
@matcher.got("key1", prompt="群事件提问")
async def _(bot: Bot, event: GroupMessageEvent):
await matcher.send("群聊消息事件响应成功!")
@matcher.got("key2", prompt="私聊事件提问")
async def _(bot: Bot, event: PrivateMessageEvent):
await matcher.send("私聊消息事件响应成功!")
```
只有触发事件符合的函数才会触发装饰器。
:::warning 注意
bot 和 event 参数具有最高的检查优先级,因此,如果参数类型不符合,所有的依赖项 `Depends` 等都不会被执行。
:::

View File

@@ -0,0 +1,286 @@
---
sidebar_position: 0
description: 选择合适的驱动器运行机器人
options:
menu:
weight: 10
category: advanced
---
# 选择驱动器
驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。
:::important 提示
驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。
:::
:::tip 提示
如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。
:::
## 驱动器类型
驱动器的类型有两种:
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook接收 WebSocket 客户端连接等情形。
客户端型驱动器具有以下两种功能:
1. 异步发送 HTTP 请求,自定义 `HTTP Method``URL``Header``Body``Cookie``Proxy``Timeout` 等。
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL``Header``Cookie``Proxy``Timeout` 等。
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
## 配置驱动器
驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。
NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`<module>[:<Driver>][+<module>[:<Mixin>]]*`
其中,`<module>` 代表**驱动器模块路径**`<Driver>` 代表**驱动器类名**,默认为 `Driver``<Mixin>` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。
特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下:
```dotenv
DRIVER=~fastapi
DRIVER=~aiohttp
DRIVER=~httpx+~websockets
DRIVER=~fastapi+~httpx+~websockets
```
## 获取驱动器
在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例:
```python
from nonebot import get_driver
driver = get_driver()
```
## 内置驱动器
### None
**类型:**服务端驱动器
NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。
```env
DRIVER=~none
```
### FastAPI默认
**类型:**服务端驱动器
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。
```env
DRIVER=~fastapi
```
#### FastAPI 配置项
##### `fastapi_openapi_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。
##### `fastapi_docs_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。
##### `fastapi_redoc_url`
类型:`str | None`
默认值:`None`
说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。
##### `fastapi_include_adapter_schema`
类型:`bool`
默认值:`True`
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`
##### `fastapi_reload`
:::warning 警告
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
```bash
nb run --reload
```
开启该功能后,在 uvicorn 运行时FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`
> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529)[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070)[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257)
后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于
1. 不支持创建子进程
2. 最多只支持 512 个套接字
3. ...
> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)
所以,一些使用了 asyncio 的库因此可能无法正常工作,如:
1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。
:::
类型:`bool`
默认值:`False`
说明:是否开启 `uvicorn``reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
```python title=bot.py
app = nonebot.get_asgi()
nonebot.run(app="bot:app")
```
##### `fastapi_reload_dirs`
类型:`List[str] | None`
默认值:`None`
说明:重载监控文件夹列表,默认为 uvicorn 默认值
##### `fastapi_reload_delay`
类型:`float | None`
默认值:`None`
说明:重载延迟,默认为 uvicorn 默认值
##### `fastapi_reload_includes`
类型:`List[str] | None`
默认值:`None`
说明:要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `fastapi_reload_excludes`
类型:`List[str] | None`
默认值:`None`
说明:不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `fastapi_extra`
类型:`Dist[str, Any]`
默认值:`{}`
说明:传递给 `FastAPI` 的其他参数
### Quart
**类型:**`ReverseDriver`
> Quart is an asyncio reimplementation of the popular Flask microframework API.
[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。
```env
DRIVER=~quart
```
#### Quart 配置项
##### `quart_reload`
:::warning 警告
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
```bash
nb run --reload
```
:::
类型:`bool`
默认值:`False`
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
```python title=bot.py
app = nonebot.get_asgi()
nonebot.run(app="bot:app")
```
##### `quart_reload_dirs`
类型:`List[str] | None`
默认值:`None`
说明:重载监控文件夹列表,默认为 uvicorn 默认值
##### `quart_reload_delay`
类型:`float | None`
默认值:`None`
说明:重载延迟,默认为 uvicorn 默认值
##### `quart_reload_includes`
类型:`List[str] | None`
默认值:`None`
说明:要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `quart_reload_excludes`
类型:`List[str] | None`
默认值:`None`
说明:不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值
##### `quart_extra`
类型:`Dist[str, Any]`
默认值:`{}`
说明:传递给 `Quart` 的其他参数
### HTTPX
**类型:**`ForwardDriver`
:::warning 注意
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
:::
> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
```env
DRIVER=~httpx
```
### websockets
**类型:**`ForwardDriver`
:::warning 注意
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
:::
> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
```env
DRIVER=~websockets
```
### AIOHTTP
**类型:**`ForwardDriver`
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
```env
DRIVER=~aiohttp
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -0,0 +1,40 @@
---
sidebar_position: 10
description: 自定义事件响应器存储
options:
menu:
weight: 110
category: advanced
---
# 事件响应器存储
事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。
NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。
## 编写存储提供者
事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。
编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类:
```python
from nonebot.matcher import MatcherProvider
class CustomProvider(MatcherProvider):
...
```
## 设置存储提供者
我们可以通过 `matchers.set_provider` 方法设置存储提供者:
```python {3}
from nonebot.matcher import matchers
matchers.set_provider(CustomProvider)
assert isinstance(matchers.provider, CustomProvider)
```

View File

@@ -0,0 +1,420 @@
---
sidebar_position: 5
description: 事件响应器组成与内置响应规则
options:
menu:
weight: 60
category: advanced
---
# 事件响应器进阶
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。
## 事件响应器组成
### 事件响应器类型
事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。
NoneBot 内置了四种常用事件类型:`meta_event``message``notice``request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。
### 事件触发权限
事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。
### 事件响应规则
事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。
### 响应优先级
响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。
### 阻断
阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。
NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。
在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。
### 有效期
事件响应器的有效期分为 `temp``expire_time``temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。
### 默认状态
事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。
## 基本辅助函数
NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数:
- `on`:创建任何类型的事件响应器。
- `on_metaevent`:创建元事件响应器。
- `on_message`:创建消息事件响应器。
- `on_request`:创建请求事件响应器。
- `on_notice`:创建通知事件响应器。
除了 `on` 函数具有一个 `type` 参数外,其余参数均相同:
- `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。
- `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。
- `handlers`:事件处理函数列表。
- `temp`:是否为临时响应器。
- `expire_time`:响应器的过期时间。
- `priority`:响应器的优先级。
- `block`:是否阻断事件传播。
- `state`:响应器的默认状态。
在消息类型的事件响应器的基础上NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。
## 内置响应规则
### `startswith`
`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`
例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则:
```python
from nonebot.rule import startswith
rule = startswith(("!", "/"), ignorecase=False)
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_startswith
matcher = on_startswith(("!", "/"), ignorecase=False)
```
### `endswith`
`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`
例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则:
```python
from nonebot.rule import endswith
rule = endswith((".", "。"), ignorecase=False)
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_endswith
matcher = on_endswith((".", "。"), ignorecase=False)
```
### `fullmatch`
`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`
例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则:
```python
from nonebot.rule import fullmatch
rule = fullmatch(("ping", "pong"), ignorecase=False)
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_fullmatch
matcher = on_fullmatch(("ping", "pong"), ignorecase=False)
```
### `keyword`
`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。
例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则:
```python
from nonebot.rule import keyword
rule = keyword("hello", "hi")
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_keyword
matcher = on_keyword("hello", "hi")
```
### `command`
`command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。
例如,当我们配置了 `Command Start``/``Command Separator``.` 时:
```python
from nonebot.rule import command
# 匹配 "/help" 或者 "/帮助" 开头的消息
rule = command("help", "帮助")
# 匹配 "/help.cmd" 开头的消息
rule = command(("help", "cmd"))
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_command
matcher = on_command("help", aliases={"帮助"})
```
此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。
```python
rule = command("help", force_whitespace=True)
rule = command("help", force_whitespace=" ")
```
命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。
### `shell_command`
`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。
例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则:
```python
from nonebot.rule import shell_command, ArgumentParser
parser = ArgumentParser()
parser.add_argument("-v", "--verbose", action="store_true")
rule = shell_command("cmd", parser=parser)
```
更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`
直接使用辅助函数新建一个响应器:
```python
from nonebot import on_shell_command
from nonebot.rule import ArgumentParser
parser = ArgumentParser()
parser.add_argument("-v", "--verbose", action="store_true")
matcher = on_shell_command("cmd", parser=parser)
```
参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。
### `regex`
`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。
:::tip 提示
正则表达式匹配使用 search 而非 match如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。
:::
例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则:
```python
from nonebot.rule import regex
rule = regex(r"[a-z]+", flags=re.IGNORECASE)
```
也可以直接使用辅助函数新建一个响应器:
```python
from nonebot import on_regex
matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE)
```
正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。
### `to_me`
`to_me` 响应规则用于匹配事件是否与机器人相关。
例如:
```python
from nonebot.rule import to_me
rule = to_me()
```
### `is_type`
`is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。
例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则:
```python
from nonebot.rule import is_type
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
rule = is_type(PrivateMessageEvent, GroupMessageEvent)
```
## 响应器组
为了更方便的管理一系列功能相近的响应器NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。
### `CommandGroup`
`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。
例如,我们创建 `/cmd``/cmd.sub``/cmd.help` 三个命令,他们具有相同的优先级:
```python
from nonebot import CommandGroup
group = CommandGroup("cmd", priority=10)
cmd = group.command(tuple())
sub_cmd = group.command("sub")
help_cmd = group.command("help")
```
### `MatcherGroup`
`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。
例如,我们创建一个具有相同响应规则的响应器组:
```python
from nonebot.rule import to_me
from nonebot import MatcherGroup
group = MatcherGroup(rule=to_me())
matcher1 = group.on_message()
matcher2 = group.on_message()
```
## 第三方响应规则
### Alconna
[`nonebot-plugin-alconna`](https://github.com/ArcletProject/nonebot-plugin-alconna) 是一类提供了拓展响应规则的插件。
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
特点包括:
- 高效
- 直观的命令组件创建方式
- 强大的类型解析与类型转换功能
- 自定义的帮助信息格式
- 多语言支持
- 易用的快捷命令创建与使用
- 可创建命令补全会话, 以实现多轮连续的补全提示
- 可嵌套的多级子命令
- 正则匹配支持
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches``AlcResult`
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
#### 插件安装
```shell
nb plugin install nonebot-plugin-alconna
```
```shell
pip install nonebot-plugin-alconna
```
#### 示例
```python
from nonebot_plugin_alconna.adapters import At
from nonebot.adapters.onebot.v12 import Message
from nonebot_plugin_alconna.adapters.onebot12 import Image
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
alc = Alconna(
"role-group",
Subcommand(
"add",
Args["name", str],
Option("member", Args["target", MultiVar(At)]),
),
Option("list"),
)
rg = on_alconna(alc, auto_send_output=True)
@rg.handle()
async def _(result: Arparma = AlconnaMatches()):
if result.find("list"):
img = await gen_role_group_list_image()
await rg.finish(Message([Image(img)]))
if result.find("add"):
group = await create_role_group(result["add.name"])
if result.find("add.member"):
ats: tuple[Ob12MS, ...] = result["add.member.target"]
group.extend(member.data["user_id"] for member in ats)
await rg.finish("添加成功")
```
我们可以看到主要的两大组件:`Option``Subcommand`
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")``Option("--foo", alias=["-F"]`
`Subcommand` 则可以传入自己的 `Option``Subcommand`
他们拥有如下共同参数:
- `help_text`: 传入该组件的帮助信息
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
其次使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
`on_alconna` 的所有参数如下:
- `command: Alconna | str`: Alconna 命令
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
#### 参考
插件文档: [📦 这里](https://github.com/ArcletProject/nonebot-plugin-alconna/blob/master/docs.md)
官方文档: [👉 指路](https://arclet.top/)
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)

View File

@@ -1,95 +0,0 @@
---
sidebar_position: 3
description: 自定义事件响应器的响应权限
options:
menu:
weight: 40
category: advanced
---
# 权限控制
**权限控制**是机器人在实际应用中需要解决的重点问题之一NoneBot2 提供了灵活的权限控制机制——`Permission`,接下来我们将简单说明。
## 应用
如同 `Rule` 一样,`Permission` 可以在[定义事件响应器](../tutorial/plugin/create-matcher.md)时添加 `permission` 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
```python
from nonebot.permission import SUPERUSER
from nonebot import on_command
matcher = on_command("测试超管", permission=SUPERUSER)
@matcher.handle()
async def _():
await matcher.send("超管命令测试成功")
@matcher.got("key1", "超管提问")
async def _():
await matcher.send("超管命令 got 成功")
```
在这段代码中,我们事件响应器指定了 `SUPERUSER` 这样一个权限,那么机器人只会响应超级管理员的 `测试超管` 命令,并且会响应该超级管理员的连续对话。
:::tip 提示
在这里需要强调的是,`Permission``Rule` 的表现并不相同, `Rule` 只会在初次响应时生效,在余下的对话中并没有限制事件;但是 `Permission` 会持续生效,在连续对话中一直对事件主体加以限制。
:::
## 进阶
`Permission` 除了可以在注册事件响应器时加以应用,还可以在编写事件处理函数 `handler` 时主动调用,我们可以利用这个特性在一个 `handler` 里对不同权限的事件主体进行区别响应,下面我们以 OneBot 适配器中的 `GROUP_ADMIN`(普通管理员非群主)和 `GROUP_OWNER` 为例,说明下怎么进行主动调用。
```python
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot.adapters.onebot.v11 import GROUP_ADMIN, GROUP_OWNER
matcher = on_command("测试权限")
@matcher.handle()
async def _(bot: Bot, event: GroupMessageEvent):
if await GROUP_ADMIN(bot, event):
await matcher.send("管理员测试成功")
elif await GROUP_OWNER(bot, event):
await matcher.send("群主测试成功")
else:
await matcher.send("群员测试成功")
```
在这段代码里,我们并没有对命令的权限指定,这个命令会响应所有在群聊中的 `测试权限` 命令,但是在 `handler` 里,我们对两个 `Permission` 进行主动调用,从而可以对不同的角色进行不同的响应。
## 自定义
如同 `Rule` 一样,`Permission` 也是由非负数个 `PermissionChecker` 组成的,但只需其中一个返回 `True` 时就会匹配成功。下面是自定义 `PermissionChecker``Permission` 的示例:
```python
from nonebot.adapters import Bot, Event
from nonebot.permission import Permission
async def async_checker(bot: Bot, event: Event) -> bool:
return True
def sync_checker(bot: Bot, event: Event) -> bool:
return True
def check(arg1, arg2):
async def _checker(bot: Bot, event: Event) -> bool:
return bool(arg1 + arg2)
return Permission(_checker)
```
`Permission``PermissionChecker` 之间可以使用 `|`(或符号)互相组合:
```python
from nonebot.permission import Permission
Permission(async_checker1) | sync_checker | async_checker2
```
同样地,如果想用 `Permission(*checkers)` 包裹构造 `Permission`,函数必须是异步的;但是在利用 `|`或符号连接构造时NoneBot2 会自动包裹同步函数为异步函数。

View File

@@ -0,0 +1,97 @@
---
sidebar_position: 2
description: 填写与获取插件相关的信息
options:
menu:
weight: 30
category: advanced
---
# 插件信息
NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。
## 插件元数据
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
现在,假设我们有一个插件 `example`, 它的模块结构如下:
```tree {4-6} title=Project
📦 awesome-bot
├── 📂 awesome_bot
│ └── 📂 plugins
| └── 📂 example
| ├── 📜 __init__.py
| └── 📜 config.py
├── 📜 pyproject.toml
└── 📜 README.md
```
我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示:
```python {1,5-11} title=example/__init__.py
from nonebot.plugin import PluginMetadata
from .config import Config
__plugin_meta__ = PluginMetadata(
name="示例插件",
description="这是一个示例插件",
usage="没什么用",
config=Config,
extra={},
)
```
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有两个可选的属性。`config` 属性用于指定插件的[配置类](../appendices/config.mdx#插件配置)`extra` 属性,它是一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
## 获取插件信息
NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件:
```python
import nonebot
plugins: set[Plugin] = nonebot.get_loaded_plugins()
```
也可以通过插件索引名称获取插件对象:
```python
import nonebot
plugin: Plugin | None = nonebot.get_plugin("example")
```
或者通过模块路径获取插件对象:
```python
import nonebot
plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example")
```
如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数:
```python
import nonebot
plugin_names: set[str] = nonebot.get_available_plugin_names()
```
插件对象 `Plugin` 中包含了多个属性:
- `name`:插件索引名称
- `module`:插件模块
- `module_name`:插件模块路径
- `manager`:插件管理器
- `matcher`:插件中定义的事件响应器
- `parent_plugin`:插件的父插件
- `sub_plugins`:插件的子插件集合
- `metadata`:插件元数据
通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。

View File

@@ -0,0 +1,41 @@
---
sidebar_position: 3
description: 编写与加载嵌套插件
options:
menu:
weight: 40
category: advanced
---
# 嵌套插件
NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。
## 创建嵌套插件
我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件:
```bash
$ nb plugin create
[?] 插件名称: parent
[?] 使用嵌套插件? (y/N) Y
[?] 输出目录: awesome_bot/plugins
```
或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。
## 已有插件
如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码:
```python title=parent/__init__.py
import nonebot
from pathlib import Path
sub_plugins = nonebot.load_plugins(
str(Path(__file__).parent.joinpath("plugins").resolve())
)
```
这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。

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