Compare commits

...

837 Commits

Author SHA1 Message Date
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
github-actions[bot]
eceef1ebec 🔖 Release 2.0.0rc3 2023-01-22 08:17:26 +00:00
Ju4tCode
50aa8c53e0 🔖 bump version 2.0.0rc3 (#1620) 2023-01-22 16:10:57 +08:00
github-actions[bot]
558f740c13 📝 Update changelog 2023-01-22 07:31:16 +00:00
wwweww
8bffba7efd 🍻 publish adapter BilibiliLive (#1616) 2023-01-22 15:29:55 +08:00
github-actions[bot]
ce93ea13e7 📝 Update changelog 2023-01-22 07:26:24 +00:00
Limnium
174182d62a 🍻 publish plugin 反向词典 (#1618) 2023-01-22 15:25:20 +08:00
github-actions[bot]
36f047be7f 📝 Update changelog 2023-01-22 07:21:13 +00:00
lgc2333
9d73af0513 🍻 publish plugin PicMCStat (#1613) 2023-01-22 15:20:03 +08:00
github-actions[bot]
ecb0d78011 📝 Update changelog 2023-01-22 07:19:32 +00:00
Ju4tCode
5920efb6c5 📝 Docs: 修改更新部分文档 (#1615) 2023-01-22 15:18:26 +08:00
github-actions[bot]
5893fbe57d 📝 Update changelog 2023-01-22 03:41:58 +00:00
17TheWord
27557af636 🍻 publish adapter Spigot (#1611) 2023-01-22 11:40:49 +08:00
github-actions[bot]
b37c7995cb 📝 Update changelog 2023-01-22 03:32:38 +00:00
StarHeart
f46addbb85 Docs: 商店搜索大小写不敏感 (#1609) 2023-01-22 11:31:32 +08:00
github-actions[bot]
6f57a290d7 📝 Update changelog 2023-01-21 06:12:32 +00:00
bridgeL
ae66e45287 🍻 publish plugin 犯人在跳舞 (#1607) 2023-01-21 14:11:18 +08:00
github-actions[bot]
03cf7f290a 📝 Update changelog 2023-01-20 04:02:45 +00:00
Jigsaw
f203aaf4ca ✏️ Plugin: 移除 nonebot-plugin-puppet (#1605) 2023-01-20 12:01:35 +08:00
github-actions[bot]
9a2edbbeb1 📝 Update changelog 2023-01-18 07:26:26 +00:00
Rinfair-CSP-A016
bd9ca99f63 🍻 publish bot SuzunoBot (#1600) 2023-01-18 15:25:12 +08:00
github-actions[bot]
8be262d305 📝 Update changelog 2023-01-17 02:30:56 +00:00
nikissXI
b92d47b362 🍻 publish plugin 喵喵自记菜谱 (#1598) 2023-01-17 10:29:45 +08:00
github-actions[bot]
bdf8cb0d57 📝 Update changelog 2023-01-16 07:06:21 +00:00
itsevin
0cb65214c6 🍻 publish plugin 语音功能 (#1596) 2023-01-16 15:05:12 +08:00
github-actions[bot]
ccc2c5676a 📝 Update changelog 2023-01-16 06:54:42 +00:00
BigOrangeQWQ
6daec67ebd 🍻 publish plugin OrangeDice! (#1594) 2023-01-16 14:53:41 +08:00
github-actions[bot]
051851faed 📝 Update changelog 2023-01-16 06:43:25 +00:00
nikissXI
8d2fca3e12 🍻 publish plugin 简易谷歌翻译插件 (#1592) 2023-01-16 14:42:17 +08:00
github-actions[bot]
76f37c485c 📝 Update changelog 2023-01-16 03:54:28 +00:00
mengxinyuan638
0c7af0873f 🍻 publish plugin 哔哩哔哩q群登录 (#1590) 2023-01-16 11:53:03 +08:00
github-actions[bot]
31fa4ec5f4 📝 Update changelog 2023-01-16 03:06:47 +00:00
nikissXI
fda490d252 ✏️ Plugin: 更新 MC 的插件信息 (#1589) 2023-01-16 11:05:43 +08:00
github-actions[bot]
40e443fd1a 📝 Update changelog 2023-01-16 03:04:37 +00:00
Akirami
4a17e581d2 🔥 移除 nonebot-plugin-aidraw (#1588) 2023-01-16 11:03:24 +08:00
github-actions[bot]
081d212487 📝 Update changelog 2023-01-13 09:11:38 +00:00
mengxinyuan638
3d6774136f 🍻 publish plugin 原神实时公告 (#1584) 2023-01-13 17:10:28 +08:00
github-actions[bot]
fa934a156a 📝 Update changelog 2023-01-12 09:09:47 +00:00
bridgeL
bac5356a90 ✏️ Plugins: 更新 ayaka_games 插件名和描述 (#1586)
Co-authored-by: Su <wxlxy316@163.com>
2023-01-12 17:08:28 +08:00
github-actions[bot]
b289065f71 📝 Update changelog 2023-01-12 02:38:13 +00:00
dpm12345
09cf0f29ba ✏️ Plugin: 更新 tts_gal 插件名和描述 (#1581) 2023-01-12 10:36:58 +08:00
github-actions[bot]
244837dd7c 📝 Update changelog 2023-01-12 02:35:30 +00:00
mengxinyuan638
a0bc113912 🍻 publish bot 辞辞(cici)Bot (#1582) 2023-01-12 10:34:19 +08:00
github-actions[bot]
6f6a296105 📝 Update changelog 2023-01-12 02:29:19 +00:00
Monarchdos
a0d316127f 🍻 publish plugin 心灵鸡汤 (#1579) 2023-01-12 10:27:57 +08:00
github-actions[bot]
f0c0d7788f 📝 Update changelog 2023-01-11 08:52:35 +00:00
Akirami
3f7e2604f1 🔊 Feature: 添加事件响应器检查完成日志 (#1578)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2023-01-11 16:51:20 +08:00
github-actions[bot]
f43c0087f7 📝 Update changelog 2023-01-10 08:47:48 +00:00
ericzhang-debug
e71d841045 🍻 publish plugin Bing每日图片获取 (#1576) 2023-01-10 16:46:32 +08:00
github-actions[bot]
a3af8da331 📝 Update changelog 2023-01-09 06:25:42 +00:00
Ju4tCode
8bdfdaef91 ⬆️ Fix: 屏蔽 fastapi 0.89.0 (#1574) 2023-01-09 14:24:30 +08:00
github-actions[bot]
afd13ed65d 📝 Update changelog 2023-01-09 02:39:02 +00:00
mengxinyuan638
d83751d0ca 🍻 publish plugin 星座运势 (#1571) 2023-01-09 10:37:45 +08:00
github-actions[bot]
63dd3b8fa7 📝 Update changelog 2023-01-09 02:32:39 +00:00
hmzz804
ae689605a5 🍻 publish plugin 回声洞 (#1570) 2023-01-09 10:31:25 +08:00
github-actions[bot]
bbaba1c955 📝 Update changelog 2023-01-08 08:51:53 +00:00
Cvandia
f1ee54e5c9 🍻 publish plugin 整点报时 (#1568) 2023-01-08 16:50:38 +08:00
github-actions[bot]
6f8e532afe 📝 Update changelog 2023-01-08 07:22:21 +00:00
Akirami
6f68ff61e5 🔥 移除 nonebot_plugin_super_resolution (#1561) 2023-01-08 15:21:05 +08:00
github-actions[bot]
a930fc0997 📝 Update changelog 2023-01-07 09:54:17 +00:00
Jigsaw
65da0947fe ✏️ update OlivOS.nb2's module_name (#1560) 2023-01-07 17:52:56 +08:00
github-actions[bot]
1b64a54421 📝 Update changelog 2023-01-07 08:03:05 +00:00
Akirami
d4e1bb7bf3 🐛 修复子插件加载失败时没有从父插件中移除的问题 (#1559) 2023-01-07 16:01:56 +08:00
github-actions[bot]
d737679ccd 📝 Update changelog 2023-01-07 07:59:53 +00:00
SkyDynamic
4cef5512ee 🍻 publish plugin Hypixel数据查询 (#1555) 2023-01-07 15:58:47 +08:00
github-actions[bot]
1d5d1602f0 📝 Update changelog 2023-01-06 12:08:47 +00:00
Ju4tCode
87e767fa25 remove default fastapi installation (#1557) 2023-01-06 20:07:28 +08:00
github-actions[bot]
c38437a22f 📝 Update changelog 2023-01-06 04:43:30 +00:00
Ju4tCode
cafb7bedb4 add pyright config (#1554) 2023-01-06 12:42:20 +08:00
github-actions[bot]
ace053f387 📝 Update changelog 2023-01-06 03:51:04 +00:00
cpuopt
d6e176d03b 🍻 publish plugin 查找图片出处 (#1552) 2023-01-06 11:49:51 +08:00
github-actions[bot]
2fca5b9664 📝 Update changelog 2023-01-06 03:44:58 +00:00
Monarchdos
cd93ace0dd 🍻 publish plugin 云签到 (#1549) 2023-01-06 11:43:47 +08:00
github-actions[bot]
b118cb6f22 📝 Update changelog 2023-01-06 03:00:44 +00:00
istrashguy
a69ccb4e6c 🍻 publish plugin 图像标注 (#1546) 2023-01-06 10:59:15 +08:00
github-actions[bot]
d5ec31d0a0 📝 Update changelog 2023-01-04 06:55:42 +00:00
CMHopeSunshine
62560635b2 🍻 publish plugin 对对联 (#1541) 2023-01-04 14:54:29 +08:00
github-actions[bot]
c00430c53f 📝 Update changelog 2023-01-04 06:34:51 +00:00
CMHopeSunshine
1dcda4bd77 🍻 publish plugin 群聊学习 (#1539) 2023-01-04 14:33:25 +08:00
github-actions[bot]
b60035f0e6 📝 Update changelog 2023-01-04 05:51:54 +00:00
Umamusume-Agnes-Digital
8551b13eab 🍻 publish plugin 求生之路2——服务器操作 (#1531) 2023-01-04 13:50:41 +08:00
github-actions[bot]
b448a6e083 📝 Update changelog 2023-01-04 05:10:19 +00:00
nikissXI
956b202087 🍻 publish plugin setu_customization (#1533) 2023-01-04 13:08:54 +08:00
github-actions[bot]
b95d49cd9c 📝 Update changelog 2023-01-04 04:59:45 +00:00
Akirami
006b9dd816 Feature: 支持给 FastAPIQuart 传递额外的参数 (#1543) 2023-01-04 12:58:26 +08:00
github-actions[bot]
a9125cd696 📝 Update changelog 2023-01-04 04:49:29 +00:00
uy/sun
ee5dcf0d42 👷 CI: 优化触发条件减少无效运行 (#1545)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-04 12:48:10 +08:00
github-actions[bot]
f13c1cc980 📝 Update changelog 2023-01-03 16:23:05 +00:00
ssttkkl
16c0a87929 🍻 publish plugin 主动消息撤回 (#1535) 2023-01-04 00:21:51 +08:00
github-actions[bot]
39d1554905 📝 Update changelog 2023-01-03 16:14:15 +00:00
ANGJustinl
37067229b0 🍻 publish plugin HttpCat🐱猫猫http状态码 (#1528) 2023-01-04 00:13:05 +08:00
pre-commit-ci[bot]
5ca708d3f4 ⬆️ auto update by pre-commit hooks (#1530)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-03 13:54:27 +08:00
github-actions[bot]
53dded52a7 📝 Update changelog 2023-01-01 07:23:20 +00:00
Akirami
f8cee790e7 Feature: 添加 logger 重导出 (#1526)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-01 15:22:07 +08:00
bridgeL
10447ff3c4 🍻 publish plugin 命令探查 (#1523) 2023-01-01 15:20:53 +08:00
github-actions[bot]
f08aec7894 📝 Update changelog 2023-01-01 07:09:08 +00:00
uy/sun
69edb98835 Feature: 将 block driver 转正为 none 驱动器 (#1522) 2023-01-01 15:08:00 +08:00
github-actions[bot]
c73ca2b43f 📝 Update changelog 2023-01-01 07:05:09 +00:00
bridgeL
848c6c5061 ✏️ Plugin: 删除 ayaka_who_is_suspect 插件 (#1525)
Co-authored-by: Su <wxlxy316@163.com>
2023-01-01 15:03:41 +08:00
github-actions[bot]
58f82bf881 📝 Update changelog 2022-12-31 12:43:05 +00:00
uy/sun
9b3e670cee 🐛 修复异常在 traceback 中无法正常显示信息 (#1521) 2022-12-31 20:41:53 +08:00
github-actions[bot]
f0bebb65b4 📝 Update changelog 2022-12-30 15:09:51 +00:00
ANGJustinl
bc845c94e2 🍻 publish plugin AnimalVoice_Convert (#1517) 2022-12-30 23:08:41 +08:00
github-actions[bot]
f4668bf0bc 📝 Update changelog 2022-12-30 03:32:03 +00:00
uy/sun
3493d69fcd 👷 CI: 添加插件加载测试 (#1519)
* 👷 添加插件加载测试

* 调整命名格式

* 添加 issue_comment 的触发条件
2022-12-30 11:30:47 +08:00
github-actions[bot]
125bcb943f 📝 Update changelog 2022-12-29 12:27:17 +00:00
OREOCODEDEV
e65eb3fb18 🍻 publish plugin 服务状态查询 (#1512) 2022-12-29 20:26:12 +08:00
github-actions[bot]
8045420f97 📝 Update changelog 2022-12-29 11:28:33 +00:00
ANGJustinl
03a90006e5 🍻 publish plugin 腾讯云图像变换 (#1514) 2022-12-29 19:27:09 +08:00
github-actions[bot]
adbe341076 📝 Update changelog 2022-12-27 10:54:55 +00:00
Johnny Hsieh
2a623b1c81 🔥 Plugin: 移除停止维护的 nonebot-plugin-filehost (#1516) 2022-12-27 18:53:46 +08:00
github-actions[bot]
c91926aea6 📝 Update changelog 2022-12-27 03:08:47 +00:00
IAXRetailer
63329257de 🍻 publish bot RanBot (#1510) 2022-12-27 11:07:31 +08:00
github-actions[bot]
c9dc6e648e 📝 Update changelog 2022-12-26 03:13:47 +00:00
Ikaros
78a818547e ✏️ Plugin: 更新 abstain_diary 插件名和描述 (#1509) 2022-12-26 11:12:40 +08:00
github-actions[bot]
41eed9d0e9 📝 Update changelog 2022-12-26 02:57:23 +00:00
StarHeart
e32019f15d 📝 Docs: 更新测试文档中的连接方式&细化插件发布描述 (#1504) 2022-12-26 10:56:16 +08:00
github-actions[bot]
59c033e2dd 📝 Update changelog 2022-12-26 02:55:30 +00:00
zhulinyv
49cf1ec5d3 🍻 publish plugin Ping (#1507) 2022-12-26 10:54:07 +08:00
github-actions[bot]
e5ad15d6d6 📝 Update changelog 2022-12-25 16:18:00 +00:00
zhulinyv
4cf9790a95 🍻 publish plugin 群友召唤术 (#1502) 2022-12-26 00:16:32 +08:00
github-actions[bot]
7467e66dab 📝 Update changelog 2022-12-25 16:06:28 +00:00
050644zf
516c1c220c 🍻 publish plugin 战地群聊天插件 (#1505) 2022-12-26 00:05:05 +08:00
github-actions[bot]
136778ae5b 📝 Update changelog 2022-12-24 02:58:56 +00:00
chrisyy2003
17538f7a66 ✏️ Plugin: 更新 gpt3 插件模块名 (#1501) 2022-12-24 10:57:34 +08:00
github-actions[bot]
d1da0be0da 📝 Update changelog 2022-12-23 15:07:19 +00:00
bridgeL
e92bc24631 🍻 publish plugin 不要复读 (#1499) 2022-12-23 23:06:04 +08:00
github-actions[bot]
73d1e5dd88 📝 Update changelog 2022-12-21 06:53:36 +00:00
nikissXI
ea83ba78ec 🍻 publish plugin JAVA MC服务器信息查询 (#1496) 2022-12-21 14:52:23 +08:00
github-actions[bot]
712f80a307 📝 Update changelog 2022-12-21 06:40:17 +00:00
Ju4tCode
5c4ef8fc00 🐛 Fix: 修复客户端请求未处理 cookies (#1491) 2022-12-21 14:39:05 +08:00
github-actions[bot]
2b0973c9f5 📝 Update changelog 2022-12-21 05:59:28 +00:00
Ikaros
767a8ecb08 ✏️ Plugin: 更新 随机禁言 插件功能描述 (#1495) 2022-12-21 13:58:11 +08:00
github-actions[bot]
b342940b69 📝 Update changelog 2022-12-21 04:46:10 +00:00
Jerry080801
7880f07db4 🍻 publish plugin 防撤回 (#1488) 2022-12-21 12:44:42 +08:00
github-actions[bot]
ac702d7eb7 📝 Update changelog 2022-12-21 03:32:39 +00:00
chrisyy2003
9f3b3b2c32 ✏️ Plugin: 更新 multi chatgpt 插件仓库地址 (#1487) 2022-12-21 11:31:35 +08:00
Ju4tCode
2d08465426 add cookies support for forward driver 2022-12-20 10:13:45 +00:00
github-actions[bot]
827d8fbc0e 📝 Update changelog 2022-12-20 07:22:48 +00:00
Ikaros-521
0320be1947 🍻 publish plugin 随机禁言 (#1485) 2022-12-20 15:21:42 +08:00
github-actions[bot]
aca65954bd 📝 Update changelog 2022-12-20 07:04:06 +00:00
RShock
1da9376fc8 🍻 publish plugin 只因进化录 (#1483) 2022-12-20 15:02:47 +08:00
github-actions[bot]
909b811f68 📝 Update changelog 2022-12-18 07:54:38 +00:00
Ju4tCode
ceecf9c692 🐛 fix on_type typing error (#1482) 2022-12-18 15:53:25 +08:00
github-actions[bot]
3de2922773 📝 Update changelog 2022-12-17 09:05:55 +00:00
Ju4tCode
a5d26b7747 add pycln to auto remove unused imports (#1481) 2022-12-17 17:04:38 +08:00
github-actions[bot]
932f50d1fb 📝 Update changelog 2022-12-17 06:25:10 +00:00
chrisyy2003
c69619f142 🍻 publish plugin GPT3 (#1479) 2022-12-17 14:24:08 +08:00
github-actions[bot]
5a49d1e0e2 📝 Update changelog 2022-12-16 05:17:35 +00:00
ReiiNoki
4f9f3f449c 🍻 publish plugin 熊老板 (#1471) 2022-12-16 13:16:23 +08:00
github-actions[bot]
60733c97be 📝 Update changelog 2022-12-15 05:39:28 +00:00
Yuelioi
7a1aa0c204 🍻 publish plugin QQ群文件备份 (#1477) 2022-12-15 13:38:27 +08:00
github-actions[bot]
6d1383a10c 📝 Update changelog 2022-12-15 05:31:38 +00:00
Ikaros-521
86006fafdc 🍻 publish plugin 戒色打卡日记 (#1474) 2022-12-15 13:30:24 +08:00
github-actions[bot]
44ca4f729a 📝 Update changelog 2022-12-14 05:01:36 +00:00
wsdtl
cf29209a55 🍻 publish plugin nonebot_plugin_idiom (#1468) 2022-12-14 13:00:22 +08:00
github-actions[bot]
5e78e2bb5d 📝 Update changelog 2022-12-13 09:49:05 +00:00
幼稚园园长
440e15e204 📝 Docs: 修复文档中部分超链接跳转到 /store.html 的问题 (#1470) 2022-12-13 17:47:45 +08:00
github-actions[bot]
6711a84cab 📝 Update changelog 2022-12-12 14:39:08 +00:00
Ikaros-521
5c2d2141e3 🍻 publish plugin 随机配色方案 (#1465) 2022-12-12 22:37:49 +08:00
github-actions[bot]
8ec1552fd6 📝 Update changelog 2022-12-11 17:09:38 +00:00
chrisyy2003
c1dca723ae 🍻 publish plugin multi-ChatGPT (#1461) 2022-12-12 01:08:28 +08:00
github-actions[bot]
b6cd0424fa 📝 Update changelog 2022-12-10 09:28:58 +00:00
ssttkkl
1beee94c1d 🍻 publish plugin 权限控制 (#1463) 2022-12-10 17:27:48 +08:00
github-actions[bot]
f2a618f663 📝 Update changelog 2022-12-09 16:13:43 +00:00
Akirami
c4286f1f39 💡 补充 params 模块的类型注解 (#1458) 2022-12-10 00:12:28 +08:00
github-actions[bot]
cd9e30bd52 📝 Update changelog 2022-12-09 06:49:49 +00:00
A-kirami
24ae0dfa15 🍻 publish plugin 汇率换算 (#1451) 2022-12-09 14:48:44 +08:00
github-actions[bot]
19a20a3407 📝 Update changelog 2022-12-09 06:44:08 +00:00
Akirami
36d7b44741 Feature: 添加正则匹配文本注入 (#1457)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-09 14:42:54 +08:00
dependabot[bot]
8176cd189c ⬆️ Bump nwtgck/actions-netlify from 1 to 2 (#1456)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-09 12:19:22 +08:00
github-actions[bot]
b25de93eb1 📝 Update changelog 2022-12-07 17:06:48 +00:00
A-kirami
79e636ad89 🍻 publish plugin 全群广播 (#1449) 2022-12-08 01:05:31 +08:00
github-actions[bot]
9c9973d8d8 📝 Update changelog 2022-12-07 15:49:53 +00:00
Akirami
04d4954d50 📝 Docs: 移除文档 自定义日志 中多余的符号 (#1448) 2022-12-07 23:48:48 +08:00
github-actions[bot]
2c81dc1975 📝 Update changelog 2022-12-07 13:44:39 +00:00
Akirami
850096ceaa 📝 Docs: 完善 调用平台 API 部分 (#1447)
Co-authored-by: StarHeart <starheart233@gmail.com>
2022-12-07 21:43:27 +08:00
github-actions[bot]
6bb15f6533 📝 Update changelog 2022-12-06 15:12:57 +00:00
Ikaros-521
92c6a8dd6e 🍻 publish plugin 图片背景消除 (#1445) 2022-12-06 23:11:44 +08:00
github-actions[bot]
723eef10bb 📝 Update changelog 2022-12-06 06:20:53 +00:00
Akirami
06c33ad6ea Feature: 支持主动销毁事件响应器 (#1444)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-06 14:19:48 +08:00
github-actions[bot]
9826bc29ca 📝 Update changelog 2022-12-05 15:22:06 +00:00
ssttkkl
f35b5b57b7 🍻 publish plugin 雀魂信息查询 (#1442) 2022-12-05 23:20:54 +08:00
github-actions[bot]
433b50c79c 📝 Update changelog 2022-12-05 08:44:04 +00:00
A-kirami
89f3496a2a 🍻 publish plugin ChatGPT (#1438) 2022-12-05 16:42:59 +08:00
github-actions[bot]
df7e8f6d83 📝 Update changelog 2022-12-04 05:23:34 +00:00
ZYKsslm
b57f17d447 🍻 publish plugin 免费快捷点歌插件 (#1435) 2022-12-04 13:22:09 +08:00
github-actions[bot]
665de1da3e 📝 Update changelog 2022-12-04 02:44:19 +00:00
Ikaros-521
091232e996 🍻 publish plugin 动画截图追溯来源 (#1433) 2022-12-04 10:43:09 +08:00
github-actions[bot]
9ee2d94f3c 📝 Update changelog 2022-12-03 06:35:43 +00:00
MingxuanGame
78bdfe65ba ✏️ Docs: 修正文档中部分配置文件示例的符号误用 (#1432)
* :typo: 使用方括号替换大括号

* ✏️ 修改 2.0.0rc2 的文档
2022-12-03 14:34:25 +08:00
github-actions[bot]
5006cf7be6 📝 Update changelog 2022-12-02 06:42:02 +00:00
jcjrobert
a818e0056e 🍻 publish plugin b站图片下载 (#1429) 2022-12-02 14:40:59 +08:00
github-actions[bot]
3efae8bfbc 📝 Update changelog 2022-12-02 05:57:28 +00:00
bridgeL
024d97b997 🍻 Plugin: 更新 ayaka_games 介绍 (#1431)
Co-authored-by: Su <wxlxy316@163.com>
2022-12-02 13:56:15 +08:00
github-actions[bot]
c90ab949d2 📝 Update changelog 2022-11-29 04:01:31 +00:00
Ju4tCode
e8ffa63b78 🐛 fix argument parser message (#1426) 2022-11-29 12:00:09 +08:00
github-actions[bot]
6c27ec7357 📝 Update changelog 2022-11-27 11:38:28 +00:00
Sena
9bf08593d7 ✏️ Plugin: 修改 novelai send magiadice 插件模块名 (#1423) 2022-11-27 19:37:15 +08:00
github-actions[bot]
b016a59a38 📝 Update changelog 2022-11-27 11:22:19 +00:00
Passerby-D
794395e737 🍻 publish plugin 记事本 (#1419) 2022-11-27 19:21:17 +08:00
github-actions[bot]
a758e6f06e 📝 Update changelog 2022-11-26 02:33:35 +00:00
monsterxcn
11feb2c0d0 🍻 publish plugin 原神前瞻直播兑换码查询 (#1421) 2022-11-26 10:32:16 +08:00
Ju4tCode
59a2ed7c2e 🎨 format code 2022-11-25 16:12:23 +00:00
github-actions[bot]
d83866f03b 🔖 Release 2.0.0rc2 2022-11-24 03:55:37 +00:00
Ju4tCode
cb83e76e16 📝 remove old doc version (#1417) 2022-11-24 11:50:20 +08:00
Ju4tCode
1644615462 🔖 bump version 2.0.0rc2 2022-11-24 03:35:31 +00:00
github-actions[bot]
89d8abf863 📝 Update changelog 2022-11-24 02:59:33 +00:00
p0ise
f8cf7c94ae 🍻 publish plugin 谁在窥屏 (#1415) 2022-11-24 10:58:29 +08:00
github-actions[bot]
bef494615f 📝 Update changelog 2022-11-24 02:58:06 +00:00
那个小白白白
6e110e725e 📝 Docs: 添加 ntchat 社区适配器 (#1414)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-24 10:56:53 +08:00
Ju4tCode
85390a14b6 ⬆️ upgrade dependencies (#1413) 2022-11-21 19:59:39 +08:00
github-actions[bot]
276041e314 📝 Update changelog 2022-11-21 10:46:01 +00:00
Ju4tCode
2922da7b2f Feature: 支持自定义 matchers 存储管理 (#1395) 2022-11-21 18:44:55 +08:00
github-actions[bot]
c783ab5e9b 📝 Update changelog 2022-11-20 09:42:50 +00:00
ZYKsslm
139190bff7 🍻 publish plugin 免费版NovelAI生图插件 (#1407) 2022-11-20 17:41:05 +08:00
github-actions[bot]
1524434444 📝 Update changelog 2022-11-19 16:05:11 +00:00
Ikaros
b6857d59b8 ✏️ 更新 nonebot_plugin_searchBiliInfo 插件标题和描述 (#1410) 2022-11-20 00:03:54 +08:00
github-actions[bot]
a7b0eb10a0 📝 Update changelog 2022-11-19 08:15:27 +00:00
synodriver
0eadb44e20 🐛 Fix: Bot __getattr__ 不再对 __xxx__ 方法返回 (#1398) 2022-11-19 16:14:03 +08:00
github-actions[bot]
6b43209d37 📝 Update changelog 2022-11-15 02:51:59 +00:00
Ju4tCode
a50990bef2 🐛 fix run pre/post hook not in context (#1391) 2022-11-15 10:50:52 +08:00
github-actions[bot]
f1525c1ecd 📝 Update changelog 2022-11-14 17:11:56 +00:00
Kaguya233qwq
7df9756205 🍻 publish plugin sky光遇 (#1393) 2022-11-15 01:10:50 +08:00
github-actions[bot]
376a720881 📝 Update changelog 2022-11-14 11:06:42 +00:00
Ju4tCode
c7377647fa Feature: 升级 devcontainer 配置 (#1392)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-14 19:05:34 +08:00
github-actions[bot]
cfbd6f1e4d 📝 Update changelog 2022-11-13 10:02:16 +00:00
EtherLeaF
cecc853f25 🍻 publish plugin Colab-NovelAI (#1389) 2022-11-13 18:01:08 +08:00
github-actions[bot]
fe5f85517e 📝 Update changelog 2022-11-13 09:38:21 +00:00
Ikaros-521
66040a7e44 🍻 publish plugin b站用户直播号、粉丝、舰团数查询 (#1384) 2022-11-13 17:37:22 +08:00
github-actions[bot]
81d2f017f6 📝 Update changelog 2022-11-12 10:23:32 +00:00
Akirami
4355025f87 Feature: 使用 importlib.metadata 替换 pkg_resources (#1388)
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-11-12 18:22:16 +08:00
github-actions[bot]
0bc8a39578 📝 Update changelog 2022-11-10 17:23:35 +00:00
Aziteee
7308f57776 🍻 publish plugin 投胎模拟器 (#1381) 2022-11-11 01:22:12 +08:00
github-actions[bot]
7de8912edb 📝 Update changelog 2022-11-09 15:55:14 +00:00
H-xiaoH
42a4edc3ee 🍻 publish plugin Apex API Query (#1374) 2022-11-09 23:54:00 +08:00
github-actions[bot]
4a12429a38 📝 Update changelog 2022-11-09 14:38:12 +00:00
Qianyiovo
092f6d05f5 🍻 publish bot Bread Dog Bot (#1379) 2022-11-09 22:37:07 +08:00
github-actions[bot]
2c0c05dca1 📝 Update changelog 2022-11-09 14:34:19 +00:00
Sena
31bafc832f 🍻 Plugin: Sena-nana项目拆分,地址更改 (#1378) 2022-11-09 22:33:08 +08:00
github-actions[bot]
e1720d8ea0 📝 Update changelog 2022-11-06 16:04:17 +00:00
jcjrobert
08be5724b9 🍻 publish plugin 随个人 (#1372) 2022-11-07 00:03:00 +08:00
github-actions[bot]
828714a4e3 📝 Update changelog 2022-11-06 06:58:29 +00:00
Melodyknit
a79eeb73a6 🍻 publish plugin 动漫资源获取 (#1370) 2022-11-06 14:57:18 +08:00
github-actions[bot]
c1aec637d5 📝 Update changelog 2022-11-04 07:19:58 +00:00
ssttkkl
12cc08efbd 🍻 publish plugin 日麻小工具 (#1364) 2022-11-04 15:18:35 +08:00
github-actions[bot]
f410af72dc 📝 Update changelog 2022-11-04 01:30:33 +00:00
StarHeart
113021cdf4 👷 CI: 测试环境添加 Python 3.11 (#1366)
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-04 09:29:13 +08:00
github-actions[bot]
fcc23f98f8 📝 Update changelog 2022-11-03 11:45:38 +00:00
gzy02
0b693c419e 🍻 publish bot hsbot (#1368) 2022-11-03 19:44:34 +08:00
github-actions[bot]
36e5b81510 📝 Update changelog 2022-11-02 17:05:59 +00:00
A-kirami
71f17bebaa 🍻 publish plugin 图像超分辨率增强 (#1361) 2022-11-03 01:04:38 +08:00
github-actions[bot]
2f45f25d13 📝 Update changelog 2022-11-02 16:57:54 +00:00
A-kirami
17d52446c3 🍻 publish plugin 二次元化图像 (#1359) 2022-11-03 00:56:36 +08:00
pre-commit-ci[bot]
2304aaf22b ⬆️ auto update by pre-commit hooks (#1358)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-01 14:12:53 +08:00
github-actions[bot]
a70c37de69 📝 Update changelog 2022-10-30 17:26:28 +00:00
ssttkkl
1cabc18277 🍻 publish plugin 日麻寄分器 (#1356) 2022-10-31 01:25:22 +08:00
github-actions[bot]
ed1235ed11 📝 Update changelog 2022-10-30 17:02:55 +00:00
MeetWq
9952b4e838 🍻 publish plugin 文本生成器 (#1354) 2022-10-31 01:01:33 +08:00
github-actions[bot]
e81390a104 📝 Update changelog 2022-10-30 16:16:02 +00:00
tkgs0
7bc22289b4 🍻 publish plugin 反嘴臭插件 (#1349) 2022-10-31 00:14:51 +08:00
github-actions[bot]
bfa4a079bf 📝 Update changelog 2022-10-28 07:06:09 +00:00
tkgs0
403850262b 🍻 publish plugin 用户&群聊黑名单 (#1347) 2022-10-28 15:05:08 +08:00
github-actions[bot]
ed25e7aa39 📝 Update changelog 2022-10-27 03:00:53 +00:00
ssttkkl
7ab480f044 🍻 publish plugin NoneBot SQLAlchemy封装 (#1344) 2022-10-27 10:59:36 +08:00
github-actions[bot]
d7d6152094 📝 Update changelog 2022-10-27 02:57:35 +00:00
bridgeL
2954e58c77 Plugin: 更新 ayaka 插件的主页链接 (#1346)
Co-authored-by: Su <wxlxy316@163.com>
2022-10-27 10:56:23 +08:00
github-actions[bot]
f684b96433 📝 Update changelog 2022-10-24 02:25:32 +00:00
jcjrobert
4388d1c9e6 🍻 publish plugin 通用抽图/语音 (#1340) 2022-10-24 10:24:15 +08:00
github-actions[bot]
876acf3e88 📝 Update changelog 2022-10-20 13:19:39 +00:00
Kaguya233qwq
963e73f517 🍻 publish plugin kfcrazy (#1338) 2022-10-20 21:18:27 +08:00
github-actions[bot]
bdc9e44142 📝 Update changelog 2022-10-20 03:14:13 +00:00
A-kirami
4f2efb7304 🍻 publish plugin 二次元图像鉴赏 (#1336) 2022-10-20 11:12:51 +08:00
github-actions[bot]
6ae891124f 📝 Update changelog 2022-10-19 11:27:42 +00:00
bridgeL
145bd4d4e1 🍻 publish plugin ayaka衍生插件 - 坏词撤回 (#1334) 2022-10-19 19:26:39 +08:00
github-actions[bot]
ab54049909 📝 Update changelog 2022-10-18 14:07:59 +00:00
Sena
7aa554f5a2 ✏️ Plugin: 补充 novelai 插件信息 (#1333) 2022-10-18 22:06:47 +08:00
github-actions[bot]
5566777374 📝 Update changelog 2022-10-18 09:57:33 +00:00
bridgeL
b2594f61de 🍻 publish plugin ayaka衍生插件 - 时区助手 (#1331) 2022-10-18 17:56:25 +08:00
github-actions[bot]
1ad1e0606c 📝 Update changelog 2022-10-18 07:07:05 +00:00
bridgeL
583d5060db 🍻 publish plugin ayaka衍生插件 - 谁是卧底 (#1329) 2022-10-18 15:05:45 +08:00
github-actions[bot]
eaa3dbdfa8 📝 Update changelog 2022-10-17 03:00:47 +00:00
bridgeL
03f378690a 🍻 publish plugin ayaka衍生插件 - 小游戏合集 (#1327) 2022-10-17 10:59:43 +08:00
github-actions[bot]
512c66ccc0 📝 Update changelog 2022-10-15 07:07:21 +00:00
lgc2333
9d20c7510a 🍻 publish plugin bnhhsh -「不能好好说话!」 (#1325) 2022-10-15 15:06:04 +08:00
github-actions[bot]
29ad8a6686 📝 Update changelog 2022-10-14 01:59:51 +00:00
Ju4tCode
db534b8824 Feature: 新增 dotenv 嵌套配置项支持 (#1324)
Co-authored-by: hemengyang <hmy0119@hotmail.com>
2022-10-14 09:58:44 +08:00
github-actions[bot]
67b96528af 📝 Update changelog 2022-10-13 04:47:18 +00:00
A-kirami
a5929f80f7 🍻 publish plugin AI绘图 (#1322) 2022-10-13 12:45:56 +08:00
github-actions[bot]
9619477a27 📝 Update changelog 2022-10-12 05:42:41 +00:00
Akirami
8377680fd7 Feature: 添加 State 响应器触发消息注入 (#1315) 2022-10-12 13:41:28 +08:00
github-actions[bot]
3e3d6f91a5 📝 Update changelog 2022-10-12 03:05:31 +00:00
sena-nana
1d3d886004 🍻 publish plugin novelai (#1318) 2022-10-12 11:04:34 +08:00
github-actions[bot]
ebc5a3cc9e 📝 Update changelog 2022-10-12 02:53:57 +00:00
Kaguyaya
1092767a51 🍻 publish plugin 游戏王小程序查价 (#1316) 2022-10-12 10:52:56 +08:00
github-actions[bot]
ab227ee64b 📝 Update changelog 2022-10-11 11:53:01 +00:00
cjladmin
00b37fb3d2 🍻 publish plugin 监测群事件 (#900) 2022-10-11 19:51:56 +08:00
github-actions[bot]
e6494dc98e 📝 Update changelog 2022-10-10 14:06:29 +00:00
JustUndertaker
c80869f952 🍻 publish adapter Ntchat (#1313) 2022-10-10 22:05:27 +08:00
github-actions[bot]
2be72eac5e 📝 Update changelog 2022-10-10 12:36:47 +00:00
HornCopper
830c4f8c6a 🍻 Bot: 修改 Inkar Suki 描述 (#1312) 2022-10-10 20:35:45 +08:00
github-actions[bot]
138fb458b7 📝 Update changelog 2022-10-08 08:18:40 +00:00
KarisAya
2c93f82ef3 🍻 publish plugin 轮盘禁言小游戏 (#1310) 2022-10-08 16:17:42 +08:00
github-actions[bot]
77aa16c2fc 📝 Update changelog 2022-10-07 02:33:18 +00:00
Ankhyty
945da7151e 🍻 publish plugin 真白萌自动签到 (#1307) 2022-10-07 10:32:15 +08:00
github-actions[bot]
41ea0df0a5 📝 Update changelog 2022-10-06 03:51:25 +00:00
Ju4tCode
cec45cf89c ⚰️ remove dead namespace (#1306) 2022-10-06 11:50:18 +08:00
Ju4tCode
2de8c66c70 ⬆️ upgrade pydantic dependency (#1305) 2022-10-05 15:13:54 +08:00
github-actions[bot]
0bcc4277e5 📝 Update changelog 2022-10-05 02:54:10 +00:00
Shadow403
d3dd93b36c 🍻 publish plugin BiliRequestAll (#1301) 2022-10-05 10:53:00 +08:00
github-actions[bot]
e9bd81d9bb 📝 Update changelog 2022-10-03 11:45:36 +00:00
17TheWord
997d4f5042 ✏️ 修改插件 MCQQ 主页地址 (#1303) 2022-10-03 19:44:21 +08:00
github-actions[bot]
3bb321c519 📝 Update changelog 2022-10-03 11:42:16 +00:00
AbCooly
fe92d29322 🍻 publish plugin 监听者 (#1298) 2022-10-03 19:41:11 +08:00
github-actions[bot]
3234871b53 🔖 Release 2.0.0-rc.1 2022-10-02 08:05:19 +00:00
Ju4tCode
03543f01f2 🔖 bump version 2.0.0rc1 (#1300) 2022-10-02 15:49:31 +08:00
github-actions[bot]
ba5c0303c7 📝 Update changelog 2022-09-29 09:21:16 +00:00
Ju4tCode
e56fdd04ad 🍻 publish adapter GitHub (#1297) 2022-09-29 17:20:05 +08:00
github-actions[bot]
9f10bb70db 📝 Update changelog 2022-09-29 08:57:20 +00:00
Ju4tCode
71aad502d1 🐛 Fix: 内置规则和权限没有捕获错误 (#1291) 2022-09-29 16:56:06 +08:00
github-actions[bot]
ab85b8651e 📝 Update changelog 2022-09-29 08:43:25 +00:00
NewYearPrism
4fe8929441 🍻 publish plugin 文字识别 (#1294) 2022-09-29 16:42:06 +08:00
github-actions[bot]
5c303710f6 📝 Update changelog 2022-09-29 08:17:33 +00:00
RandomEnch
68d2ada94b 🍻 publish plugin 在线编曲 (#1292) 2022-09-29 16:16:07 +08:00
github-actions[bot]
75470fe157 📝 Update changelog 2022-09-25 15:24:53 +00:00
koking0
47b3fc516a 🍻 publish plugin 图灵机器人 (#1288) 2022-09-25 23:23:49 +08:00
github-actions[bot]
84c24b014f 📝 Update changelog 2022-09-23 12:26:33 +00:00
lgc2333
756cde6525 🍻 publish plugin PicStatus (#1286) 2022-09-23 20:25:22 +08:00
github-actions[bot]
57ef19af94 📝 Update changelog 2022-09-21 08:58:18 +00:00
Kaguya233qwq
5927b517e2 🍻 publish plugin 阿里云盘福利码自动兑换 (#1282) 2022-09-21 16:57:01 +08:00
github-actions[bot]
132205bfcc 📝 Update changelog 2022-09-21 08:49:44 +00:00
dpm12345
b31dfa9ab0 🍻 publish plugin gal角色语音生成 (#1280) 2022-09-21 16:48:17 +08:00
github-actions[bot]
b249802c38 📝 Update changelog 2022-09-19 09:56:45 +00:00
Todysheep
9df705aaa7 🍻 publish plugin 漂流瓶 (#1278) 2022-09-19 17:55:38 +08:00
github-actions[bot]
a0df535f0c 📝 Update changelog 2022-09-18 14:36:17 +00:00
AkiraXie
31022a653d Feature: SUPERUSER 权限匹配任意超管事件 (#1275)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-18 22:33:36 +08:00
github-actions[bot]
ba77443dde 📝 Update changelog 2022-09-18 14:30:09 +00:00
XZhouQD
984f743097 🍻 publish plugin BWIKI助手移植版 (#1273) 2022-09-18 22:29:03 +08:00
github-actions[bot]
638a9c94af 📝 Update changelog 2022-09-16 11:52:43 +00:00
su226
92f1d5a4d7 🍻 publish bot IdhagnBot (#1266) 2022-09-16 19:51:35 +08:00
github-actions[bot]
248af2ae1a 📝 Update changelog 2022-09-15 13:58:15 +00:00
littlebutt
4c37be7312 🍻 publish plugin nonebot物联网插件 (#1264) 2022-09-15 21:56:57 +08:00
github-actions[bot]
2cb8eafa81 📝 Update changelog 2022-09-13 02:47:25 +00:00
CMHopeSunshine
05bff5ec17 🍻 publish bot LittlePaimon (#1255) 2022-09-13 10:46:18 +08:00
github-actions[bot]
13245cb58f 📝 Update changelog 2022-09-12 13:27:14 +00:00
AbCooly
37bc7326b5 🍻 publish plugin 狼人杀插件 (#1251) 2022-09-12 21:25:50 +08:00
github-actions[bot]
f6d189d8c5 📝 Update changelog 2022-09-12 04:30:37 +00:00
bridgeL
600ef7031f 🍻 publish plugin ayaka - 文字游戏开发辅助插件 (#1253) 2022-09-12 12:29:30 +08:00
github-actions[bot]
7bedf7c8d0 📝 Update changelog 2022-09-11 12:59:20 +00:00
ppxxxg22
f62ee5893c 🍻 publish plugin 图像超分辨率重建 (#1249) 2022-09-11 20:57:55 +08:00
github-actions[bot]
71234e9a68 📝 Update changelog 2022-09-10 12:56:12 +00:00
Akirami
3bbca0fa70 Feature: 改进 CommandGroupMatcherGroup 的结构 (#1240) 2022-09-10 20:54:49 +08:00
github-actions[bot]
20f144ba93 📝 Update changelog 2022-09-09 10:53:37 +00:00
Akirami
4c8bc9f0cb 🍻 Fix: 修正 GenshinUID 的发布类型 (#1243) 2022-09-09 18:52:12 +08:00
github-actions[bot]
064509f26b 📝 Update changelog 2022-09-09 03:54:19 +00:00
Ju4tCode
8c42490a7e 🔇 Feature: 调整日志输出格式与等级 (#1233) 2022-09-09 11:52:57 +08:00
github-actions[bot]
179d7105c9 📝 Update changelog 2022-09-09 02:16:34 +00:00
KarisAya
1c14e638c8 🍻 publish plugin Minecraft Server 聊天同步 (#1244) 2022-09-09 10:15:26 +08:00
github-actions[bot]
c6eef06b55 📝 Update changelog 2022-09-08 02:38:39 +00:00
Akirami
beef564a22 🔥 remove unused imports (#1236) 2022-09-08 10:37:16 +08:00
github-actions[bot]
672f2ceecc 📝 Update changelog 2022-09-07 02:25:49 +00:00
Sclock
28142402d7 🍻 publish plugin 查询ETH合并日期 (#1231) 2022-09-07 10:24:39 +08:00
github-actions[bot]
b886329fb8 📝 Update changelog 2022-09-07 02:00:34 +00:00
Ju4tCode
a0b186aff3 ♻️ improve dependent structure (#1227) 2022-09-07 09:59:05 +08:00
pre-commit-ci[bot]
595c64e760 ⬆️ auto update by pre-commit hooks (#1229)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-06 09:54:57 +08:00
github-actions[bot]
5114749073 📝 Update changelog 2022-09-05 06:23:20 +00:00
84227871
af2d7b5797 🍻 publish bot GenshinUID (#1225) 2022-09-05 14:21:53 +08:00
github-actions[bot]
56943c0908 📝 Update changelog 2022-09-05 05:52:33 +00:00
SDIJF1521
45478deb95 🍻 publish bot 小白机器人 (#1223) 2022-09-05 13:51:16 +08:00
github-actions[bot]
d281ec5bf9 📝 Update changelog 2022-09-05 02:02:23 +00:00
17TheWord
291a7cbb8b 🍻 publish plugin 星际战甲事件查询 (#1219) 2022-09-05 10:01:20 +08:00
github-actions[bot]
41259546bd 📝 Update changelog 2022-09-04 11:31:42 +00:00
Ljzd-PRO
f96038241f ✏️ 更新插件米游社辅助工具 tag (#1221) 2022-09-04 19:30:32 +08:00
github-actions[bot]
b051320d78 📝 Update changelog 2022-09-04 03:18:37 +00:00
Ljzd-PRO
d12efac9f4 🍻 publish plugin 米游社辅助工具 (#1217) 2022-09-04 11:17:20 +08:00
github-actions[bot]
f87a38a30a 📝 Update changelog 2022-09-03 02:27:15 +00:00
monsterxcn
bf016b3f69 🍻 publish plugin 原神每日材料查询 (#1215) 2022-09-03 10:26:07 +08:00
github-actions[bot]
373f5255f1 📝 Update changelog 2022-09-02 05:41:40 +00:00
Ju4tCode
5a35015195 📝 update documentation 2022-09-02 13:40:24 +08:00
Melodyknit
d3a2f1dc08 🍻 publish adapter Console (#1212) 2022-09-02 13:40:24 +08:00
github-actions[bot]
f1aec4eb10 📝 Update changelog 2022-09-01 02:42:53 +00:00
Ju4tCode
cd30be21ba 🐛 fix nested user permission update (#1208) 2022-09-01 10:41:43 +08:00
17TheWord
f150a9ee89 🍻 publish plugin MC_QQ_MCRcon (#1210) 2022-09-01 10:41:08 +08:00
github-actions[bot]
e68281f60f 📝 Update changelog 2022-09-01 02:05:08 +00:00
monsterxcn
32be64485a 🍻 publish plugin 原神角色展柜查询 (#1207) 2022-09-01 10:03:55 +08:00
github-actions[bot]
c76f492305 📝 Update changelog 2022-08-31 07:46:34 +00:00
s52047qwas
29b0351644 🍻 publish plugin 修仙模拟器 (#1195) 2022-08-31 15:45:27 +08:00
github-actions[bot]
ef3350fd9c 📝 Update changelog 2022-08-31 06:57:46 +00:00
Raidenneox
459699de5c 🍻 publish plugin 赛博浅草寺 (#1205) 2022-08-31 14:56:25 +08:00
github-actions[bot]
31c3eb8fd6 📝 Update changelog 2022-08-31 02:08:36 +00:00
Lan
1cfdee2645 Featue: load_plugin 支持 pathlib.Path (#1194)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-08-31 10:07:14 +08:00
github-actions[bot]
4e76518a58 📝 Update changelog 2022-08-31 02:05:28 +00:00
GC-ZF
b53e029df1 🍻 publish plugin 不背单词 (#1203) 2022-08-31 10:04:01 +08:00
github-actions[bot]
c1faf68806 📝 Update changelog 2022-08-30 11:39:33 +00:00
CofinCup
fe64b904ff 🍻 publish plugin 自识别todo (#1192) 2022-08-30 19:38:24 +08:00
github-actions[bot]
a621ade449 📝 Update changelog 2022-08-30 01:55:21 +00:00
Ju4tCode
3fda978064 Feature: 新增事件类型过滤 rule (#1183) 2022-08-30 09:54:09 +08:00
github-actions[bot]
60ab93164c 📝 Update changelog 2022-08-28 12:12:28 +00:00
sena-nana
2b22d5abda 🍻 publish plugin 雨课堂自动签到 (#1188) 2022-08-28 20:11:22 +08:00
github-actions[bot]
e3a4834383 📝 Update changelog 2022-08-27 14:30:10 +00:00
sena-nana
21087036af 🍻 publish plugin 反馈及通知 (#1186) 2022-08-27 22:29:05 +08:00
github-actions[bot]
94d336ef4d 📝 Update changelog 2022-08-27 14:16:48 +00:00
sena-nana
07707213a5 🍻 publish plugin MagiaDice骰娘及TRPGLOG (#1184) 2022-08-27 22:15:41 +08:00
github-actions[bot]
7579878fb4 📝 Update changelog 2022-08-27 13:51:18 +00:00
Nranphy
6599b6420e 🍻 publish plugin 面麻小助手 (#1190) 2022-08-27 21:50:04 +08:00
github-actions[bot]
135c6e8168 📝 Update changelog 2022-08-26 02:16:15 +00:00
X-Skirt-X
743e7363ea 🍻 publish plugin 话痨排行榜 (#1181) 2022-08-26 10:15:07 +08:00
github-actions[bot]
a101428c81 📝 Update changelog 2022-08-25 02:27:25 +00:00
YunJin
0d2b1f693e ✏️ Plugin: 修改插件多功能简易群管信息 (#1180) 2022-08-25 10:26:06 +08:00
github-actions[bot]
e5a53dfd5c 📝 Update changelog 2022-08-25 02:00:45 +00:00
KarisAya
915c2b3e43 🍻 publish plugin 保存群聊闪照 (#1178) 2022-08-25 09:59:40 +08:00
github-actions[bot]
767b6a9913 📝 Update changelog 2022-08-24 01:55:17 +00:00
Ju4tCode
3f8af04803 add rich text support for shell command (#1171) 2022-08-24 09:54:08 +08:00
github-actions[bot]
00af815b8a 📝 Update changelog 2022-08-23 07:01:42 +00:00
InariInDream
24df594b97 🍻 publish plugin 课表查询 (#1167) 2022-08-23 15:00:34 +08:00
github-actions[bot]
d6567f9288 📝 Update changelog 2022-08-23 06:35:43 +00:00
yzyyz1387
4eb158245e 🍻 publish plugin 业余无线电助手 (#1172) 2022-08-23 14:34:27 +08:00
github-actions[bot]
ef35266d3e 📝 Update changelog 2022-08-23 04:04:55 +00:00
he0119
1d1beb100a 🍻 publish plugin NoneBot 树形帮助插件 (#1176) 2022-08-23 12:03:41 +08:00
github-actions[bot]
06ab6093b7 📝 Update changelog 2022-08-23 03:54:04 +00:00
yzyyz1387
c1ce7fb940 🍻 publish plugin 工作性价比 (#1174) 2022-08-23 11:53:01 +08:00
github-actions[bot]
6e03ddbf12 📝 Update changelog 2022-08-23 03:48:13 +00:00
KarisAya
40c8787828 🍻 publish plugin 娶群友 (#1169) 2022-08-23 11:47:03 +08:00
github-actions[bot]
be459e0bbb 📝 Update changelog 2022-08-22 10:12:31 +00:00
ssttkkl
1056828f90 🍻 publish plugin PixivBot (#1164) 2022-08-22 18:11:28 +08:00
github-actions[bot]
ef18e8943d 📝 Update changelog 2022-08-22 06:40:04 +00:00
Mix
92ff1df419 🐛 修复当消息与不支持的类型相加时抛出的异常类型错误 (#1166) 2022-08-22 14:39:00 +08:00
github-actions[bot]
be5ac88a18 📝 Update changelog 2022-08-21 03:25:26 +00:00
Yiyuiii
fac647370a 🍻 publish plugin 日韩中 VITS 模型原神拟声 (#1161) 2022-08-21 11:24:11 +08:00
github-actions[bot]
05a3891903 📝 Update changelog 2022-08-20 02:19:01 +00:00
Ju4tCode
4deae8f00c 🔥 remove deprecated State param (#1160) 2022-08-20 10:17:52 +08:00
github-actions[bot]
0f70e975b0 📝 Update changelog 2022-08-19 01:10:13 +00:00
SkyDynamic
982680be91 🍻 publish plugin 每日人品 (#1155) 2022-08-19 09:09:10 +08:00
github-actions[bot]
96b0a863e6 📝 Update changelog 2022-08-18 01:43:06 +00:00
CrazyBoyM
d64bb37c6d 🍻 publish plugin nonebot-plugin-drawer (#1145) 2022-08-18 09:41:48 +08:00
github-actions[bot]
51d7f1783d 📝 Update changelog 2022-08-16 02:08:08 +00:00
YunJin
64c18379c9 ✏️ change plugin admin hello info (#1159) 2022-08-16 10:06:52 +08:00
GC_XiaoZhang
2735f0cba9 ✏️ change plugin fireN info (#1158) 2022-08-16 10:06:02 +08:00
github-actions[bot]
660dbaf3b8 📝 Update changelog 2022-08-16 02:04:40 +00:00
Ju4tCode
898c29d7ee 💥 remove deprecated nonebot.plugins toml table (#1151)
Feature: 移除过时的 `nonebot.plugins` toml 配置
2022-08-16 10:03:37 +08:00
github-actions[bot]
cdc507bab9 📝 Update changelog 2022-08-15 13:34:28 +00:00
YunJin
f32bcdc1fc ✏️ update plugin 多功能简易群管 (#1154) 2022-08-15 21:33:13 +08:00
github-actions[bot]
013602da21 📝 Update changelog 2022-08-14 11:42:11 +00:00
Ju4tCode
4974c596ec 💥 remove Python 3.7 support (#1148) 2022-08-14 19:41:00 +08:00
github-actions[bot]
0620bec51f 📝 Update changelog 2022-08-14 09:11:29 +00:00
KarisAya
8870e6a26e 🍻 publish plugin 小游戏合集 (#1149) 2022-08-14 17:10:21 +08:00
github-actions[bot]
0e3ed0e7ab 📝 Update changelog 2022-08-12 07:02:00 +00:00
HuYihe2008
6cc3b68447 🍻 publish plugin 简易群管(带入群欢迎) (#1141) 2022-08-12 15:00:52 +08:00
github-actions[bot]
549a37b172 📝 Update changelog 2022-08-11 05:52:38 +00:00
ZombieFly
16394ad68b 🍻 publish plugin wiki条目搜索、获取简介 (#1132) 2022-08-11 13:51:20 +08:00
github-actions[bot]
57e580c255 📝 Update changelog 2022-08-11 05:51:05 +00:00
Ankhyty
6c23d89494 🍻 publish plugin bangumi搜索 (#1136) 2022-08-11 13:50:01 +08:00
github-actions[bot]
675e70f579 📝 Update changelog 2022-08-09 11:01:23 +00:00
bingqiu456
7a098b96f8 🍻 publish plugin 疫情小助手-频道版 (#1130) 2022-08-09 19:00:15 +08:00
github-actions[bot]
c9794bf91d 📝 Update changelog 2022-08-08 13:08:47 +00:00
Ju4tCode
1766d4da69 💥 remove deprecated export (#1125) 2022-08-08 21:07:36 +08:00
github-actions[bot]
6583bc8c61 📝 Update changelog 2022-08-08 04:19:01 +00:00
17TheWord
179f16346a 🍻 publish plugin MC_QQ通信 (#1126) 2022-08-08 12:17:47 +08:00
github-actions[bot]
badb0c9ff4 📝 Update changelog 2022-08-08 02:26:00 +00:00
lgc2333
ee0ea85e40 🍻 publish plugin BAWiki (#1128) 2022-08-08 10:24:48 +08:00
github-actions[bot]
e5e69c2726 🔖 Release 2.0.0-beta.5 2022-08-04 06:27:40 +00:00
github-actions[bot]
7c7ea613e9 📝 Update changelog 2022-08-04 06:18:34 +00:00
Ju4tCode
bb1b94e5e3 🔖 bump version 2.0.0-beta.5 (#1122) 2022-08-04 14:14:50 +08:00
github-actions[bot]
8420add975 📝 Update changelog 2022-08-04 05:40:36 +00:00
Ju4tCode
2192e8cb6d 🐛 fix parent detect error after require (#1121) 2022-08-04 13:39:20 +08:00
github-actions[bot]
48ccef2f06 📝 Update changelog 2022-08-02 02:31:11 +00:00
AkiraXie
c6bc24efc2 🐛 run_postprecessors handle matcher.state now (#1119) 2022-08-02 10:29:48 +08:00
github-actions[bot]
63f8d78d20 📝 Update changelog 2022-08-01 06:19:28 +00:00
ArgonarioD
db36c262db 🍻 publish plugin 「能不能好好说话?」缩写翻译 (#1117) 2022-08-01 14:18:15 +08:00
github-actions[bot]
732a13b692 📝 Update changelog 2022-08-01 02:53:25 +00:00
Ju4tCode
71bf1d1147 🐛 fix import error if setuptools not installed (#1116)
Fix: 修复 setuptools 未安装导致 ImportError
2022-08-01 10:52:15 +08:00
github-actions[bot]
6e98ac031c 📝 Update changelog 2022-07-31 02:33:58 +00:00
syrinka
9a49354ddd 🍻 publish plugin 推送钩子 (#1114) 2022-07-31 10:32:36 +08:00
github-actions[bot]
455752bd92 📝 Update changelog 2022-07-28 04:42:49 +00:00
yuyuziYYZ
2a51b07229 🍻 publish bot SkadiBot (#1112) 2022-07-28 12:41:42 +08:00
github-actions[bot]
732b5b0b1b 📝 Update changelog 2022-07-25 02:01:16 +00:00
ziru-w
a0dcc7753c 🍻 publish plugin 易命令 (#1110) 2022-07-25 09:59:58 +08:00
github-actions[bot]
12942f2d50 📝 Update changelog 2022-07-23 02:25:34 +00:00
bingqiu456
192d094f54 🍻 publish plugin 群昵称时间 (#1108) 2022-07-23 10:24:17 +08:00
github-actions[bot]
bb02d50837 📝 Update changelog 2022-07-22 06:23:48 +00:00
那个小白白白
bc8c65d0d8 Bot: 修改剑网三 bot 信息 (#1107) 2022-07-22 14:22:36 +08:00
github-actions[bot]
9447b1f462 📝 Update changelog 2022-07-20 02:23:17 +00:00
Dobiichi-Origami
c03b0c73cb Feature: on_x 支持 expire_time 参数 (#1106)
Co-authored-by: Dobiichi-Origami <454470535@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com>
2022-07-20 10:21:31 +08:00
github-actions[bot]
19f4c01ad3 📝 Update changelog 2022-07-15 02:12:35 +00:00
synodriver
9bd07b9ced add block driver startup/shutdown sync support (#1104)
Feature: 正向驱动器 startup/shutdown hook 支持同步函数
2022-07-15 10:11:19 +08:00
github-actions[bot]
fe5cf5624c 📝 Update changelog 2022-07-14 03:31:37 +00:00
Shine-Light
a14c38300e 🍻 publish bot 真宵Bot (#1102) 2022-07-14 11:30:34 +08:00
github-actions[bot]
9e908d5b3f 📝 Update changelog 2022-07-12 07:22:59 +00:00
ziru-w
f1ab95489c 🍻 publish plugin 处理好友添加和群邀请 (#1098) 2022-07-12 15:21:30 +08:00
github-actions[bot]
3c42e26e27 📝 Update changelog 2022-07-12 06:59:52 +00:00
zheuziihau
c248b8c354 🍻 publish plugin 明日方舟寻访记录分析 (#1096) 2022-07-12 14:58:45 +08:00
github-actions[bot]
0ecea50778 📝 Update changelog 2022-07-11 08:05:27 +00:00
ziru-w
33d4d01d51 🍻 publish plugin b站视频每日推送 (#1094) 2022-07-11 16:04:06 +08:00
github-actions[bot]
1667440c64 📝 Update changelog 2022-07-10 06:38:53 +00:00
KarisAya
141527238c 🍻 publish plugin 自动回复(文i)插件 (#1089) 2022-07-10 14:37:54 +08:00
github-actions[bot]
e2d0453741 📝 Update changelog 2022-07-10 02:48:38 +00:00
10-24
0849df1c76 🍻 publish plugin ACC计算工具 (#1092) 2022-07-10 10:47:35 +08:00
github-actions[bot]
c4d45c087a 📝 Update changelog 2022-07-08 04:26:23 +00:00
Ju4tCode
be15cfabcc 📝 Docs: 添加 nonemoji 并更新开发指南 (#1088)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-08 12:24:48 +08:00
github-actions[bot]
dddbeb389f 📝 Update changelog 2022-07-06 03:02:29 +00:00
yaowan233
bdfaf4840f 🍻 publish plugin OSU查分插件 (#1081) 2022-07-06 11:00:35 +08:00
GC-ZF
b37b1380a3 🍻 publish plugin 战地1、5战绩查询工具 (#1086) 2022-07-06 10:59:34 +08:00
github-actions[bot]
d8ed5c2e80 📝 Update changelog 2022-07-06 02:38:40 +00:00
GC-ZF
4bc391c066 🍻 publish plugin 一起燚xN吧 (#1084) 2022-07-06 10:37:18 +08:00
pre-commit-ci[bot]
5aa6138bf3 ⬆️ auto update by pre-commit hooks (#1080)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-05 10:17:39 +08:00
github-actions[bot]
fc78b9c547 📝 Update changelog 2022-07-05 02:17:00 +00:00
Ju4tCode
118874080d ✏️ fix docs event message type error (#1079) 2022-07-05 10:15:55 +08:00
github-actions[bot]
cf2137a1a9 📝 Update changelog 2022-07-02 02:21:11 +00:00
StarHeart
14b145b58d 📝 Docs: 修复旧 Vuepress 文档缓存问题 (#1077)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-02 10:19:51 +08:00
github-actions[bot]
1aba737cbd 📝 Update changelog 2022-07-01 02:58:18 +00:00
Ju4tCode
fe38b1f17f 📝 Docs: 更新 Readme 贡献图片 (#1074) 2022-07-01 10:57:05 +08:00
github-actions[bot]
776651284f 📝 Update changelog 2022-07-01 02:39:14 +00:00
CMHopeSunshine
068cc3a7ea 🍻 publish plugin 米游币商品自动兑换 (#1075) 2022-07-01 10:38:16 +08:00
github-actions[bot]
0e7e88cfa2 📝 Update changelog 2022-06-30 06:31:31 +00:00
StarHeart
c120f9be70 📝 unregister sw loaded from vuepress (#1073) 2022-06-30 14:30:15 +08:00
github-actions[bot]
563436b38e 📝 Update changelog 2022-06-30 06:29:47 +00:00
shinianj
386be6cbb6 🍻 publish plugin 赛马 (#1068) 2022-06-30 14:28:42 +08:00
github-actions[bot]
b5e29533d8 📝 Update changelog 2022-06-30 04:20:11 +00:00
MingxuanGame
beb19adad5 📝 fix docs call Permission wrong (#1072) 2022-06-30 12:19:10 +08:00
github-actions[bot]
e2289c78b0 📝 Update changelog 2022-06-29 02:56:19 +00:00
hamo-reid
d3f261eb34 🍻 publish plugin PicMenu (#1070) 2022-06-29 10:55:16 +08:00
github-actions[bot]
d54c2e6bf4 📝 Update changelog 2022-06-28 02:26:03 +00:00
Mai-icy
6fdebc4912 🍻 publish plugin 面包店小游戏 (#1063) 2022-06-28 10:25:02 +08:00
github-actions[bot]
f1ffac5ca7 📝 Update changelog 2022-06-25 08:20:06 +00:00
A-kirami
12716ee79a 🍻 publish plugin 黑白名单 (#1060) 2022-06-25 16:19:08 +08:00
github-actions[bot]
42413281bb 📝 Update changelog 2022-06-25 04:34:24 +00:00
github-actions[bot]
a67eda4c80 📝 Update changelog 2022-06-24 02:52:05 +00:00
Akirami
9dbea871b8 ✏️ fix type T_RunPostProcessor incorrect description (#1057)
Bug: 修复 typing 中 T_RunPostProcessor 类型的注释描述不正确
2022-06-24 10:51:06 +08:00
github-actions[bot]
4181f4ca77 📝 Update changelog 2022-06-24 02:49:36 +00:00
Special-Week
fe4a33d19b 🍻 publish plugin BitTorrent (#1058) 2022-06-24 10:48:29 +08:00
github-actions[bot]
58d8815f39 🔖 Release 2.0.0-beta.4 2022-06-20 11:40:59 +00:00
Ju4tCode
b80083fed5 🔖 bump version 2.0.0-beta.4 (#1056) 2022-06-20 19:29:56 +08:00
github-actions[bot]
4ba17d900a 📝 Update changelog 2022-06-20 07:53:30 +00:00
Ju4tCode
f11970132c Fix: 修复 MessageSegment 在有额外数据时报错 (#1055) 2022-06-20 15:52:12 +08:00
github-actions[bot]
c91c9380a7 📝 Update changelog 2022-06-20 07:51:00 +00:00
Ju4tCode
06ee47edcd Feature: 添加插件元信息定义 (#1046) 2022-06-20 15:49:53 +08:00
github-actions[bot]
a82ce00a4b 📝 Update changelog 2022-06-19 01:25:55 +00:00
AquamarineCyan
e0902eeb58 🍻 publish plugin 历史上的今天 (#1048) 2022-06-19 09:24:36 +08:00
github-actions[bot]
b754ac2fbc 📝 Update changelog 2022-06-18 07:59:15 +00:00
Special-Week
1ae0a654bf 🍻 publish plugin 智能回复 (#1053) 2022-06-18 15:58:10 +08:00
github-actions[bot]
b85348f648 📝 Update changelog 2022-06-18 06:49:01 +00:00
Ju4tCode
7b06469a30 🐛 fix env var not override dotenv file (#1052) 2022-06-18 14:47:42 +08:00
github-actions[bot]
a62a49d477 📝 Update changelog 2022-06-18 03:13:55 +00:00
Special-Week
e2f96a0b5c 🍻 publish plugin nonebot_plugin_setu4 (#1050) 2022-06-18 11:12:40 +08:00
github-actions[bot]
28c22b7511 📝 Update changelog 2022-06-16 03:43:18 +00:00
nikissXI
c027e4d2ce 🍻 publish bot nya_bot (#1044) 2022-06-16 11:42:09 +08:00
github-actions[bot]
e38bb2b530 📝 Update changelog 2022-06-06 02:26:03 +00:00
18870
19a9a3c3c5 🍻 publish plugin 命令重启机器人 (#1037) 2022-06-06 10:24:55 +08:00
github-actions[bot]
4c8f5059db 📝 Update changelog 2022-06-04 11:34:00 +00:00
ZMXC01
9ab1acf1e7 🍻 publish plugin 青年大学习自动提交 (#1035) 2022-06-04 19:32:59 +08:00
github-actions[bot]
175acd38eb 📝 Update changelog 2022-06-03 13:00:11 +00:00
BlueGlassBlock
4241eb538c 🎨 Feature: 日志记录自动检测终端是否支持彩色 (#1034)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-06-03 20:59:04 +08:00
github-actions[bot]
c55b32b9f9 📝 Update changelog 2022-06-03 02:42:41 +00:00
bingqiu456
2082e5f5c2 🍻 publish plugin 疫情小助手 (#1032) 2022-06-03 10:41:18 +08:00
SEAFHMC
8485a356e7 🍻 publish plugin 谁艾特我了 (#1030) 2022-06-02 14:48:41 +08:00
dependabot[bot]
f6f70fb435 ⬆️ Bump httpx from 0.22.0 to 0.23.0 (#1029)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-02 11:21:28 +08:00
github-actions[bot]
c3ce4f76d1 📝 Update changelog 2022-06-01 16:37:04 +00:00
benx1n
db90a9ebab 🍻 publish plugin Hikari-战舰世界水表查询 (#1024) 2022-06-02 00:35:51 +08:00
github-actions[bot]
3962a98743 📝 Update changelog 2022-06-01 10:36:51 +00:00
Ju4tCode
3ce23bc593 🍻 publish adapter OneBot V12 (#1026)
Co-authored-by: yanyongyu <yanyongyu@users.noreply.github.com>
2022-06-01 18:35:40 +08:00
github-actions[bot]
3238a82042 📝 Update changelog 2022-05-31 04:36:51 +00:00
axStar
ed1f920088 🍻 publish plugin Warframe时间查询 (#1022) 2022-05-31 12:35:52 +08:00
github-actions[bot]
c34b3439fa 📝 Update changelog 2022-05-30 12:41:17 +00:00
Ju4tCode
bd2225c43c 🍻 publish plugin imagetools (#1020) (#1021)
Co-authored-by: MeetWq <MeetWq@users.noreply.github.com>
2022-05-30 20:40:10 +08:00
github-actions[bot]
381234725e 📝 Update changelog 2022-05-29 02:57:28 +00:00
NumberSir
a5ee6ac401 🍻 publish plugin 明日方舟工具箱 (#1018) 2022-05-29 10:56:14 +08:00
github-actions[bot]
79fd12768d 📝 Update changelog 2022-05-27 09:29:27 +00:00
ASTWY
c0ab86f91b 🍻 publish plugin B站视频伪分享卡片 (#1013) 2022-05-27 17:28:20 +08:00
Ju4tCode
ae21bfdd0e 👷 fix python ci cache path error (#1012)
* 👷 fix python ci cache path error

* 🐛 fix path error

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

* 📝  Minimize QQ Channel icon SVG
2022-05-21 19:41:50 +08:00
github-actions[bot]
20f414d0de 📝 Update changelog 2022-05-21 03:18:43 +00:00
AkiraXie
5924f1e7ac 📝 Docs: 调整跨插件访问文档 (#993)
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-05-21 11:17:27 +08:00
github-actions[bot]
2fbd44eef9 📝 Update changelog 2022-05-21 03:15:19 +00:00
kexue-z
0adf4d6934 🍻 publish plugin 数据库连接插件 (#994) 2022-05-21 11:14:19 +08:00
github-actions[bot]
2c91a4c7f1 📝 Update changelog 2022-05-21 02:54:46 +00:00
NumberSir
625c72ab0d 🍻 publish plugin 百度翻译 (#991) 2022-05-21 10:53:47 +08:00
github-actions[bot]
449a2c5f96 📝 Update changelog 2022-05-21 01:15:31 +00:00
AkashiCoin
9e64f3f8ab 🍻 publish plugin MockingBird语音 2022-05-21 09:14:27 +08:00
351 changed files with 30024 additions and 18204 deletions

View File

@@ -1,20 +0,0 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/codespaces-linux/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/universal:linux
# ** [Optional] Uncomment this section to install additional packages. **
# USER root
#
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
USER codespace
# [Required] Poetry
RUN curl -sSL https://install.python-poetry.org | python - -y
RUN poetry config virtualenvs.in-project true
# [Required] Gitmoji CLI
# Deprecated: Maybe removed once nonemoji is done
RUN yarn global add gitmoji-cli
ENV PATH="$PATH:/home/codespace/.yarn/bin"

View File

@@ -1,28 +1,16 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/codespaces-linux
{ {
"name": "NoneBot", "name": "Default Linux Universal",
"build": { "image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"dockerfile": "Dockerfile", "features": {
"context": ".." "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": {
"vscode": {
"settings": { "settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go",
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"python.analysis.diagnosticMode": "workspace", "python.analysis.diagnosticMode": "workspace",
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic",
"[python]":{ "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": true
@@ -43,7 +31,6 @@
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"lldb.executable": "/usr/bin/lldb",
"files.exclude": { "files.exclude": {
"**/__pycache__": true "**/__pycache__": true
}, },
@@ -52,20 +39,7 @@
"**/__pycache__": true "**/__pycache__": true
} }
}, },
"remoteUser": "codespace",
"overrideCommand": false,
"mounts": ["source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"],
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined",
"--privileged",
"--init"
],
// Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"GitHub.vscode-pull-request-github",
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort", "ms-python.isort",
@@ -73,11 +47,7 @@
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss"
], ]
}
// Use 'forwardPorts' to make a list of ports inside the container available locally. }
// "forwardPorts": [],
// "oryx build" will automatically install your dependencies and attempt to build your project
"postCreateCommand": "poetry install -E all && poetry run pre-commit install && yarn install"
} }

View File

@@ -5,7 +5,10 @@ runs:
using: "composite" using: "composite"
steps: steps:
- run: | - 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/ cp -r ./build/nonebot/* ./website/docs/api/
yarn prettier yarn prettier
shell: bash shell: bash

View File

@@ -9,10 +9,10 @@ runs:
node-version: "16" node-version: "16"
- id: yarn-cache-dir-path - id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
shell: bash shell: bash
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

View File

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

View File

@@ -5,6 +5,10 @@ on:
branches: branches:
- master - master
pull_request: pull_request:
paths:
- "nonebot/**"
- "packages/**"
- "tests/**"
jobs: jobs:
test: test:
@@ -15,7 +19,7 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
strategy: strategy:
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10", "3.11"]
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false fail-fast: false
env: env:

View File

@@ -5,19 +5,47 @@ on:
types: [opened, reopened, edited] types: [opened, reopened, edited]
pull_request_target: pull_request_target:
types: [closed] types: [closed]
issue_comment:
types: [created]
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
cancel-in-progress: true
jobs: jobs:
plugin_test:
runs-on: ubuntu-latest
name: nonebot2 plugin test
if: github.event_name != 'issue_comment' || !github.event.issue.pull_request
permissions:
issues: read
outputs:
result: ${{ steps.plugin-test.outputs.RESULT }}
output: ${{ steps.plugin-test.outputs.OUTPUT }}
steps:
- 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://github.com/nonebot/nonebot2-publish-bot/releases/latest/download/plugin_test.py -o plugin_test.py
python plugin_test.py
publish_bot: publish_bot:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: nonebot2 publish bot name: nonebot2 publish bot
needs: plugin_test
steps: steps:
- name: Checkout code - name: Checkout Code
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
- name: NoneBot2 Publish Bot - name: NoneBot2 Publish Bot
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:latest
with: with:
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
config: > config: >
@@ -27,3 +55,6 @@ jobs:
"bot_path": "website/static/bots.json", "bot_path": "website/static/bots.json",
"adapter_path": "website/static/adapters.json" "adapter_path": "website/static/adapters.json"
} }
env:
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}

View File

@@ -35,7 +35,7 @@ jobs:
with: with:
changelog_file: website/src/pages/changelog.md changelog_file: website/src/pages/changelog.md
latest_changes_position: '# 更新日志\n\n' latest_changes_position: '# 更新日志\n\n'
latest_changes_title: '## 最近更新' latest_changes_title: "## 最近更新"
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )' replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
changelog_body: ${{ steps.release-drafter.outputs.body }} changelog_body: ${{ steps.release-drafter.outputs.body }}
commit_and_push: false commit_and_push: false
@@ -49,7 +49,6 @@ jobs:
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog" git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
git push git push
release: release:
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -31,7 +31,7 @@ jobs:
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
- name: Deploy to Netlify - name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1 uses: nwtgck/actions-netlify@v2
with: with:
publish-dir: "./website/build" publish-dir: "./website/build"
production-deploy: true production-deploy: true

View File

@@ -32,7 +32,7 @@ jobs:
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
- name: Deploy to Netlify - name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1 uses: nwtgck/actions-netlify@v2
with: with:
publish-dir: "./website/build" publish-dir: "./website/build"
production-deploy: false production-deploy: false

4
.markdownlint.yaml Normal file
View File

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

View File

@@ -1,3 +1,4 @@
default_install_hook_types: [pre-commit, prepare-commit-msg]
ci: ci:
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks" autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
autofix_prs: true autofix_prs: true
@@ -5,18 +6,33 @@ ci:
autoupdate_schedule: monthly autoupdate_schedule: monthly
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos: repos:
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.3
hooks:
- id: pycln
args: [--config, pyproject.toml]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 5.10.1 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
stages: [commit]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.3.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.6.2 rev: v3.0.0-alpha.6
hooks: hooks:
- id: prettier - id: prettier
types_or: [javascript, jsx, ts, tsx, markdown] types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
stages: [commit]
- repo: https://github.com/nonebot/nonemoji
rev: v0.1.3
hooks:
- id: nonemoji
stages: [prepare-commit-msg]

View File

@@ -29,14 +29,11 @@ NoneBot 使用 [poetry](https://python-poetry.org/) 管理项目依赖,由于
poetry install poetry install
# 安装 pre-commit git hook # 安装 pre-commit git hook
pre-commit install pre-commit install
# 安装 gitmoji git hook
yarn global add gitmoji-cli
gitmoji -i
``` ```
### 使用 GitHub Codespaces (Dev Container) ### 使用 GitHub CodespacesDev Container
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)
### Commit 规范 ### Commit 规范
@@ -44,12 +41,22 @@ gitmoji -i
NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。 NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。
或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commitnonemoji 已默认作为项目开发依赖安装。
```bash
nonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...]
```
### 工作流概述
`master` 分支为 NoneBot 的开发分支,在任何情况下都请不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文,以便自动生成更新日志。
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。
### 撰写文档 ### 撰写文档
NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。 NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server如下所示 如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server如下所示
```bash ```bash
@@ -57,22 +64,30 @@ yarn install
yarn start yarn start
``` ```
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
以下是比较重要的编写与排版规范。目前 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)补充修改得到。
如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。
### 参与开发 ### 参与开发
NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。 NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
## 为社区做贡献 ## 为社区做贡献
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。 你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/developer/plugin-publishing) 一节。
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。 我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。 虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。
同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。 同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。
## Git 工作流
`master` 分支为 NoneBot 的开发分支,请在任何情况下都不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。

View File

@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<a href="https://pypi.python.org/pypi/nonebot2"> <a href="https://pypi.python.org/pypi/nonebot2">
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi"> <img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
</a> </a>
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python"> <img src="https://img.shields.io/badge/python-3.8+-blue" alt="python">
<a href="https://codecov.io/gh/nonebot/nonebot2"> <a href="https://codecov.io/gh/nonebot/nonebot2">
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/> <img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
</a> </a>
@@ -29,23 +29,29 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/> <img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/>
</a> </a>
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master"> <a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" /> <img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
</a> </a>
<br /> <br />
<a href="https://onebot.dev/"> <a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp"> <img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a> </a>
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p"> <a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="ding"> <img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a> </a>
<a href="https://core.telegram.org/bots/api"> <a href="https://core.telegram.org/bots/api">
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram"> <img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
</a> </a>
<a href="https://open.feishu.cn/document/home/index"> <a href="https://open.feishu.cn/document/home/index">
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAk1BMVEX///8zMzNJSUlSUlJcXFxtbW0zMzNLS0szMzMzMzNBQUGVlZUzMzM1NTU0NDQzMzMzMzM0NDQ0NDQ0NDQ3NzdDQ0M0NDQ2NjY4ODg9PT0zMzM0NDQ5OTk7OzszMzM0NDQ3NzczMzM0NDQ0NDQ0NDQ0NDQ3Nzc2NjY4ODg2NjY7Ozs0NDQ6Ojo6Ojo3Nzc4ODgzMzNGdMWJAAAAMHRSTlMD6h0TDgr8GNf0KQbvhLT45KKTmm4jwHVJLdLFQzbcjFTgzsq7rl58T2kyqD46Y1riMDRhAAAFr0lEQVR42uzZWXKiUACF4YMyqKAQhyjOc7STmLP/1bVlLukESIJ3sLGKbwFU/Q8HuIBKpVKpVCqVSqVSqVQqlUqlUvmNM10Mcfda/U6TPdw3e9lb8ayLO+bPniYu+amjNcPd8U7PFhML0RE5uCvnaY/5SVt0WFvckcu0vxjiYmDxbu5cl2mn9UVHRMa4B2LaP3RYKD1vL6adccRFLSLL/izxxbRz7UXHimdLlFdq2mlvnztYRznZh96cP3G/dkxQRrOnR5c/c5eiQ+S2UTbe/sHir9zD1w6+okz8aXvMItyRqE46ApSHmHYRYdLRoPCMcrAP3TkLC6fpDp5QAn/EtItqij3UG/zgQZH5aWc7ZqJjzA9jKFGf9ppXC3I6uMB/Mzh2mpQQ/Mnp4BSy1Kctx4pFx5qfhA4kqE87pCyrldfBDm6sLqat2mGnttXHDfkvYtryooHo2PCrFm5lcNw1qWr1XUeEm7BH3QYVRJNGcOmoietNmNKDWeKFnCo6b3Wc1drW/NsOLpFRqmmT4xgfPFw42Q7XhkFi2kq2DtKcR2Y8wpTacRdQ3aZYB59ggiOmrS6sFevgDNr9GW6pzRAZdsQsC3rV3x4i6uQha8+sB2h9am9c6rVBDj9ixr5k007rIs+CGV65pl3wXjRi2hrKYjFtM/rI02JaW3XaPYtGtZHHY9qL0lN7QuO2yBMzpenLTvtkZNos+AY1ZcpObtoLtWmrj6TNlCOuJqZ9M3PkaDBlIBHCmwpHyHpjSgMS2ryhcIqsmsWULmR0eTPhK7LsMdMOKHdJM+nw8E+8ZoYDOT3eRDDDuz6HNt7VeszaQtYDJch+38WRZ51TDO+0Y54hylzy0XHib2JI83c0zIoLd1hAeUus1jenQe2HQ79Dg6wB3i1d/uoNpS2JrulgHWqcRxqySjoObrFjfUlLVrVrOtiGMmdCA+ZJx8hlEa9QZ2+oXcNLOkIWEUAHe22sYxqykGdoUV//5w6eoKlkTI3Gdbx7CVmQB10lDWqzSTpemyyoAW28ubYO++oOLqBPbUUtJknHrMnCRihdyaOTdAQsLHSgtSTS2BEHLK4DvQYWFW2lOtiHZi3Fko6fXCjgNVooV0nHl7tMBP1aAaXtJDvYgwGxdMmzLzu1JUyYNSU7IAwiiZ8OJrxKlTzI38QnMORFoqSn8Fh9gikvIa/UVejgDMZMQ9mOOa8WwKCRyysslF6hn2HSwZX4+ew1KGEPCSZKhoqHMw9mLd1rO8aUMYZpexbRV/2AsYBxy7/t3NtuglAQheFR6wEPVEQtaq1WxQNqnfd/urY08QJFYHZS15D9vcHckMzOz/QWA9/3jqHrbmbr1bT10a90ncQcoiclgKY/Vq81q6P2JJqfI+NHPqdDSMRzsEtIXmYGcQcQk2fwKgHxTCIVJGMWwTu6sWGxPSFx+QpkOfz3QcYEJWQhtGsbR5aKCIrHInjXNsSDeITFZ6ELYZEMAnltY8AyawKz4KJAr21IBzkRmB6LOIRGOEhIaHYsciA0uxIshwa/DLQIzrAEy2HswBIBwck9yNOvbWT4YgHEU4zbEiyHsQsXhnmKccmxp2cbxvb8CyDbMBXwD4hsw1BQguUw9s4Mk20YOTFQtmHiDJVtGJhjZRtyEVi2ITbhnLBOMd5qOvqXwz9RFy3bkJpU0LINeTCsJdvIztHVZhsJo77SbOPG6FNltpFQqMxsE7hmS+9ymJxE7XKYUGupyzZS1Kbaso00tbWybONBTadyObyjPlaVbTycRFO28Uh9oyjbEJ/E2JImnVDXy1y6zpHvW5E2npJsI5unI9vIwVe3HKYZaMg2clkoyDby6Wl5mcv0Bp9t5DVEzzZyG4JnG/kdsLONArbQ2UYRlwZwtlHIsoGbbRSdRNtymGbf0LYcpgleQbMNwdUCbcthmrP2j++VjqdSy7Isy7Isy4LxDTcBnqEPd5jdAAAAAElFTkSuQmCC" alt="feishu"> <img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
</a>
<a href="https://docs.github.com/en/developers/apps">
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
</a> </a>
<a href="https://bot.q.qq.com/wiki/"> <a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC+lBMVEUAAAApRHRAbvYyVI4dMlsNGjcvTH5anN0/a5xdo+Vdpvpms/dMfvdcoPZjr/hJfu5krvA9Z+8/a+M9adxQjNdYmdVAbdU4XtRRjcxFd8wyVbM+aZowUJwpRJlZneRIevU5YORWl+1fqOZVle9ZnOpEdOk/aelepus/a+NQiuVKgeQ6YeRQi905X91EdtxAbthZnNlRjdk5YdpQjuhLgehhquc7Y+c6Y8k3XctWlcs0V8tJfMMzVsM9aMJVksdHebVKgLQyVJknP5REdKD///9irv9Ccf5EdP9GfP9jsf7+/f5SkP9mtf9GeP/+/vxJfv5Ukv9Ng/9boP9Gef1ls/9Ulf9Wl//+//5KgP9Nhv9dpP9Oiv9Tk/5Abv9Lg/9Fdv9NiP7///5Qj/7+/fxfpv9Nh/xhq/9QjP9Wm/79//1co/9ir/5fqf5bov9Qi//8/f1XmP1Wl/1ksv/7/v38/vv9/vlgqv9Znf9IfPzZ5fdeqP9nt/5anv5Xmf9Uk/1anvxpuf73/P34+/tot/9hrf1anvpMhvpSjv1pt/9ZnP1aofxTkfxNivza5/dOhPf+//1Ri/z+//vz+fvw9/tIf/v6/fpdofrI2PlYmfnF3Pe0y/Pm7/jW4/e0zvRapPxXlfpTkvpGevri7ffS3vVMiP5XnvxXnPtQivn3/Ph0r/dgpPZ1qfXi6/S9z/NYmf/z+v5Yn/7L2vlfmflUk/hXi/dRgvSw0fGnu+1dqP3t9Pthq/vA1vjc6vfU4vd4sfdepPdsn/dam/fU5fawzfRjjfRhi/NJhP0/a/y80fhTjfhrrfeXvvZuk/WyyPR9rvSqz/OcwfOmv/OSuPO0yfKguPCWr/BPi+9MheRKgv1gsPzq9PpgpvpVjvrN2/jG1vdonvbA0PSrx/OtxPCkxPDp8PvN4ftnuPuCufloqPiex/a3z/WPuvWQqvR2mfRplvReivSOqvOKufKGufKCrPKBoPJzlfFjr+88Ze6ftux8nOxxq/SGr/CFr/Cvx+3R19+QAAAARHRSTlMAEP4ZEwUEvC3S/v7+/Pv08PDXvJqampqSgEtJLS3f/Pz4+O/n5+bl4tTU1Lq6ure2trGdnZ2dlI6Afl5eXldMS0lJLRAR2gcAAAewSURBVFjDlZcFfBJRHMdvdnd3d3cn1qk3h4Et7EQ965h3KKgTQUGdOgYMHII6a5vO2d3d3d3d3X4+/t974A51xvfDu7vfP348HsdxR6VAulaN6lUu8SHVgQOpPpSoXK9Rq3TUf5CmSdWcB/oFcCBn1SZp/rG9aP6c/X5LzvxF/6W9Vqq2KZKq1t8sgvKT9pQt8gf9qb9QOVLWjux+K8oVSrE9bYNU7f6BVA3SpjD93O3+kdy//Ripcw34QefOnaViwM8iV+rf9Ffo7Kdr166du87vSgQGZGcIJocq/OIQlAv1EbAHDL/0hXzOhFxBP61f7vbJdJ0PFfN1RBB0MIPAUO7Alazvz+l0WY4+Pbthw4az66QG63Do6dEsuh+W9QO+//moVRet071L8phomUzBGPcGBwe3hxdmn5FVyGS0yZP0TodpHzy/kGQBygYjonUvbtpFraDwGUjYZ2JRVNSKK26+0EXjWNnkZciH26OjPy3gOZ5nVVCq+p0BzfK8wC/4FB2MLfL5+4sU7wkER7/dhN6aYxia0zKmvT0l7DUxHE1zHMcoVLJNb6ODUbB4EYpQHddcPHpXK5OpWNHp8CxbtmzmPqnBvpnLAIeTQdPT3j16EUer+yaQpT8i8pqAVi9h4cu1kZGR69bt7y9h/7p1EFz7cmEC+iTiwkgczUKmkC8ECkIs54wiI4j2j5EXQwj9QyCBNgCRIZbIj3ZRYET+nCVkP4TwKqTJHoJTXlbFscZzlv6WXr1CkpGKXhb0PozVynoj1SiTHV3lmvYC1JZnCQzNOG9a1BY1aAlKqbBYLEm8jWYSnpGypmBQTQ0sUp9iVCqr/bl60SK1UqlchIIzYCABB/BCIRR/breqVMwp9SIlUA2u39mVCPUWBlZQHz8DiT591t5euPD2WiUR6rXfiAKhnBHvhXVkrs9AGWX2dFTLGX0AZfxCTiaTHYwChTjl1LL8HZ+YccvJa52nfCL+IHyT3Lb4kVi1pAqOBPqMjPfSYHBmZBQSI6Nm0TTDXicCFCsy9HWfiDqGDPTxIzEFqTqDMVF6BTIYHEXUKhDMrMGEKddstIr2qajQYzT6sIeIrENVCsVc1VuRQeiU0FAYU4gBEkhfY5CBT4TCDJABkZWoCx0xxGB26BSspqyiFSowwAKUSNvoWUQQA1p/NRTLC9R5El+jZ5BBR9Qzffr0VbTICrNiQSC9yskKrN9g+hkoVOljp2N5njrUBdExlhj07UI4qGVZ7ePpREw/KLJa5xxfqu9sBRjMjCXJQ5SvaI3ehg0mELlz94oVu3ei3IQJE2J3eu0rdt/AAvAZoEMwp2L7Iias0aOlAYO+hHmrV89De5KVqAndicEaUhlLremOCJuHDeZ0D+v+V4jBPFK5hjrfA+E3CAvr8RfCwvwGWJ6nLmgQYTF6Gr4cZACCELN6Z+Kbr1/eJO5cHaOBBAyNpkeYZg86kWbGgAAuUBXDw8OXh4fH6BlkoNGgY43mxumkbR6HySiIomBKcHi2JZ2+odHELIdSzR5YbevMmHBMRar2EEyMXgXxOcvD4Viza88mnuNYhmNpDMMwIr9pzy4NJMOXIwMGDHBfbaog3k+LQzNgHscNmTZkSNwut0mgaQH6OK0NtmDG8yb3rrghY8YMiZuD5jozbhoSQwpShaeNAaYt3obWYIt58TQkzKe3JYisKCoUNKegwUFI8J7257agSi8SIAtT6UqPRxjc+De2axocLzYbzObjSVtX8Dyj1TICv2Jr0nEzsHLMmPFxZvS75d2GMSDGl4bbxypyuXy8wXBLUMloe6JBvtJgGB8BjitdiUfmuO/dc885kuhaaRg/fiVUwUi00zKVcAuqoLEKRVGN5UCE4bhJRdPiFrPZJZcQESEPwOUybxFpWmU6bsCZxuiyXnKEfIQ8wjWTZxSs6WRExNwRfiCByn4IOSRPwr+kit/qikChkvjmNc/Qob2HznWd5G1WgXbc2TF36A9GkI0fqLrj4AWrjT/pmju0d++heShEhqy9EUt3CwKso9F75OHSHTuWLp3bW8LcpSj28IjXCCsoCLuv4GjWDBSm5sCBSG6/zMNJDldjh2fBggVbEwdicOXAz1sh5HHwrBbON3bTdpKoCc1kCrg00xMHzICzwanD0VrTkoESlpi06L/dZuMUMoXjSaaNKIgmQMgzFti48corj1aEk88KJwpjXDJWwhIjSysUVi0HJ4bn1ZWNG1EwT/ItTpnhhCVuO8sKCjDgjNuHSwAD9M8lsJzdvcQXKyO502ueaTQwaNDmza/dl50+g9EStsMMwMB52f168+bho4dDKFNzSkKBQX4mH37w6MT69etPHCaacBiHHj04PHnQoNGkrkDgjWb6NslMJrSRhjZPHhQYSp+WCiCoWHJu2LBhl2BIDSASGMoY9MvNdjFfKUA2RCRHpQbFfne7nrHDqA4dOozCG/Qie6mAQURG6P+VoPS4DED7wONAkT4ohUeeAtm6/QPZCqSlUqJF+U6dOnVDA23Q/ldRvsUfH/vyZuv0R7LlDaL+TOsamSdNmjTuBwEic43W1N/JkDfH1EmIqT78xznyZvjXh+9m6XNM/Ikc6Zulof6DdIUb1s1Y6n3m+/czvy+VsW7Dwik9/n8HzjZEy9x05tIAAAAASUVORK5CYII=" alt="QQ频道"> <img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk">
</a>
</a> </a>
<br /> <br />
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh"> <a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
@@ -65,15 +71,13 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
<p align="center"> <p align="center">
<a href="https://v2.nonebot.dev/">文档</a> <a href="https://v2.nonebot.dev/">文档</a>
· ·
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a> <a href="https://v2.nonebot.dev/docs/quick-start">快速上手</a>
·
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
· ·
<a href="#插件">文档打不开?</a> <a href="#插件">文档打不开?</a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://asciinema.org/a/464654"> <a href="https://asciinema.org/a/569440">
<img src="https://v2.nonebot.dev/img/setup.svg"> <img src="https://v2.nonebot.dev/img/setup.svg">
</a> </a>
</p> </p>
@@ -86,20 +90,34 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如 - 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑 - 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
- 生而可靠100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support)) - 生而可靠100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/editor-support))
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源)) - 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议 - 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
- [OneBot 协议](https://onebot.dev/) (QQ 等)
- [钉钉](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p) | 协议名称 | 状态 | 注释 |
- [Telegram](https://core.telegram.org/bots/api) | :-----------------------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
- [飞书](https://open.feishu.cn/document/home/index) | [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
- [QQ 频道](https://bot.q.qq.com/wiki/) | [Telegram](https://core.telegram.org/bots/api) | ✅ | |
- 坚实后盾:支持多种 web 框架,可自定义替换 | [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
- [FastAPI](https://fastapi.tiangolo.com/) | [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
- [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask) | [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
- [aiohttp](https://docs.aiohttp.org/en/stable/) | [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
- [httpx](https://www.python-httpx.org/) | Console | ✅ | 控制台交互 |
- [websockets](https://websockets.readthedocs.io/en/stable/) | [开黑啦](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) | ↗️ | 由社区贡献 |
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
| 驱动框架 | 类型 |
| :--------------------------------------------------------: | :----: |
| [FastAPI](https://fastapi.tiangolo.com/) | 服务端 |
| [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask) | 服务端 |
| [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 |
| [httpx](https://www.python-httpx.org/) | 客户端 |
| [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 |
更多:[概览](https://v2.nonebot.dev/docs/) 更多:[概览](https://v2.nonebot.dev/docs/)
@@ -117,12 +135,17 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
懒得看文档?下面是快速安装指南: 懒得看文档?下面是快速安装指南:
1. (**强烈建议**)使用你喜欢的 Python 环境管理工具创建新的虚拟环境。 1. 安装 [pipx](https://pypa.github.io/pipx/)
2. 使用 `pip` (或其他) 安装 NoneBot 脚手架。
```bash ```bash
pip install nb-cli python -m pip install --user pipx
python -m pipx ensurepath
```
2. 安装脚手架
```bash
pipx install nb-cli
``` ```
3. 使用脚手架创建项目 3. 使用脚手架创建项目
@@ -131,6 +154,12 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
nb create nb create
``` ```
4. 运行项目
```bash
nb run
```
## 社区资源 ## 社区资源
### 常见问题 ### 常见问题
@@ -183,5 +212,5 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
感谢以下开发者对 NoneBot2 作出的贡献: 感谢以下开发者对 NoneBot2 作出的贡献:
<a href="https://github.com/nonebot/nonebot2/graphs/contributors"> <a href="https://github.com/nonebot/nonebot2/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2" /> <img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" />
</a> </a>

View File

@@ -16,6 +16,7 @@
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>` - `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>` - `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>` - `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>` - `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>` - `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>` - `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
@@ -25,9 +26,10 @@
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>` - `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>` - `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>` - `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>` - `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>` - `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
- `export` => {ref}``export` <nonebot.plugin.export.export>` - `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
- `require` => {ref}``require` <nonebot.plugin.load.require>` - `require` => {ref}``require` <nonebot.plugin.load.require>`
FrontMatter: FrontMatter:
@@ -35,24 +37,25 @@ FrontMatter:
description: nonebot 模块 description: nonebot 模块
""" """
import importlib import os
from typing import Any, Dict, Type, Optional from importlib.metadata import version
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
import pkg_resources 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.config import Env, Config
from nonebot.log import logger, default_filter 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 from nonebot.drivers import Driver, ReverseDriver, combine_driver
try: try:
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2") __version__ = version("nonebot2")
__version__ = _dist.version except Exception: # pragma: no cover
VERSION = _dist.parsed_version
except pkg_resources.DistributionNotFound: # pragma: no cover
__version__ = None __version__ = None
VERSION = None
A = TypeVar("A", bound=Adapter)
_driver: Optional[Driver] = None _driver: Optional[Driver] = None
@@ -78,6 +81,56 @@ def get_driver() -> Driver:
return _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: def get_app() -> Any:
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。 """获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
@@ -170,42 +223,35 @@ def get_bots() -> Dict[str, Bot]:
bots = nonebot.get_bots() bots = nonebot.get_bots()
``` ```
""" """
driver = get_driver() return get_driver().bots
return 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]: def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
drivers = obj_str.split("+") drivers = obj_str.split("+")
DriverClass = _resolve_dot_notation( DriverClass = resolve_dot_notation(
drivers[0], "Driver", default_prefix="nonebot.drivers." drivers[0], "Driver", default_prefix="nonebot.drivers."
) )
if len(drivers) == 1: if len(drivers) == 1:
logger.trace(f"Detected driver {DriverClass} with no mixins.") logger.trace(f"Detected driver {DriverClass} with no mixins.")
return DriverClass return DriverClass
mixins = [ mixins = [
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.") resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
for mixin in drivers[1:] for mixin in drivers[1:]
] ]
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.") logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
return combine_driver(DriverClass, *mixins) return combine_driver(DriverClass, *mixins)
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None: def _log_patcher(record: "loguru.Record"):
record["name"] = (
plugin.name
if (module_name := record["name"])
and (plugin := get_plugin_by_module_name(module_name))
else (module_name and module_name.split(".")[0])
)
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。 """初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
@@ -225,13 +271,17 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
if not _driver: if not _driver:
logger.success("NoneBot is initializing...") logger.success("NoneBot is initializing...")
env = Env() env = Env()
_env_file = _env_file or f".env.{env.environment}"
config = Config( config = Config(
**kwargs, **kwargs,
_common_config=env.dict(), _env_file=(".env", _env_file)
_env_file=_env_file or f".env.{env.environment}", if isinstance(_env_file, (str, os.PathLike))
else _env_file,
) )
default_filter.level = config.log_level logger.configure(
extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher
)
logger.opt(colors=True).info( logger.opt(colors=True).info(
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>" f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
) )
@@ -239,7 +289,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}" f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
) )
DriverClass: Type[Driver] = _resolve_combine_expr(config.driver) DriverClass = _resolve_combine_expr(config.driver)
_driver = DriverClass(env, config) _driver = DriverClass(env, config)
@@ -260,7 +310,7 @@ def run(*args: Any, **kwargs: Any) -> None:
from nonebot.plugin import on as on from nonebot.plugin import on as on
from nonebot.plugin import export as export from nonebot.plugin import on_type as on_type
from nonebot.plugin import require as require from nonebot.plugin import require as require
from nonebot.plugin import on_regex as on_regex from nonebot.plugin import on_regex as on_regex
from nonebot.plugin import on_notice as on_notice from nonebot.plugin import on_notice as on_notice
@@ -284,5 +334,7 @@ from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
__autodoc__ = {"internal": False} __autodoc__ = {"internal": False}

View File

@@ -7,21 +7,6 @@ FrontMatter:
description: nonebot.adapters 模块 description: nonebot.adapters 模块
""" """
from typing import Iterable
try:
import pkg_resources
pkg_resources.declare_namespace(__name__)
del pkg_resources
except ImportError:
import pkgutil
__path__: Iterable[str] = pkgutil.extend_path(__path__, __name__) # type: ignore
del pkgutil
except Exception:
pass
from nonebot.internal.adapter import Bot as Bot from nonebot.internal.adapter import Bot as Bot
from nonebot.internal.adapter import Event as Event from nonebot.internal.adapter import Event as Event
from nonebot.internal.adapter import Adapter as Adapter from nonebot.internal.adapter import Adapter as Adapter

View File

@@ -9,23 +9,21 @@ FrontMatter:
description: nonebot.config 模块 description: nonebot.config 模块
""" """
import os import os
from pathlib import Path
from datetime import timedelta from datetime import timedelta
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
from pydantic import BaseSettings, IPvAnyAddress from pydantic.utils import deep_update
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
from pydantic.env_settings import ( from pydantic.env_settings import (
DotenvType,
SettingsError, SettingsError,
EnvSettingsSource, EnvSettingsSource,
InitSettingsSource, InitSettingsSource,
SettingsSourceCallable, SettingsSourceCallable,
read_env_file,
env_file_sentinel,
) )
from nonebot.log import logger from nonebot.log import logger
from nonebot.utils import escape_tag
class CustomEnvSettings(EnvSettingsSource): class CustomEnvSettings(EnvSettingsSource):
@@ -33,32 +31,14 @@ class CustomEnvSettings(EnvSettingsSource):
""" """
Build environment variables suitable for passing to the Model. Build environment variables suitable for passing to the Model.
""" """
d: Dict[str, Optional[str]] = {} d: Dict[str, Any] = {}
if settings.__config__.case_sensitive: if settings.__config__.case_sensitive:
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
else: else:
env_vars = {k.lower(): v for k, v in os.environ.items()} env_vars = {k.lower(): v for k, v in os.environ.items()}
env_file_vars: Dict[str, Optional[str]] = {} env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
env_file = (
self.env_file
if self.env_file != env_file_sentinel
else settings.__config__.env_file
)
env_file_encoding = (
self.env_file_encoding
if self.env_file_encoding is not None
else settings.__config__.env_file_encoding
)
if env_file is not None:
env_path = Path(env_file)
if env_path.is_file():
env_file_vars = read_env_file(
env_path,
encoding=env_file_encoding, # type: ignore
case_sensitive=settings.__config__.case_sensitive,
)
env_vars = {**env_file_vars, **env_vars} env_vars = {**env_file_vars, **env_vars}
for field in settings.__fields__.values(): for field in settings.__fields__.values():
@@ -70,30 +50,56 @@ class CustomEnvSettings(EnvSettingsSource):
if env_val is not None: if env_val is not None:
break break
is_complex, allow_parse_failure = self.field_is_complex(field)
if is_complex:
if env_val is None: if env_val is None:
continue if env_val_built := self.explode_env_vars(field, env_vars):
d[field.alias] = env_val_built
if field.is_complex(): else:
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
try: try:
env_val = settings.__config__.json_loads(env_val) env_val = settings.__config__.parse_env_var(field.name, env_val)
except ValueError as e: # pragma: no cover except ValueError as e:
if not allow_parse_failure:
raise SettingsError( raise SettingsError(
f'error parsing JSON for "{env_name}"' # type: ignore f'error parsing env var "{env_name}"' # type: ignore
) from e ) from e
if isinstance(env_val, dict):
d[field.alias] = deep_update(
env_val, self.explode_env_vars(field, env_vars)
)
else:
d[field.alias] = env_val
elif env_val is not None:
# simplest case, field is not complex, we only need to add the value if it was found
d[field.alias] = env_val d[field.alias] = env_val
if env_file_vars: # remain user custom config
for env_name, env_val in env_file_vars.items(): for env_name in env_file_vars:
if (env_val is None or len(env_val) == 0) and env_name in env_vars:
env_val = env_vars[env_name] env_val = env_vars[env_name]
if env_val and (val_striped := env_val.strip()):
# there's a value, decode that as JSON
try: try:
if env_val: env_val = settings.__config__.parse_env_var(env_name, val_striped)
env_val = settings.__config__.json_loads(env_val.strip())
except ValueError as e: except ValueError as e:
logger.opt(colors=True, exception=e).trace( logger.trace(
f"Error while parsing JSON for {escape_tag(env_name)}. Assumed as string." "Error while parsing JSON for "
f"{env_name!r}={val_striped!r}. "
"Assumed as string."
) )
# explode value when it's a nested dict
env_name, *nested_keys = env_name.split(self.env_nested_delimiter)
if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):
result = {}
*keys, last_key = nested_keys
_tmp = result
for key in keys:
_tmp = _tmp.setdefault(key, {})
_tmp[last_key] = env_val
d[env_name] = deep_update(d.get(env_name, {}), result)
elif not nested_keys:
d[env_name] = env_val d[env_name] = env_val
return d return d
@@ -106,6 +112,9 @@ class BaseConfig(BaseSettings):
return self.__dict__.get(name) return self.__dict__.get(name)
class Config: class Config:
extra = Extra.allow
env_nested_delimiter = "__"
@classmethod @classmethod
def customise_sources( def customise_sources(
cls, cls,
@@ -117,7 +126,10 @@ class BaseConfig(BaseSettings):
return ( return (
init_settings, init_settings,
CustomEnvSettings( CustomEnvSettings(
env_settings.env_file, env_settings.env_file_encoding env_settings.env_file,
env_settings.env_file_encoding,
env_settings.env_nested_delimiter,
env_settings.env_prefix_len,
), ),
InitSettingsSource(common_config), InitSettingsSource(common_config),
file_secret_settings, file_secret_settings,
@@ -137,7 +149,6 @@ class Env(BaseConfig):
""" """
class Config: class Config:
extra = "allow"
env_file = ".env" env_file = ".env"
@@ -147,11 +158,10 @@ class Config(BaseConfig):
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
这些配置将会在 json 反序列化后一起带入 `Config` 类中。 这些配置将会在 json 反序列化后一起带入 `Config` 类中。
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration) 配置方法参考: [配置](https://v2.nonebot.dev/docs/appendices/config)
""" """
_env_file: str = ".env" _env_file: DotenvType = ".env", ".env.prod"
_common_config: Dict[str, Any] = {}
# nonebot configs # nonebot configs
driver: str = "~fastapi" driver: str = "~fastapi"
@@ -163,7 +173,7 @@ class Config(BaseConfig):
""" """
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。""" """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
port: int = 8080 port: int = Field(default=8080, ge=1, le=65535)
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。""" """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
log_level: Union[int, str] = "INFO" log_level: Union[int, str] = "INFO"
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称 """NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
@@ -231,8 +241,7 @@ class Config(BaseConfig):
# or from env file using json loads # or from env file using json loads
class Config: class Config:
extra = "allow" env_file = ".env", ".env.prod"
env_file = ".env.prod"
__autodoc__ = { __autodoc__ = {

View File

@@ -4,7 +4,9 @@ FrontMatter:
sidebar_position: 9 sidebar_position: 9
description: nonebot.consts 模块 description: nonebot.consts 模块
""" """
from typing_extensions import Literal import os
import sys
from typing import Literal
# used by Matcher # used by Matcher
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}" RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
@@ -30,6 +32,8 @@ CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
"""命令参数存储 key""" """命令参数存储 key"""
CMD_START_KEY: Literal["command_start"] = "command_start" CMD_START_KEY: Literal["command_start"] = "command_start"
"""命令开头存储 key""" """命令开头存储 key"""
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
"""命令与参数间空白符存储 key"""
SHELL_ARGS: Literal["_args"] = "_args" SHELL_ARGS: Literal["_args"] = "_args"
"""shell 命令 parse 后参数字典存储 key""" """shell 命令 parse 后参数字典存储 key"""
@@ -38,7 +42,19 @@ SHELL_ARGV: Literal["_argv"] = "_argv"
REGEX_MATCHED: Literal["_matched"] = "_matched" REGEX_MATCHED: Literal["_matched"] = "_matched"
"""正则匹配结果存储 key""" """正则匹配结果存储 key"""
REGEX_STR: Literal["_matched_str"] = "_matched_str"
"""正则匹配文本存储 key"""
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups" REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
"""正则匹配 group 元组存储 key""" """正则匹配 group 元组存储 key"""
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict" REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
"""正则匹配 group 字典存储 key""" """正则匹配 group 字典存储 key"""
STARTSWITH_KEY: Literal["_startswith"] = "_startswith"
"""响应触发前缀 key"""
ENDSWITH_KEY: Literal["_endswith"] = "_endswith"
"""响应触发后缀 key"""
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

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

View File

@@ -4,11 +4,11 @@ FrontMatter:
description: nonebot.dependencies.utils 模块 description: nonebot.dependencies.utils 模块
""" """
import inspect import inspect
from typing import Any, Dict, TypeVar, Callable from typing import Any, Dict, TypeVar, Callable, ForwardRef
from loguru import logger from loguru import logger
from pydantic.fields import ModelField from pydantic.fields import ModelField
from pydantic.typing import ForwardRef, evaluate_forwardref from pydantic.typing import evaluate_forwardref
from nonebot.exception import TypeMisMatch from nonebot.exception import TypeMisMatch
@@ -28,8 +28,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
) )
for param in signature.parameters.values() for param in signature.parameters.values()
] ]
typed_signature = inspect.Signature(typed_params) return inspect.Signature(typed_params)
return typed_signature
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any: def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:

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

@@ -21,16 +21,16 @@ from contextlib import asynccontextmanager
from nonebot.typing import overrides from nonebot.typing import overrides
from nonebot.drivers import Request, Response from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.drivers._block_driver import BlockDriver from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
try: try:
import aiohttp import aiohttp
except ImportError: except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`" "Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
) from None ) from e
class Mixin(ForwardMixin): class Mixin(ForwardMixin):
@@ -56,7 +56,13 @@ class Mixin(ForwardMixin):
files = aiohttp.FormData() files = aiohttp.FormData()
for name, file in setup.files: for name, file in setup.files:
files.add_field(name, file[1], content_type=file[2], filename=file[0]) files.add_field(name, file[1], content_type=file[2], filename=file[0])
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
cookies = {
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
}
async with aiohttp.ClientSession(
cookies=cookies, version=version, trust_env=True
) as session:
async with session.request( async with session.request(
setup.method, setup.method,
setup.url, setup.url,
@@ -66,13 +72,12 @@ class Mixin(ForwardMixin):
timeout=timeout, timeout=timeout,
proxy=setup.proxy, proxy=setup.proxy,
) as response: ) as response:
res = Response( return Response(
response.status, response.status,
headers=response.headers.copy(), headers=response.headers.copy(),
content=await response.read(), content=await response.read(),
request=setup, request=setup,
) )
return res
@overrides(ForwardMixin) @overrides(ForwardMixin)
@asynccontextmanager @asynccontextmanager
@@ -92,8 +97,7 @@ class Mixin(ForwardMixin):
headers=setup.headers, headers=setup.headers,
proxy=setup.proxy, proxy=setup.proxy,
) as ws: ) as ws:
websocket = WebSocket(request=setup, session=session, websocket=ws) yield WebSocket(request=setup, session=session, websocket=ws)
yield websocket
class WebSocket(BaseWebSocket): class WebSocket(BaseWebSocket):
@@ -166,5 +170,5 @@ class WebSocket(BaseWebSocket):
await self.websocket.send_bytes(data) await self.websocket.send_bytes(data)
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
"""AIOHTTP Driver""" """AIOHTTP Driver"""

View File

@@ -1,5 +1,11 @@
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配 """[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
```bash
nb driver install fastapi
# 或者
pip install nonebot2[fastapi]
```
:::tip 提示 :::tip 提示
本驱动仅支持服务端连接 本驱动仅支持服务端连接
::: :::
@@ -9,15 +15,13 @@ FrontMatter:
description: nonebot.drivers.fastapi 模块 description: nonebot.drivers.fastapi 模块
""" """
import logging
from functools import wraps
from typing import Any, List, Tuple, Union, Callable, Optional
import uvicorn import logging
import contextlib
from functools import wraps
from typing import Any, Dict, List, Tuple, Union, Optional
from pydantic import BaseSettings from pydantic import BaseSettings
from fastapi.responses import Response
from fastapi import FastAPI, Request, UploadFile, status
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
from nonebot.config import Env from nonebot.config import Env
from nonebot.typing import overrides from nonebot.typing import overrides
@@ -28,6 +32,18 @@ from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup 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 ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
) from e
def catch_closed(func): def catch_closed(func):
@wraps(func) @wraps(func)
@@ -57,12 +73,14 @@ class Config(BaseSettings):
"""开启/关闭冷重载""" """开启/关闭冷重载"""
fastapi_reload_dirs: Optional[List[str]] = None fastapi_reload_dirs: Optional[List[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值""" """重载监控文件夹列表,默认为 uvicorn 默认值"""
fastapi_reload_delay: Optional[float] = None fastapi_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值""" """重载延迟,默认为 uvicorn 默认值"""
fastapi_reload_includes: Optional[List[str]] = None fastapi_reload_includes: Optional[List[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值""" """要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
fastapi_reload_excludes: Optional[List[str]] = None fastapi_reload_excludes: Optional[List[str]] = None
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值""" """不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
fastapi_extra: Dict[str, Any] = {}
"""传递给 `FastAPI` 的其他参数。"""
class Config: class Config:
extra = "ignore" extra = "ignore"
@@ -76,10 +94,14 @@ class Driver(ReverseDriver):
self.fastapi_config: Config = Config(**config.dict()) self.fastapi_config: Config = Config(**config.dict())
self._lifespan = Lifespan()
self._server_app = FastAPI( self._server_app = FastAPI(
lifespan=self._lifespan_manager,
openapi_url=self.fastapi_config.fastapi_openapi_url, openapi_url=self.fastapi_config.fastapi_openapi_url,
docs_url=self.fastapi_config.fastapi_docs_url, docs_url=self.fastapi_config.fastapi_docs_url,
redoc_url=self.fastapi_config.fastapi_redoc_url, redoc_url=self.fastapi_config.fastapi_redoc_url,
**self.fastapi_config.fastapi_extra,
) )
@property @property
@@ -131,14 +153,20 @@ class Driver(ReverseDriver):
) )
@overrides(ReverseDriver) @overrides(ReverseDriver)
def on_startup(self, func: Callable) -> Callable: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_""" return self._lifespan.on_startup(func)
return self.server_app.on_event("startup")(func)
@overrides(ReverseDriver) @overrides(ReverseDriver)
def on_shutdown(self, func: Callable) -> Callable: def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_""" return self._lifespan.on_shutdown(func)
return self.server_app.on_event("shutdown")(func)
@contextlib.asynccontextmanager
async def _lifespan_manager(self, app: FastAPI):
await self._lifespan.startup()
try:
yield
finally:
await self._lifespan.shutdown()
@overrides(ReverseDriver) @overrides(ReverseDriver)
def run( def run(
@@ -186,14 +214,12 @@ class Driver(ReverseDriver):
setup: HTTPServerSetup, setup: HTTPServerSetup,
) -> Response: ) -> Response:
json: Any = None json: Any = None
try: with contextlib.suppress(Exception):
json = await request.json() json = await request.json()
except Exception:
pass
data: Optional[dict] = None data: Optional[dict] = None
files: Optional[List[Tuple[str, FileTypes]]] = None files: Optional[List[Tuple[str, FileTypes]]] = None
try: with contextlib.suppress(Exception):
form = await request.form() form = await request.form()
data = {} data = {}
files = [] files = []
@@ -204,8 +230,7 @@ class Driver(ReverseDriver):
) )
else: else:
data[key] = value data[key] = value
except Exception:
pass
http_request = BaseRequest( http_request = BaseRequest(
request.method, request.method,
str(request.url), str(request.url),
@@ -219,7 +244,9 @@ class Driver(ReverseDriver):
) )
response = await setup.handle_func(http_request) response = await setup.handle_func(http_request)
return Response(response.content, response.status_code, dict(response.headers)) return Response(
response.content, response.status_code, dict(response.headers.items())
)
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup): async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
request = BaseRequest( request = BaseRequest(
@@ -261,7 +288,7 @@ class FastAPIWebSocket(BaseWebSocket):
async def close( async def close(
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = "" self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
) -> None: ) -> None:
await self.websocket.close(code) await self.websocket.close(code, reason)
@overrides(BaseWebSocket) @overrides(BaseWebSocket)
async def receive(self) -> Union[str, bytes]: async def receive(self) -> Union[str, bytes]:

View File

@@ -18,7 +18,7 @@ from typing import Type, AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from nonebot.typing import overrides from nonebot.typing import overrides
from nonebot.drivers._block_driver import BlockDriver from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import ( from nonebot.drivers import (
Request, Request,
Response, Response,
@@ -31,10 +31,10 @@ from nonebot.drivers import (
try: try:
import httpx import httpx
except ImportError: except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install httpx by using `pip install nonebot2[httpx]`" "Please install httpx by using `pip install nonebot2[httpx]`"
) from None ) from e
class Mixin(ForwardMixin): class Mixin(ForwardMixin):
@@ -48,17 +48,18 @@ class Mixin(ForwardMixin):
@overrides(ForwardMixin) @overrides(ForwardMixin)
async def request(self, setup: Request) -> Response: async def request(self, setup: Request) -> Response:
async with httpx.AsyncClient( async with httpx.AsyncClient(
cookies=setup.cookies.jar,
http2=setup.version == HTTPVersion.H2, http2=setup.version == HTTPVersion.H2,
proxies=setup.proxy, # type: ignore proxies=setup.proxy,
follow_redirects=True, follow_redirects=True,
) as client: ) as client:
response = await client.request( response = await client.request(
setup.method, setup.method,
str(setup.url), str(setup.url),
content=setup.content, # type: ignore content=setup.content,
data=setup.data, # type: ignore data=setup.data,
json=setup.json, json=setup.json,
files=setup.files, # type: ignore files=setup.files,
headers=tuple(setup.headers.items()), headers=tuple(setup.headers.items()),
timeout=setup.timeout, timeout=setup.timeout,
) )
@@ -76,5 +77,5 @@ class Mixin(ForwardMixin):
yield ws yield ws
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
"""HTTPX Driver""" """HTTPX Driver"""

View File

@@ -1,78 +1,90 @@
"""None 驱动适配
:::tip 提示
本驱动不支持任何服务器或客户端连接
:::
FrontMatter:
sidebar_position: 6
description: nonebot.drivers.none 模块
"""
import signal import signal
import asyncio import asyncio
import threading import threading
from typing import Set, Callable, Awaitable
from nonebot.log import logger from nonebot.log import logger
from nonebot.drivers import Driver from nonebot.consts import WINDOWS
from nonebot.typing import overrides from nonebot.typing import overrides
from nonebot.config import Env, Config from nonebot.config import Env, Config
from nonebot.drivers import Driver as BaseDriver
from ._lifespan import LIFESPAN_FUNC, Lifespan
STARTUP_FUNC = Callable[[], Awaitable[None]]
SHUTDOWN_FUNC = Callable[[], Awaitable[None]]
HANDLED_SIGNALS = ( HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`. 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 BlockDriver(Driver): class Driver(BaseDriver):
"""None 驱动框架"""
def __init__(self, env: Env, config: Config): def __init__(self, env: Env, config: Config):
super().__init__(env, config) super().__init__(env, config)
self.startup_funcs: Set[STARTUP_FUNC] = set()
self.shutdown_funcs: Set[SHUTDOWN_FUNC] = set() self._lifespan = Lifespan()
self.should_exit: asyncio.Event = asyncio.Event() self.should_exit: asyncio.Event = asyncio.Event()
self.force_exit: bool = False self.force_exit: bool = False
@property @property
@overrides(Driver) @overrides(BaseDriver)
def type(self) -> str: def type(self) -> str:
"""驱动名称: `block_driver`""" """驱动名称: `none`"""
return "block_driver" return "none"
@property @property
@overrides(Driver) @overrides(BaseDriver)
def logger(self): def logger(self):
"""block driver 使用的 logger""" """none driver 使用的 logger"""
return logger return logger
@overrides(Driver) @overrides(BaseDriver)
def on_startup(self, func: STARTUP_FUNC) -> STARTUP_FUNC: def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
""" """
注册一个启动时执行的函数 注册一个启动时执行的函数
""" """
self.startup_funcs.add(func) return self._lifespan.on_startup(func)
return func
@overrides(Driver) @overrides(BaseDriver)
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC: def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
""" """
注册一个停止时执行的函数 注册一个停止时执行的函数
""" """
self.shutdown_funcs.add(func) return self._lifespan.on_shutdown(func)
return func
@overrides(Driver) @overrides(BaseDriver)
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
"""启动 block driver""" """启动 none driver"""
super().run(*args, **kwargs) super().run(*args, **kwargs)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(self.serve()) loop.run_until_complete(self._serve())
async def serve(self): async def _serve(self):
self.install_signal_handlers() self._install_signal_handlers()
await self.startup() await self._startup()
if self.should_exit.is_set(): if self.should_exit.is_set():
return return
await self.main_loop() await self._main_loop()
await self.shutdown() await self._shutdown()
async def startup(self): async def _startup(self):
# run startup
cors = [startup() for startup in self.startup_funcs]
if cors:
try: try:
await asyncio.gather(*cors) await self._lifespan.startup()
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running startup function. " "<r><bg #f8bbd0>Error when running startup function. "
@@ -81,18 +93,16 @@ class BlockDriver(Driver):
logger.info("Application startup completed.") logger.info("Application startup completed.")
async def main_loop(self): async def _main_loop(self):
await self.should_exit.wait() await self.should_exit.wait()
async def shutdown(self): async def _shutdown(self):
logger.info("Shutting down") logger.info("Shutting down")
logger.info("Waiting for application shutdown.") logger.info("Waiting for application shutdown.")
# run shutdown
cors = [shutdown() for shutdown in self.shutdown_funcs]
if cors:
try: try:
await asyncio.gather(*cors) await self._lifespan.shutdown()
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running shutdown function. " "<r><bg #f8bbd0>Error when running shutdown function. "
@@ -120,7 +130,7 @@ class BlockDriver(Driver):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.stop() loop.stop()
def install_signal_handlers(self) -> None: def _install_signal_handlers(self) -> None:
if threading.current_thread() is not threading.main_thread(): if threading.current_thread() is not threading.main_thread():
# Signals can only be listened to from the main thread. # Signals can only be listened to from the main thread.
return return
@@ -129,13 +139,13 @@ class BlockDriver(Driver):
try: try:
for sig in HANDLED_SIGNALS: for sig in HANDLED_SIGNALS:
loop.add_signal_handler(sig, self.handle_exit, sig, None) loop.add_signal_handler(sig, self._handle_exit, sig, None)
except NotImplementedError: except NotImplementedError:
# Windows # Windows
for sig in HANDLED_SIGNALS: for sig in HANDLED_SIGNALS:
signal.signal(sig, self.handle_exit) signal.signal(sig, self._handle_exit)
def handle_exit(self, sig, frame): def _handle_exit(self, sig, frame):
if self.should_exit.is_set(): if self.should_exit.is_set():
self.force_exit = True self.force_exit = True
else: else:

View File

@@ -17,9 +17,8 @@ FrontMatter:
import asyncio import asyncio
from functools import wraps from functools import wraps
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
import uvicorn
from pydantic import BaseSettings from pydantic import BaseSettings
from nonebot.config import Env from nonebot.config import Env
@@ -32,15 +31,16 @@ from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
try: try:
import uvicorn
from quart import request as _request from quart import request as _request
from quart import websocket as _websocket from quart import websocket as _websocket
from quart import Quart, Request, Response from quart import Quart, Request, Response
from quart.datastructures import FileStorage from quart.datastructures import FileStorage
from quart import Websocket as QuartWebSocket from quart import Websocket as QuartWebSocket
except ImportError: except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install Quart by using `pip install nonebot2[quart]`" "Please install Quart by using `pip install nonebot2[quart]`"
) from None ) from e
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) _AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
@@ -63,12 +63,14 @@ class Config(BaseSettings):
"""开启/关闭冷重载""" """开启/关闭冷重载"""
quart_reload_dirs: Optional[List[str]] = None quart_reload_dirs: Optional[List[str]] = None
"""重载监控文件夹列表,默认为 uvicorn 默认值""" """重载监控文件夹列表,默认为 uvicorn 默认值"""
quart_reload_delay: Optional[float] = None quart_reload_delay: float = 0.25
"""重载延迟,默认为 uvicorn 默认值""" """重载延迟,默认为 uvicorn 默认值"""
quart_reload_includes: Optional[List[str]] = None quart_reload_includes: Optional[List[str]] = None
"""要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值""" """要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
quart_reload_excludes: Optional[List[str]] = None quart_reload_excludes: Optional[List[str]] = None
"""不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值""" """不要监听的文件列表,支持 glob pattern默认为 uvicorn 默认值"""
quart_extra: Dict[str, Any] = {}
"""传递给 `Quart` 的其他参数。"""
class Config: class Config:
extra = "ignore" extra = "ignore"
@@ -82,7 +84,9 @@ class Driver(ReverseDriver):
self.quart_config = Config(**config.dict()) self.quart_config = Config(**config.dict())
self._server_app = Quart(self.__class__.__qualname__) self._server_app = Quart(
self.__class__.__qualname__, **self.quart_config.quart_extra
)
@property @property
@overrides(ReverseDriver) @overrides(ReverseDriver)

View File

@@ -23,17 +23,17 @@ from nonebot.typing import overrides
from nonebot.log import LoguruHandler from nonebot.log import LoguruHandler
from nonebot.drivers import Request, Response from nonebot.drivers import Request, Response
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.drivers._block_driver import BlockDriver from nonebot.drivers.none import Driver as NoneDriver
from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
try: try:
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed
from websockets.legacy.client import Connect, WebSocketClientProtocol from websockets.legacy.client import Connect, WebSocketClientProtocol
except ImportError: except ModuleNotFoundError as e: # pragma: no cover
raise ImportError( raise ImportError(
"Please install websockets by using `pip install nonebot2[websockets]`" "Please install websockets by using `pip install nonebot2[websockets]`"
) ) from e
logger = logging.Logger("websockets.client", "INFO") logger = logging.Logger("websockets.client", "INFO")
logger.addHandler(LoguruHandler()) logger.addHandler(LoguruHandler())
@@ -70,7 +70,7 @@ class Mixin(ForwardMixin):
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
connection = Connect( connection = Connect(
str(setup.url), str(setup.url),
extra_headers=setup.headers.items(), extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
open_timeout=setup.timeout, open_timeout=setup.timeout,
) )
async with connection as ws: async with connection as ws:
@@ -101,8 +101,7 @@ class WebSocket(BaseWebSocket):
@overrides(BaseWebSocket) @overrides(BaseWebSocket)
@catch_closed @catch_closed
async def receive(self) -> Union[str, bytes]: async def receive(self) -> Union[str, bytes]:
msg = await self.websocket.recv() return await self.websocket.recv()
return msg
@overrides(BaseWebSocket) @overrides(BaseWebSocket)
@catch_closed @catch_closed
@@ -129,5 +128,5 @@ class WebSocket(BaseWebSocket):
await self.websocket.send(data) await self.websocket.send(data)
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
"""Websockets Driver""" """Websockets Driver"""

View File

@@ -37,6 +37,9 @@ from pydantic.fields import ModelField
class NoneBotException(Exception): class NoneBotException(Exception):
"""所有 NoneBot 发生的异常基类。""" """所有 NoneBot 发生的异常基类。"""
def __str__(self) -> str:
return self.__repr__()
# Rule Exception # Rule Exception
class ParserExit(NoneBotException): class ParserExit(NoneBotException):
@@ -46,11 +49,12 @@ class ParserExit(NoneBotException):
self.status = status self.status = status
self.message = message self.message = message
def __repr__(self): def __repr__(self) -> str:
return f"<ParserExit status={self.status} message={self.message}>" return (
f"ParserExit(status={self.status}"
def __str__(self): + (f", message={self.message!r}" if self.message else "")
return self.__repr__() + ")"
)
# Processor Exception # Processor Exception
@@ -68,11 +72,8 @@ class IgnoredException(ProcessException):
def __init__(self, reason: Any): def __init__(self, reason: Any):
self.reason: Any = reason self.reason: Any = reason
def __repr__(self): def __repr__(self) -> str:
return f"<IgnoredException, reason={self.reason}>" return f"IgnoredException(reason={self.reason!r})"
def __str__(self):
return self.__repr__()
class SkippedException(ProcessException): class SkippedException(ProcessException):
@@ -99,11 +100,11 @@ class TypeMisMatch(SkippedException):
self.param: ModelField = param self.param: ModelField = param
self.value: Any = value self.value: Any = value
def __repr__(self): def __repr__(self) -> str:
return f"<TypeMisMatch, param={self.param}, value={self.value}>" return (
f"TypeMisMatch(param={self.param.name}, "
def __str__(self): f"type={self.param._type_display()}, value={self.value!r}>"
self.__repr__() )
class MockApiException(ProcessException): class MockApiException(ProcessException):
@@ -116,11 +117,8 @@ class MockApiException(ProcessException):
def __init__(self, result: Any): def __init__(self, result: Any):
self.result = result self.result = result
def __repr__(self): def __repr__(self) -> str:
return f"<ApiCancelledException, result={self.result}>" return f"MockApiException(result={self.result!r})"
def __str__(self):
return self.__repr__()
class StopPropagation(ProcessException): class StopPropagation(ProcessException):
@@ -195,7 +193,8 @@ class AdapterException(NoneBotException):
adapter_name: 标识 adapter adapter_name: 标识 adapter
""" """
def __init__(self, adapter_name: str) -> None: def __init__(self, adapter_name: str, *args: object) -> None:
super().__init__(*args)
self.adapter_name: str = adapter_name self.adapter_name: str = adapter_name
@@ -231,4 +230,8 @@ class WebSocketClosed(DriverException):
self.reason = reason self.reason = reason
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<WebSocketClosed code={self.code} reason={self.reason}>" return (
f"WebSocketClosed(code={self.code}"
+ (f", reason={self.reason!r}" if self.reason else "")
+ ")"
)

View File

@@ -33,6 +33,9 @@ class Adapter(abc.ABC):
self.bots: Dict[str, Bot] = {} self.bots: Dict[str, Bot] = {}
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例""" """本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
def __repr__(self) -> str:
return f"Adapter(name={self.get_name()!r})"
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def get_name(cls) -> str: def get_name(cls) -> str:
@@ -63,8 +66,9 @@ class Adapter(abc.ABC):
参数: 参数:
bot: {ref}`nonebot.adapters.Bot` 实例 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.driver._bot_disconnect(bot)
self.bots.pop(bot.self_id, None)
def setup_http_server(self, setup: HTTPServerSetup): def setup_http_server(self, setup: HTTPServerSetup):
"""设置一个 HTTP 服务器路由配置""" """设置一个 HTTP 服务器路由配置"""

View File

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

View File

@@ -1,4 +1,5 @@
import abc import abc
from typing import Any, Type, TypeVar
from pydantic import BaseModel from pydantic import BaseModel
@@ -6,6 +7,8 @@ from nonebot.utils import DataclassEncoder
from .message import Message from .message import Message
E = TypeVar("E", bound="Event")
class Event(abc.ABC, BaseModel): class Event(abc.ABC, BaseModel):
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。""" """Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
@@ -14,6 +17,12 @@ class Event(abc.ABC, BaseModel):
extra = "allow" extra = "allow"
json_encoders = {Message: DataclassEncoder} json_encoders = {Message: DataclassEncoder}
@classmethod
def validate(cls: Type["E"], value: Any) -> "E":
if isinstance(value, Event) and not isinstance(value, cls):
raise TypeError(f"{value} is incompatible with Event type {cls}")
return super().validate(value)
@abc.abstractmethod @abc.abstractmethod
def get_type(self) -> str: def get_type(self) -> str:
"""获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。""" """获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。"""
@@ -38,7 +47,7 @@ class Event(abc.ABC, BaseModel):
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
异常: 异常:
NoLogException NoLogException:
""" """
return f"[{self.get_event_name()}]: {self.get_event_description()}" return f"[{self.get_event_name()}]: {self.get_event_description()}"

View File

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

View File

@@ -49,13 +49,16 @@ class MessageTemplate(Formatter, Generic[TF]):
) -> None: ) -> None:
... ...
def __init__( # type:ignore def __init__(
self, template, factory=str self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
) -> None: # TODO: fix type hint here ) -> None:
self.template: TF = template self.template: TF = template # type: ignore
self.factory: Type[TF] = factory self.factory: Type[TF] = factory # type: ignore
self.format_specs: Dict[str, FormatSpecFunc] = {} self.format_specs: Dict[str, FormatSpecFunc] = {}
def __repr__(self) -> str:
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
def add_format_spec( def add_format_spec(
self, spec: FormatSpecFunc_T, name: Optional[str] = None self, spec: FormatSpecFunc_T, name: Optional[str] = None
) -> FormatSpecFunc_T: ) -> FormatSpecFunc_T:
@@ -95,7 +98,7 @@ class MessageTemplate(Formatter, Generic[TF]):
else: else:
raise TypeError("template must be a string or instance of Message!") raise TypeError("template must be a string or instance of Message!")
self.check_unused_args(list(used_args), args, kwargs) self.check_unused_args(used_args, args, kwargs)
return cast(TF, full_message) return cast(TF, full_message)
def vformat( def vformat(
@@ -116,10 +119,9 @@ class MessageTemplate(Formatter, Generic[TF]):
) -> Tuple[TF, int]: ) -> Tuple[TF, int]:
results: List[Any] = [self.factory()] 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 format_string
): ):
# output the literal text # output the literal text
if literal_text: if literal_text:
results.append(literal_text) results.append(literal_text)

View File

@@ -1,6 +1,6 @@
import abc import abc
import asyncio import asyncio
from contextlib import asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from nonebot.log import logger from nonebot.log import logger
@@ -8,8 +8,12 @@ from nonebot.config import Env, Config
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException from nonebot.exception import SkippedException
from nonebot.utils import escape_tag, run_coro_with_catch 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.internal.params import BotParam, DependParam, DefaultParam
from nonebot.typing import (
T_DependencyCache,
T_BotConnectionHook,
T_BotDisconnectionHook,
)
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
@@ -40,12 +44,18 @@ class Driver(abc.ABC):
"""环境名称""" """环境名称"""
self.config: Config = config self.config: Config = config
"""全局配置对象""" """全局配置对象"""
self._clients: Dict[str, "Bot"] = {} self._bots: Dict[str, "Bot"] = {}
def __repr__(self) -> str:
return (
f"Driver(type={self.type!r}, "
f"adapters={len(self._adapters)}, bots={len(self._bots)})"
)
@property @property
def bots(self) -> Dict[str, "Bot"]: def bots(self) -> Dict[str, "Bot"]:
"""获取当前所有已连接的 Bot""" """获取当前所有已连接的 Bot"""
return self._clients return self._bots
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None: def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器 """注册一个协议适配器
@@ -124,18 +134,20 @@ class Driver(abc.ABC):
def _bot_connect(self, bot: "Bot") -> None: def _bot_connect(self, bot: "Bot") -> None:
"""在连接成功后,调用该函数来注册 bot 对象""" """在连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients: if bot.self_id in self._bots:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}") raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot self._bots[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None: async def _run_hook(bot: "Bot") -> None:
coros = list( dependency_cache: T_DependencyCache = {}
map( async with AsyncExitStack() as stack:
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)), if coros := [
self._bot_connection_hook, run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
) )
) for hook in self._bot_connection_hook
if coros: ]:
try: try:
await asyncio.gather(*coros) await asyncio.gather(*coros)
except Exception as e: except Exception as e:
@@ -148,17 +160,19 @@ class Driver(abc.ABC):
def _bot_disconnect(self, bot: "Bot") -> None: def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象""" """在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients: if bot.self_id in self._bots:
del self._clients[bot.self_id] del self._bots[bot.self_id]
async def _run_hook(bot: "Bot") -> None: async def _run_hook(bot: "Bot") -> None:
coros = list( dependency_cache: T_DependencyCache = {}
map( async with AsyncExitStack() as stack:
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)), if coros := [
self._bot_disconnection_hook, run_coro_with_catch(
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
(SkippedException,),
) )
) for hook in self._bot_disconnection_hook
if coros: ]:
try: try:
await asyncio.gather(*coros) await asyncio.gather(*coros)
except Exception as e: except Exception as e:
@@ -233,13 +247,11 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
if not mixins: if not mixins:
return driver return driver
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore def type_(self: ForwardDriver) -> str:
@property
def type(self) -> str:
return ( return (
driver.type.__get__(self) driver.type.__get__(self)
+ "+" + "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins)) + "+".join(map(lambda x: x.type.__get__(self), mixins))
) )
return CombinedDriver return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore

View File

@@ -1,4 +1,5 @@
import abc import abc
import urllib.request
from enum import Enum from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from http.cookiejar import Cookie, CookieJar from http.cookiejar import Cookie, CookieJar
@@ -105,12 +106,9 @@ class Request:
self.url: URL = url self.url: URL = url
# headers # headers
self.headers: CIMultiDict[str] self.headers: CIMultiDict[str] = (
if headers is not None: CIMultiDict(headers) if headers is not None else CIMultiDict()
self.headers = CIMultiDict(headers) )
else:
self.headers = CIMultiDict()
# cookies # cookies
self.cookies = Cookies(cookies) self.cookies = Cookies(cookies)
@@ -131,9 +129,7 @@ class Request:
self.files.append((name, file_info)) # type: ignore self.files.append((name, file_info)) # type: ignore
def __repr__(self) -> str: def __repr__(self) -> str:
class_name = self.__class__.__name__ return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')"
url = str(self.url)
return f"<{class_name}({self.method!r}, {url!r})>"
class Response: class Response:
@@ -149,24 +145,27 @@ class Response:
self.status_code: int = status_code self.status_code: int = status_code
# headers # headers
self.headers: CIMultiDict[str] self.headers: CIMultiDict[str] = (
if headers is not None: CIMultiDict(headers) if headers is not None else CIMultiDict()
self.headers = CIMultiDict(headers) )
else:
self.headers = CIMultiDict()
# body # body
self.content: ContentTypes = content self.content: ContentTypes = content
# request # request
self.request: Optional[Request] = request self.request: Optional[Request] = request
def __repr__(self) -> str:
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
class WebSocket(abc.ABC): class WebSocket(abc.ABC):
def __init__(self, *, request: Request): def __init__(self, *, request: Request):
# request # request
self.request: Request = request self.request: Request = request
def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.request.url!s}')"
@property @property
@abc.abstractmethod @abc.abstractmethod
def closed(self) -> bool: def closed(self) -> bool:
@@ -304,6 +303,11 @@ class Cookies(MutableMapping):
for cookie in cookies.jar: for cookie in cookies.jar:
self.jar.set_cookie(cookie) self.jar.set_cookie(cookie)
def as_header(self, request: Request) -> Dict[str, str]:
urllib_request = self._CookieCompatRequest(request)
self.jar.add_cookie_header(urllib_request)
return urllib_request.added_headers
def __setitem__(self, name: str, value: str) -> None: def __setitem__(self, name: str, value: str) -> None:
return self.set(name, value) return self.set(name, value)
@@ -320,17 +324,28 @@ class Cookies(MutableMapping):
return len(self.jar) return len(self.jar)
def __iter__(self) -> Iterator[Cookie]: def __iter__(self) -> Iterator[Cookie]:
return (cookie for cookie in self.jar) return iter(self.jar)
def __repr__(self) -> str: def __repr__(self) -> str:
cookies_repr = ", ".join( cookies_repr = ", ".join(
[ f"Cookie({cookie.name}={cookie.value} for {cookie.domain})"
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
for cookie in self.jar for cookie in self.jar
]
) )
return f"{self.__class__.__name__}({cookies_repr})"
return f"<Cookies [{cookies_repr}]>" class _CookieCompatRequest(urllib.request.Request):
def __init__(self, request: Request) -> None:
super().__init__(
url=str(request.url),
headers=dict(request.headers),
method=request.method,
)
self.request = request
self.added_headers: Dict[str, str] = {}
def add_unredirected_header(self, key: str, value: str) -> None:
super().add_unredirected_header(key, value)
self.added_headers[key] = value
@dataclass @dataclass

View File

@@ -0,0 +1,11 @@
from .manager import MatcherManager as MatcherManager
from .provider import MatcherProvider as MatcherProvider
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
matchers = MatcherManager()
from .matcher import Matcher as Matcher
from .matcher import current_bot as current_bot
from .matcher import current_event as current_event
from .matcher import current_handler as current_handler
from .matcher import current_matcher as current_matcher

View File

@@ -0,0 +1,104 @@
from typing import (
TYPE_CHECKING,
Any,
List,
Type,
Tuple,
Union,
TypeVar,
Iterator,
KeysView,
Optional,
ItemsView,
ValuesView,
MutableMapping,
overload,
)
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
if TYPE_CHECKING:
from .matcher import Matcher
T = TypeVar("T")
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
"""事件响应器管理器
实现了常用字典操作,用于管理事件响应器。
"""
def __init__(self):
self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({})
def __repr__(self) -> str:
return f"MatcherManager(provider={self.provider!r})"
def __contains__(self, o: object) -> bool:
return o in self.provider
def __iter__(self) -> Iterator[int]:
return iter(self.provider)
def __len__(self) -> int:
return len(self.provider)
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
return self.provider[key]
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
self.provider[key] = value
def __delitem__(self, key: int) -> None:
del self.provider[key]
def __eq__(self, other: Any) -> bool:
return isinstance(other, MatcherManager) and self.provider == other.provider
def keys(self) -> KeysView[int]:
return self.provider.keys()
def values(self) -> ValuesView[List[Type["Matcher"]]]:
return self.provider.values()
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
return self.provider.items()
@overload
def get(self, key: int) -> Optional[List[Type["Matcher"]]]:
...
@overload
def get(self, key: int, default: T) -> Union[List[Type["Matcher"]], T]:
...
def get(
self, key: int, default: Optional[T] = None
) -> Optional[Union[List[Type["Matcher"]], T]]:
return self.provider.get(key, default)
def pop(self, key: int) -> List[Type["Matcher"]]:
return self.provider.pop(key)
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
return self.provider.popitem()
def clear(self) -> None:
self.provider.clear()
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
self.provider.update(__m)
def setdefault(
self, key: int, default: List[Type["Matcher"]]
) -> List[Type["Matcher"]]:
return self.provider.setdefault(key, default)
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
"""设置事件响应器存储器
参数:
provider_class: 事件响应器存储器类
"""
self.provider = provider_class(self.provider)

View File

@@ -1,23 +1,34 @@
from types import ModuleType from types import ModuleType
from datetime import datetime
from contextvars import ContextVar from contextvars import ContextVar
from collections import defaultdict from typing_extensions import Self
from contextlib import AsyncExitStack from datetime import datetime, timedelta
from contextlib import AsyncExitStack, contextmanager
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Dict,
List, List,
Type, Type,
Union, Union,
TypeVar, TypeVar,
Callable, Callable,
ClassVar,
Iterable,
NoReturn, NoReturn,
Optional, Optional,
overload,
) )
from nonebot.log import logger from nonebot.log import logger
from nonebot.internal.rule import Rule
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.internal.permission import User, Permission
from nonebot.internal.adapter import (
Bot,
Event,
Message,
MessageSegment,
MessageTemplate,
)
from nonebot.consts import ( from nonebot.consts import (
ARG_KEY, ARG_KEY,
RECEIVE_KEY, RECEIVE_KEY,
@@ -34,18 +45,13 @@ from nonebot.typing import (
T_PermissionUpdater, T_PermissionUpdater,
) )
from nonebot.exception import ( from nonebot.exception import (
TypeMisMatch,
PausedException, PausedException,
StopPropagation, StopPropagation,
SkippedException, SkippedException,
FinishedException, FinishedException,
RejectedException, RejectedException,
) )
from nonebot.internal.params import (
from .rule import Rule
from .permission import USER, Permission
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
from .params import (
Depends, Depends,
ArgParam, ArgParam,
BotParam, BotParam,
@@ -56,13 +62,13 @@ from .params import (
MatcherParam, MatcherParam,
) )
from . import matchers
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.plugin import Plugin from nonebot.plugin import Plugin
T = TypeVar("T") T = TypeVar("T")
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
"""用于存储当前所有的事件响应器"""
current_bot: ContextVar[Bot] = ContextVar("current_bot") current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event") current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher") current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
@@ -71,68 +77,55 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type): class MatcherMeta(type):
if TYPE_CHECKING: if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str] module_name: Optional[str]
module_prefix: Optional[str]
type: str type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<Matcher from {self.module_name or 'unknown'}, " f"{self.__name__}(type={self.type!r}"
f"type={self.type}, priority={self.priority}, " + (f", module={self.module_name}" if self.module_name else "")
f"temp={self.temp}>" + ")"
) )
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta): 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]] = [] handlers: List[Dependent[Any]] = []
"""事件响应器拥有的事件处理函数列表""" """事件响应器拥有的事件处理函数列表"""
priority: int = 1 priority: ClassVar[int] = 1
"""事件响应器优先级""" """事件响应器优先级"""
block: bool = False 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 = [ HANDLER_PARAM_TYPES = (
DependParam, DependParam,
BotParam, BotParam,
EventParam, EventParam,
@@ -140,7 +133,7 @@ class Matcher(metaclass=MatcherMeta):
ArgParam, ArgParam,
MatcherParam, MatcherParam,
DefaultParam, DefaultParam,
] )
def __init__(self): def __init__(self):
self.handlers = self.handlers.copy() self.handlers = self.handlers.copy()
@@ -148,13 +141,11 @@ class Matcher(metaclass=MatcherMeta):
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, " f"{self.__class__.__name__}(type={self.type!r}"
f"priority={self.priority}, temp={self.temp}>" + (f", module={self.module_name}" if self.module_name else "")
+ ")"
) )
def __str__(self) -> str:
return repr(self)
@classmethod @classmethod
def new( def new(
cls, cls,
@@ -168,13 +159,13 @@ class Matcher(metaclass=MatcherMeta):
*, *,
plugin: Optional["Plugin"] = None, plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None, module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None, expire_time: Optional[Union[datetime, timedelta]] = None,
default_state: Optional[T_State] = None, default_state: Optional[T_State] = None,
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None, default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
default_permission_updater: Optional[ default_permission_updater: Optional[
Union[T_PermissionUpdater, Dependent[Permission]] Union[T_PermissionUpdater, Dependent[Permission]]
] = None, ] = None,
) -> Type["Matcher"]: ) -> Type[Self]:
""" """
创建一个新的事件响应器并存储至 `matchers <#matchers>`_ 创建一个新的事件响应器并存储至 `matchers <#matchers>`_
@@ -195,8 +186,8 @@ class Matcher(metaclass=MatcherMeta):
Type[Matcher]: 新的事件响应器类 Type[Matcher]: 新的事件响应器类
""" """
NewMatcher = type( NewMatcher = type(
"Matcher", cls.__name__,
(Matcher,), (cls,),
{ {
"plugin": plugin, "plugin": plugin,
"module": module, "module": module,
@@ -216,26 +207,38 @@ class Matcher(metaclass=MatcherMeta):
if handlers if handlers
else [], else [],
"temp": temp, "temp": temp,
"expire_time": expire_time, "expire_time": (
expire_time
and (
expire_time
if isinstance(expire_time, datetime)
else datetime.now() + expire_time
)
),
"priority": priority, "priority": priority,
"block": block, "block": block,
"_default_state": default_state or {}, "_default_state": default_state or {},
"_default_type_updater": ( "_default_type_updater": (
default_type_updater
and (
default_type_updater default_type_updater
if isinstance(default_type_updater, Dependent) if isinstance(default_type_updater, Dependent)
else default_type_updater else Dependent[str].parse(
and Dependent[str].parse( call=default_type_updater,
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES allow_types=cls.HANDLER_PARAM_TYPES,
)
) )
), ),
"_default_permission_updater": ( "_default_permission_updater": (
default_permission_updater
and (
default_permission_updater default_permission_updater
if isinstance(default_permission_updater, Dependent) if isinstance(default_permission_updater, Dependent)
else default_permission_updater else Dependent[Permission].parse(
and Dependent[Permission].parse(
call=default_permission_updater, call=default_permission_updater,
allow_types=cls.HANDLER_PARAM_TYPES, allow_types=cls.HANDLER_PARAM_TYPES,
) )
)
), ),
}, },
) )
@@ -246,6 +249,11 @@ class Matcher(metaclass=MatcherMeta):
return NewMatcher return NewMatcher
@classmethod
def destroy(cls) -> None:
"""销毁当前的事件响应器"""
matchers[cls.priority].remove(cls)
@classmethod @classmethod
async def check_perm( async def check_perm(
cls, cls,
@@ -322,7 +330,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod @classmethod
def append_handler( def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
) -> Dependent[Any]: ) -> Dependent[Any]:
handler_ = Dependent[Any].parse( handler_ = Dependent[Any].parse(
call=handler, call=handler,
@@ -334,7 +342,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod @classmethod
def handle( def handle(
cls, parameterless: Optional[List[Any]] = None cls, parameterless: Optional[Iterable[Any]] = None
) -> Callable[[T_Handler], T_Handler]: ) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来向事件响应器直接添加一个处理函数 """装饰一个函数来向事件响应器直接添加一个处理函数
@@ -350,7 +358,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod @classmethod
def receive( def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
) -> Callable[[T_Handler], T_Handler]: ) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 """装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
@@ -368,14 +376,20 @@ class Matcher(metaclass=MatcherMeta):
return return
await matcher.reject() await matcher.reject()
_parameterless = [Depends(_receive), *(parameterless or [])] _parameterless = (Depends(_receive), *(parameterless or tuple()))
def _decorator(func: T_Handler) -> T_Handler: def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func: if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1] func_handler = cls.handlers[-1]
for depend in reversed(_parameterless): new_handler = Dependent(
func_handler.prepend_parameterless(depend) call=func_handler.call,
params=func_handler.params,
parameterless=Dependent.parse_parameterless(
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
)
+ func_handler.parameterless,
)
cls.handlers[-1] = new_handler
else: else:
cls.append_handler(func, parameterless=_parameterless) cls.append_handler(func, parameterless=_parameterless)
@@ -388,7 +402,7 @@ class Matcher(metaclass=MatcherMeta):
cls, cls,
key: str, key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None, prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None, parameterless: Optional[Iterable[Any]] = None,
) -> Callable[[T_Handler], T_Handler]: ) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key` """装饰一个函数来指示 NoneBot 获取一个参数 `key`
@@ -409,17 +423,20 @@ class Matcher(metaclass=MatcherMeta):
return return
await matcher.reject(prompt) await matcher.reject(prompt)
_parameterless = [ _parameterless = (Depends(_key_getter), *(parameterless or tuple()))
Depends(_key_getter),
*(parameterless or []),
]
def _decorator(func: T_Handler) -> T_Handler: def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func: if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1] func_handler = cls.handlers[-1]
for depend in reversed(_parameterless): new_handler = Dependent(
func_handler.prepend_parameterless(depend) call=func_handler.call,
params=func_handler.params,
parameterless=Dependent.parse_parameterless(
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
)
+ func_handler.parameterless,
)
cls.handlers[-1] = new_handler
else: else:
cls.append_handler(func, parameterless=_parameterless) cls.append_handler(func, parameterless=_parameterless)
@@ -547,7 +564,17 @@ class Matcher(metaclass=MatcherMeta):
""" """
raise SkippedException raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]: @overload
def get_receive(self, id: str) -> Union[Event, None]:
...
@overload
def get_receive(self, id: str, default: T) -> Union[Event, T]:
...
def get_receive(
self, id: str, default: Optional[T] = None
) -> Optional[Union[Event, T]]:
"""获取一个 `receive` 事件 """获取一个 `receive` 事件
如果没有找到对应的事件返回 `default` 如果没有找到对应的事件返回 `default`
@@ -559,14 +586,34 @@ class Matcher(metaclass=MatcherMeta):
self.state[RECEIVE_KEY.format(id=id)] = event self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]: @overload
def get_last_receive(self) -> Union[Event, None]:
...
@overload
def get_last_receive(self, default: T) -> Union[Event, T]:
...
def get_last_receive(
self, default: Optional[T] = None
) -> Optional[Union[Event, T]]:
"""获取最近一次 `receive` 事件 """获取最近一次 `receive` 事件
如果没有事件返回 `default` 如果没有事件返回 `default`
""" """
return self.state.get(LAST_RECEIVE_KEY, default) return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]: @overload
def get_arg(self, key: str) -> Union[Message, None]:
...
@overload
def get_arg(self, key: str, default: T) -> Union[Message, T]:
...
def get_arg(
self, key: str, default: Optional[T] = None
) -> Optional[Union[Message, T]]:
"""获取一个 `got` 消息 """获取一个 `got` 消息
如果没有找到对应的消息返回 `default` 如果没有找到对应的消息返回 `default`
@@ -583,24 +630,59 @@ class Matcher(metaclass=MatcherMeta):
else: else:
self.state[REJECT_TARGET] = target self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]: @overload
def get_target(self) -> Union[str, None]:
...
@overload
def get_target(self, default: T) -> Union[str, T]:
...
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
return self.state.get(REJECT_TARGET, default) return self.state.get(REJECT_TARGET, default)
def stop_propagation(self): def stop_propagation(self):
"""阻止事件传播""" """阻止事件传播"""
self.block = True 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 updater = self.__class__._default_type_updater
if not updater: return (
return "message" await updater(
return await updater(bot=bot, event=event, state=self.state, matcher=self) 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(
updater = self.__class__._default_permission_updater self,
if not updater: bot: Bot,
return USER(event.get_session_id(), perm=self.permission) event: Event,
return await updater(bot=bot, event=event, state=self.state, matcher=self) 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,
stack=stack,
dependency_cache=dependency_cache,
)
return Permission(User.from_event(event, perm=self.permission))
async def resolve_reject(self): async def resolve_reject(self):
handler = current_handler.get() handler = current_handler.get()
@@ -608,6 +690,18 @@ class Matcher(metaclass=MatcherMeta):
if REJECT_CACHE_TARGET in self.state: if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET] self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
@contextmanager
def ensure_context(self, bot: Bot, event: Event):
b_t = current_bot.set(bot)
e_t = current_event.set(event)
m_t = current_matcher.set(self)
try:
yield
finally:
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
async def simple_run( async def simple_run(
self, self,
bot: Bot, bot: Bot,
@@ -617,12 +711,11 @@ class Matcher(metaclass=MatcherMeta):
dependency_cache: Optional[T_DependencyCache] = None, dependency_cache: Optional[T_DependencyCache] = None,
): ):
logger.trace( logger.trace(
f"Matcher {self} run with incoming args: " f"{self} run with incoming args: "
f"bot={bot}, event={event}, state={state}" f"bot={bot}, event={event!r}, state={state!r}"
) )
b_t = current_bot.set(bot)
e_t = current_event.set(event) with self.ensure_context(bot, event):
m_t = current_matcher.set(self)
try: try:
# Refresh preprocess state # Refresh preprocess state
self.state.update(state) self.state.update(state)
@@ -640,20 +733,12 @@ class Matcher(metaclass=MatcherMeta):
stack=stack, stack=stack,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
) )
except TypeMisMatch as e: except SkippedException:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
logger.debug(f"Handler {handler} skipped") logger.debug(f"Handler {handler} skipped")
except StopPropagation: except StopPropagation:
self.block = True self.block = True
finally: finally:
logger.info(f"Matcher {self} running complete") logger.info(f"{self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
# 运行handlers # 运行handlers
async def run( async def run(
@@ -669,10 +754,12 @@ class Matcher(metaclass=MatcherMeta):
except RejectedException: except RejectedException:
await self.resolve_reject() await self.resolve_reject()
type_ = await self.update_type(bot, event) type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(bot, event) permission = await self.update_permission(
bot, event, stack, dependency_cache
)
Matcher.new( self.new(
type_, type_,
Rule(), Rule(),
permission, permission,
@@ -682,16 +769,18 @@ class Matcher(metaclass=MatcherMeta):
block=True, block=True,
plugin=self.plugin, plugin=self.plugin,
module=self.module, module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout, expire_time=bot.config.session_expire_timeout,
default_state=self.state, default_state=self.state,
default_type_updater=self.__class__._default_type_updater, default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater, default_permission_updater=self.__class__._default_permission_updater,
) )
except PausedException: except PausedException:
type_ = await self.update_type(bot, event) type_ = await self.update_type(bot, event, stack, dependency_cache)
permission = await self.update_permission(bot, event) permission = await self.update_permission(
bot, event, stack, dependency_cache
)
Matcher.new( self.new(
type_, type_,
Rule(), Rule(),
permission, permission,
@@ -701,21 +790,10 @@ class Matcher(metaclass=MatcherMeta):
block=True, block=True,
plugin=self.plugin, plugin=self.plugin,
module=self.module, module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout, expire_time=bot.config.session_expire_timeout,
default_state=self.state, default_state=self.state,
default_type_updater=self.__class__._default_type_updater, default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater, default_permission_updater=self.__class__._default_permission_updater,
) )
except FinishedException: except FinishedException:
pass pass
__autodoc__ = {
"MatcherMeta": False,
"Matcher.get_target": False,
"Matcher.set_target": False,
"Matcher.update_type": False,
"Matcher.update_permission": False,
"Matcher.resolve_reject": False,
"Matcher.simple_run": False,
}

View File

@@ -0,0 +1,27 @@
import abc
from collections import defaultdict
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
if TYPE_CHECKING:
from .matcher import Matcher
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
"""事件响应器存储器基类
参数:
matchers: 当前存储器中已有的事件响应器
"""
@abc.abstractmethod
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
raise NotImplementedError
class _DictProvider(defaultdict, MatcherProvider):
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
super().__init__(list, matchers)
DEFAULT_PROVIDER_CLASS = _DictProvider
"""默认存储器类型"""

View File

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

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
from typing_extensions import Self
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine from typing import Set, Tuple, Union, NoReturn, Optional
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.utils import run_coro_with_catch from nonebot.utils import run_coro_with_catch
@@ -37,16 +38,19 @@ class Permission:
] ]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None: def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set( self.checkers: Set[Dependent[bool]] = {
checker checker
if isinstance(checker, Dependent) if isinstance(checker, Dependent)
else Dependent[bool].parse( else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES call=checker, allow_types=self.HANDLER_PARAM_TYPES
) )
for checker in checkers for checker in checkers
) }
"""存储 `PermissionChecker`""" """存储 `PermissionChecker`"""
def __repr__(self) -> str:
return f"Permission({', '.join(repr(checker) for checker in self.checkers)})"
async def __call__( async def __call__(
self, self,
bot: Bot, bot: Bot,
@@ -54,7 +58,7 @@ class Permission:
stack: Optional[AsyncExitStack] = None, stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None, dependency_cache: Optional[T_DependencyCache] = None,
) -> bool: ) -> bool:
"""检查是否满足某个权限 """检查是否满足某个权限
参数: 参数:
bot: Bot 对象 bot: Bot 对象
@@ -106,7 +110,7 @@ class Permission:
class User: class User:
"""检查当前事件是否属于指定会话 """检查当前事件是否属于指定会话
参数: 参数:
users: 会话 ID 元组 users: 会话 ID 元组
@@ -121,19 +125,63 @@ class User:
self.users = users self.users = users
self.perm = perm self.perm = perm
async def __call__(self, bot: Bot, event: Event) -> bool: def __repr__(self) -> str:
return bool( return (
event.get_session_id() in self.users f"User(users={self.users}"
and (self.perm is None or await self.perm(bot, event)) + (f", permission={self.perm})" if self.perm else "")
+ ")"
) )
async def __call__(self, bot: Bot, event: Event) -> bool:
try:
session = event.get_session_id()
except Exception:
return False
return bool(
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): def USER(*users: str, perm: Optional[Permission] = None):
"""匹配当前事件属于指定会话 """匹配当前事件属于指定会话
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
参数: 参数:
user: 会话白名单 user: 会话白名单
perm: 需要同时满足的权限 perm: 需要同时满足的权限
""" """
return Permission(User(users, perm)) return Permission(User.from_permission(*users, perm=perm))

View File

@@ -37,16 +37,19 @@ class Rule:
] ]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None: def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set( self.checkers: Set[Dependent[bool]] = {
checker checker
if isinstance(checker, Dependent) if isinstance(checker, Dependent)
else Dependent[bool].parse( else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES call=checker, allow_types=self.HANDLER_PARAM_TYPES
) )
for checker in checkers for checker in checkers
) }
"""存储 `RuleChecker`""" """存储 `RuleChecker`"""
def __repr__(self) -> str:
return f"Rule({', '.join(repr(checker) for checker in self.checkers)})"
async def __call__( async def __call__(
self, self,
bot: Bot, bot: Bot,

View File

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

View File

@@ -9,10 +9,16 @@ from nonebot.internal.matcher import Matcher as Matcher
from nonebot.internal.matcher import matchers as matchers from nonebot.internal.matcher import matchers as matchers
from nonebot.internal.matcher import current_bot as current_bot from nonebot.internal.matcher import current_bot as current_bot
from nonebot.internal.matcher import current_event as current_event from nonebot.internal.matcher import current_event as current_event
from nonebot.internal.matcher import MatcherManager as MatcherManager
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
from nonebot.internal.matcher import current_handler as current_handler from nonebot.internal.matcher import current_handler as current_handler
from nonebot.internal.matcher import current_matcher as current_matcher from nonebot.internal.matcher import current_matcher as current_matcher
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
__autodoc__ = { __autodoc__ = {
"Matcher": True, "Matcher": True,
"matchers": True, "matchers": True,
"MatcherManager": True,
"MatcherProvider": True,
"DEFAULT_PROVIDER_CLASS": True,
} }

View File

@@ -8,9 +8,10 @@ FrontMatter:
""" """
import asyncio import asyncio
import contextlib
from datetime import datetime from datetime import datetime
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
from nonebot.log import logger from nonebot.log import logger
from nonebot.rule import TrieRule from nonebot.rule import TrieRule
@@ -50,14 +51,14 @@ _event_postprocessors: Set[Dependent[Any]] = set()
_run_preprocessors: Set[Dependent[Any]] = set() _run_preprocessors: Set[Dependent[Any]] = set()
_run_postprocessors: Set[Dependent[Any]] = set() _run_postprocessors: Set[Dependent[Any]] = set()
EVENT_PCS_PARAMS = [ EVENT_PCS_PARAMS = (
DependParam, DependParam,
BotParam, BotParam,
EventParam, EventParam,
StateParam, StateParam,
DefaultParam, DefaultParam,
] )
RUN_PREPCS_PARAMS = [ RUN_PREPCS_PARAMS = (
DependParam, DependParam,
BotParam, BotParam,
EventParam, EventParam,
@@ -65,8 +66,8 @@ RUN_PREPCS_PARAMS = [
ArgParam, ArgParam,
MatcherParam, MatcherParam,
DefaultParam, DefaultParam,
] )
RUN_POSTPCS_PARAMS = [ RUN_POSTPCS_PARAMS = (
DependParam, DependParam,
ExceptionParam, ExceptionParam,
BotParam, BotParam,
@@ -75,7 +76,7 @@ RUN_POSTPCS_PARAMS = [
ArgParam, ArgParam,
MatcherParam, MatcherParam,
DefaultParam, DefaultParam,
] )
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor: def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
@@ -111,7 +112,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
async def _check_matcher( async def _check_matcher(
priority: int,
Matcher: Type[Matcher], Matcher: Type[Matcher],
bot: "Bot", bot: "Bot",
event: "Event", event: "Event",
@@ -120,10 +120,8 @@ async def _check_matcher(
dependency_cache: Optional[T_DependencyCache] = None, dependency_cache: Optional[T_DependencyCache] = None,
) -> None: ) -> None:
if Matcher.expire_time and datetime.now() > Matcher.expire_time: if Matcher.expire_time and datetime.now() > Matcher.expire_time:
try: with contextlib.suppress(Exception):
matchers[priority].remove(Matcher) Matcher.destroy()
except Exception:
pass
return return
try: try:
@@ -138,11 +136,8 @@ async def _check_matcher(
return return
if Matcher.temp: if Matcher.temp:
try: with contextlib.suppress(Exception):
matchers[priority].remove(Matcher) Matcher.destroy()
except Exception:
pass
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache) await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
@@ -157,11 +152,9 @@ async def _run_matcher(
logger.info(f"Event will be handled by {Matcher}") logger.info(f"Event will be handled by {Matcher}")
matcher = Matcher() matcher = Matcher()
if coros := [
coros = list( run_coro_with_catch(
map( proc(
lambda x: run_coro_with_catch(
x(
matcher=matcher, matcher=matcher,
bot=bot, bot=bot,
event=event, event=event,
@@ -170,54 +163,51 @@ async def _run_matcher(
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
), ),
(SkippedException,), (SkippedException,),
),
_run_preprocessors,
) )
) for proc in _run_preprocessors
if coros: ]:
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try: try:
await asyncio.gather(*coros) await asyncio.gather(*coros)
except IgnoredException: except IgnoredException:
logger.opt(colors=True).info( logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
f"Matcher {matcher} running is <b>cancelled</b>"
)
return return
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running RunPreProcessors. " "<r><bg #f8bbd0>Error when running RunPreProcessors. Running cancelled!</bg #f8bbd0></r>"
"Running cancelled!</bg #f8bbd0></r>"
) )
return return
exception = None exception = None
try: try:
logger.debug(f"Running matcher {matcher}") logger.debug(f"Running {matcher}")
await matcher.run(bot, event, state, stack, dependency_cache) await matcher.run(bot, event, state, stack, dependency_cache)
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>" f"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>"
) )
exception = e exception = e
coros = list( if coros := [
map( run_coro_with_catch(
lambda x: run_coro_with_catch( proc(
x(
matcher=matcher, matcher=matcher,
exception=exception, exception=exception,
bot=bot, bot=bot,
event=event, event=event,
state=state, state=matcher.state,
stack=stack, stack=stack,
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
), ),
(SkippedException,), (SkippedException,),
),
_run_postprocessors,
) )
) for proc in _run_postprocessors
if coros: ]:
# ensure matcher function can be correctly called
with matcher.ensure_context(bot, event):
try: try:
await asyncio.gather(*coros) await asyncio.gather(*coros)
except Exception as e: except Exception as e:
@@ -244,7 +234,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
``` ```
""" """
show_log = True show_log = True
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | " log_msg = f"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | "
try: try:
log_msg += event.get_log_string() log_msg += event.get_log_string()
except NoLogException: except NoLogException:
@@ -256,10 +246,9 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
dependency_cache: T_DependencyCache = {} dependency_cache: T_DependencyCache = {}
async with AsyncExitStack() as stack: async with AsyncExitStack() as stack:
coros = list( if coros := [
map( run_coro_with_catch(
lambda x: run_coro_with_catch( proc(
x(
bot=bot, bot=bot,
event=event, event=event,
state=state, state=state,
@@ -267,11 +256,9 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
), ),
(SkippedException,), (SkippedException,),
),
_event_preprocessors,
) )
) for proc in _event_preprocessors
if coros: ]:
try: try:
if show_log: if show_log:
logger.debug("Running PreProcessors...") logger.debug("Running PreProcessors...")
@@ -306,7 +293,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
pending_tasks = [ pending_tasks = [
_check_matcher( _check_matcher(
priority, matcher, bot, event, state.copy(), stack, dependency_cache matcher, bot, event, state.copy(), stack, dependency_cache
) )
for matcher in matchers[priority] for matcher in matchers[priority]
] ]
@@ -324,10 +311,12 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>" "<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
) )
coros = list( if show_log:
map( logger.debug("Checking for matchers completed")
lambda x: run_coro_with_catch(
x( if coros := [
run_coro_with_catch(
proc(
bot=bot, bot=bot,
event=event, event=event,
state=state, state=state,
@@ -335,11 +324,9 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
dependency_cache=dependency_cache, dependency_cache=dependency_cache,
), ),
(SkippedException,), (SkippedException,),
),
_event_postprocessors,
) )
) for proc in _event_postprocessors
if coros: ]:
try: try:
if show_log: if show_log:
logger.debug("Running PostProcessors...") logger.debug("Running PostProcessors...")

View File

@@ -5,17 +5,17 @@ FrontMatter:
description: nonebot.params 模块 description: nonebot.params 模块
""" """
from typing import Any, Dict, List, Tuple, Optional import warnings
from typing import Any, Dict, List, Tuple, Union, Optional
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.adapters import Event, Message
from nonebot.internal.params import Arg as Arg from nonebot.internal.params import Arg as Arg
from nonebot.internal.params import State as State
from nonebot.internal.params import ArgStr as ArgStr from nonebot.internal.params import ArgStr as ArgStr
from nonebot.internal.params import Depends as Depends from nonebot.internal.params import Depends as Depends
from nonebot.internal.params import ArgParam as ArgParam from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import BotParam as BotParam from nonebot.internal.params import BotParam as BotParam
from nonebot.adapters import Event, Message, MessageSegment
from nonebot.internal.params import EventParam as EventParam from nonebot.internal.params import EventParam as EventParam
from nonebot.internal.params import StateParam as StateParam from nonebot.internal.params import StateParam as StateParam
from nonebot.internal.params import DependParam as DependParam from nonebot.internal.params import DependParam as DependParam
@@ -25,15 +25,21 @@ from nonebot.internal.params import MatcherParam as MatcherParam
from nonebot.internal.params import ExceptionParam as ExceptionParam from nonebot.internal.params import ExceptionParam as ExceptionParam
from nonebot.consts import ( from nonebot.consts import (
CMD_KEY, CMD_KEY,
REGEX_STR,
PREFIX_KEY, PREFIX_KEY,
REGEX_DICT, REGEX_DICT,
SHELL_ARGS, SHELL_ARGS,
SHELL_ARGV, SHELL_ARGV,
CMD_ARG_KEY, CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY, RAW_CMD_KEY,
REGEX_GROUP, REGEX_GROUP,
ENDSWITH_KEY,
CMD_START_KEY, CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED, REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
) )
@@ -109,16 +115,25 @@ def CommandStart() -> str:
return Depends(_command_start) 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: def _shell_command_args(state: T_State) -> Any:
return state[SHELL_ARGS] return state[SHELL_ARGS] # Namespace or ParserExit
def ShellCommandArgs(): def ShellCommandArgs() -> Any:
"""shell 命令解析后的参数字典""" """shell 命令解析后的参数字典"""
return Depends(_shell_command_args, use_cache=False) return Depends(_shell_command_args, use_cache=False)
def _shell_command_argv(state: T_State) -> List[str]: def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
return state[SHELL_ARGV] return state[SHELL_ARGV]
@@ -133,10 +148,25 @@ def _regex_matched(state: T_State) -> str:
def RegexMatched() -> str: def RegexMatched() -> 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) return Depends(_regex_matched, use_cache=False)
def _regex_group(state: T_State): def _regex_str(state: T_State) -> str:
return state[REGEX_STR]
def RegexStr() -> str:
"""正则匹配结果文本"""
return Depends(_regex_str, use_cache=False)
def _regex_group(state: T_State) -> Tuple[Any, ...]:
return state[REGEX_GROUP] return state[REGEX_GROUP]
@@ -145,7 +175,7 @@ def RegexGroup() -> Tuple[Any, ...]:
return Depends(_regex_group, use_cache=False) return Depends(_regex_group, use_cache=False)
def _regex_dict(state: T_State): def _regex_dict(state: T_State) -> Dict[str, Any]:
return state[REGEX_DICT] return state[REGEX_DICT]
@@ -154,10 +184,46 @@ def RegexDict() -> Dict[str, Any]:
return Depends(_regex_dict, use_cache=False) return Depends(_regex_dict, use_cache=False)
def _startswith(state: T_State) -> str:
return state[STARTSWITH_KEY]
def Startswith() -> str:
"""响应触发前缀"""
return Depends(_startswith, use_cache=False)
def _endswith(state: T_State) -> str:
return state[ENDSWITH_KEY]
def Endswith() -> str:
"""响应触发后缀"""
return Depends(_endswith, use_cache=False)
def _fullmatch(state: T_State) -> str:
return state[FULLMATCH_KEY]
def Fullmatch() -> str:
"""响应触发完整消息"""
return Depends(_fullmatch, use_cache=False)
def _keyword(state: T_State) -> str:
return state[KEYWORD_KEY]
def Keyword() -> str:
"""响应触发关键字"""
return Depends(_keyword, use_cache=False)
def Received(id: Optional[str] = None, default: Any = None) -> Any: def Received(id: Optional[str] = None, default: Any = None) -> Any:
"""`receive` 事件参数""" """`receive` 事件参数"""
def _received(matcher: "Matcher"): def _received(matcher: "Matcher") -> Any:
return matcher.get_receive(id or "", default) return matcher.get_receive(id or "", default)
return Depends(_received, use_cache=False) return Depends(_received, use_cache=False)
@@ -174,7 +240,6 @@ def LastReceived(default: Any = None) -> Any:
__autodoc__ = { __autodoc__ = {
"Arg": True, "Arg": True,
"State": True,
"ArgStr": True, "ArgStr": True,
"Depends": True, "Depends": True,
"ArgParam": True, "ArgParam": True,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,12 @@ FrontMatter:
description: nonebot.plugin.on 模块 description: nonebot.plugin.on 模块
""" """
import re import re
import sys
import inspect import inspect
from types import ModuleType from types import ModuleType
from datetime import datetime, timedelta
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.permission import Permission from nonebot.permission import Permission
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
@@ -19,6 +20,7 @@ from nonebot.rule import (
ArgumentParser, ArgumentParser,
regex, regex,
command, command,
is_type,
keyword, keyword,
endswith, endswith,
fullmatch, fullmatch,
@@ -26,23 +28,49 @@ from nonebot.rule import (
shell_command, shell_command,
) )
from .manager import _current_plugin from .plugin import Plugin
from . import get_plugin_by_module_name
from .manager import _current_plugin_chain
def _store_matcher(matcher: Type[Matcher]) -> None: def store_matcher(matcher: Type[Matcher]) -> None:
plugin = _current_plugin.get() """存储一个事件响应器到插件。
# only store the matcher defined in the plugin
if plugin: 参数:
plugin.matcher.add(matcher) 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() current_frame = inspect.currentframe()
if current_frame is None: if current_frame is None:
return None return None
frame = inspect.getouterframes(current_frame)[depth + 1].frame frame = inspect.getouterframes(current_frame)[depth + 1].frame
module_name = frame.f_globals["__name__"] return inspect.getmodule(frame)
return sys.modules.get(module_name)
def on( def on(
@@ -52,13 +80,13 @@ def on(
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None, handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False, temp: bool = False,
expire_time: Optional[Union[datetime, timedelta]] = None,
priority: int = 1, priority: int = 1,
block: bool = False, block: bool = False,
state: Optional[T_State] = None, state: Optional[T_State] = None,
_depth: int = 0, _depth: int = 0,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个基础事件响应器,可自定义类型。
注册一个基础事件响应器,可自定义类型。
参数: 参数:
type: 事件响应器类型 type: 事件响应器类型
@@ -66,6 +94,7 @@ def on(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -75,165 +104,81 @@ def on(
Rule() & rule, Rule() & rule,
Permission() | permission, Permission() | permission,
temp=temp, temp=temp,
expire_time=expire_time,
priority=priority, priority=priority,
block=block, block=block,
handlers=handlers, handlers=handlers,
plugin=_current_plugin.get(), plugin=get_matcher_plugin(_depth + 1),
module=_get_matcher_module(_depth + 1), module=get_matcher_module(_depth + 1),
default_state=state, default_state=state,
) )
_store_matcher(matcher) store_matcher(matcher)
return matcher return matcher
def on_metaevent( def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
rule: Optional[Union[Rule, T_RuleChecker]] = None, """注册一个元事件响应器。
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个元事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
matcher = Matcher.new(
"meta_event",
Rule() & rule,
Permission(),
temp=temp,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
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,
priority: int = 1,
block: bool = True,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个消息事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
matcher = Matcher.new( return on("meta_event", *args, **kwargs, _depth=_depth + 1)
"message",
Rule() & rule,
Permission() | permission,
temp=temp,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
def on_notice( def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
rule: Optional[Union[Rule, T_RuleChecker]] = None, """注册一个消息事件响应器。
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个通知事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
matcher = Matcher.new( kwargs.setdefault("block", True)
"notice", return on("message", *args, **kwargs, _depth=_depth + 1)
Rule() & rule,
Permission(),
temp=temp,
priority=priority,
block=block,
handlers=handlers,
plugin=_current_plugin.get(),
module=_get_matcher_module(_depth + 1),
default_state=state,
)
_store_matcher(matcher)
return matcher
def on_request( def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
rule: Optional[Union[Rule, T_RuleChecker]] = None, """注册一个通知事件响应器。
*,
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
_depth: int = 0,
) -> Type[Matcher]:
"""
注册一个请求事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
matcher = Matcher.new( return on("notice", *args, **kwargs, _depth=_depth + 1)
"request",
Rule() & rule,
Permission(), def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
temp=temp, """注册一个请求事件响应器。
priority=priority,
block=block, 参数:
handlers=handlers, rule: 事件响应规则
plugin=_current_plugin.get(), permission: 事件响应权限
module=_get_matcher_module(_depth + 1), handlers: 事件处理函数列表
default_state=state, temp: 是否为临时事件响应器(仅执行一次)
) expire_time: 事件响应器最终有效时间点,过时即被删除
_store_matcher(matcher) priority: 事件响应器优先级
return matcher block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
return on("request", *args, **kwargs, _depth=_depth + 1)
def on_startswith( def on_startswith(
@@ -243,8 +188,7 @@ def on_startswith(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数: 参数:
msg: 指定消息开头内容 msg: 指定消息开头内容
@@ -253,6 +197,7 @@ def on_startswith(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -267,8 +212,7 @@ def on_endswith(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数: 参数:
msg: 指定消息结尾内容 msg: 指定消息结尾内容
@@ -277,6 +221,7 @@ def on_endswith(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -291,8 +236,7 @@ def on_fullmatch(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数: 参数:
msg: 指定消息全匹配内容 msg: 指定消息全匹配内容
@@ -301,6 +245,7 @@ def on_fullmatch(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -314,8 +259,7 @@ def on_keyword(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数: 参数:
keywords: 关键词列表 keywords: 关键词列表
@@ -323,6 +267,7 @@ def on_keyword(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -334,11 +279,11 @@ def on_command(
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None, rule: Optional[Union[Rule, T_RuleChecker]] = None,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None, aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息以指定命令开头时响应。
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_ 命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
@@ -346,18 +291,22 @@ def on_command(
cmd: 指定命令内容 cmd: 指定命令内容
rule: 事件响应规则 rule: 事件响应规则
aliases: 命令别名 aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
commands = set([cmd]) | (aliases or set()) commands = {cmd} | (aliases or set())
block = kwargs.pop("block", False) kwargs.setdefault("block", False)
return on_message( return on_message(
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1 command(*commands, force_whitespace=force_whitespace) & rule,
**kwargs,
_depth=_depth + 1,
) )
@@ -369,8 +318,7 @@ def on_shell_command(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -384,12 +332,13 @@ def on_shell_command(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
commands = set([cmd]) | (aliases or set()) commands = {cmd} | (aliases or set())
return on_message( return on_message(
shell_command(*commands, parser=parser) & rule, shell_command(*commands, parser=parser) & rule,
**kwargs, **kwargs,
@@ -404,8 +353,7 @@ def on_regex(
_depth: int = 0, _depth: int = 0,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_ 命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -416,6 +364,7 @@ def on_regex(
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
@@ -423,78 +372,135 @@ def on_regex(
return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1) return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)
class CommandGroup: def on_type(
"""命令组,用于声明一组有相同名称前缀的命令。""" types: Union[Type[Event], Tuple[Type[Event], ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
*,
_depth: int = 0,
**kwargs,
) -> Type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:
types: 事件类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
event_types = types if isinstance(types, tuple) else (types,)
return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1)
class _Group:
def __init__(self, **kwargs):
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
self.matchers: List[Type[Matcher]] = []
"""组内事件响应器列表"""
self.base_kwargs: Dict[str, Any] = kwargs
"""其他传递给 `on` 的参数默认值"""
def _get_final_kwargs(
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
) -> Dict[str, Any]:
"""获取最终传递给 `on` 的参数
参数:
update: 更新的关键字参数
exclude: 需要排除的参数
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(update)
if exclude:
for key in exclude:
final_kwargs.pop(key, None)
final_kwargs["_depth"] = 1
return final_kwargs
class CommandGroup(_Group):
"""命令组,用于声明一组有相同名称前缀的命令。
参数:
cmd: 指定命令内容
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs): def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
""" """命令前缀"""
参数: super().__init__(**kwargs)
cmd: 命令前缀
**kwargs: `on_command` 的参数默认值,参考 `on_command <#on-command-cmd-rule-none-aliases-none-kwargs>`_
"""
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
""" self.base_kwargs.pop("aliases", None)
命令前缀
""" def __repr__(self) -> str:
if "aliases" in kwargs: return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
del kwargs["aliases"]
self.base_kwargs: Dict[str, Any] = kwargs
"""
其他传递给 `on_command` 的参数默认值
"""
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]: def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
""" """注册一个新的命令。新参数将会覆盖命令组默认值
注册一个新的命令。
参数: 参数:
cmd: 命令前缀 cmd: 指定命令内容
**kwargs: `on_command` 的参数,将会覆盖命令组默认值 aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
""" """
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd cmd = self.basecmd + sub_cmd
matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
final_kwargs = self.base_kwargs.copy() self.matchers.append(matcher)
final_kwargs.update(kwargs) return matcher
return on_command(cmd, **final_kwargs, _depth=1)
def shell_command( def shell_command(
self, cmd: Union[str, Tuple[str, ...]], **kwargs self, cmd: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
注册一个新的命令。
参数: 参数:
cmd: 命令前缀 cmd: 指定命令内容
**kwargs: `on_shell_command` 的参数,将会覆盖命令组默认值 rule: 事件响应规则
aliases: 命令别名
parser: `nonebot.rule.ArgumentParser` 对象
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
""" """
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
cmd = self.basecmd + sub_cmd cmd = self.basecmd + sub_cmd
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
final_kwargs = self.base_kwargs.copy() self.matchers.append(matcher)
final_kwargs.update(kwargs) return matcher
return on_shell_command(cmd, **final_kwargs, _depth=1)
class MatcherGroup: class MatcherGroup(_Group):
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。""" """事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
def __init__(self, **kwargs): def __repr__(self) -> str:
""" return f"MatcherGroup(matchers={len(self.matchers)})"
创建一个事件响应器组合,参数为默认值,与 `on` 一致
"""
self.matchers: List[Type[Matcher]] = []
"""
组内事件响应器列表
"""
self.base_kwargs: Dict[str, Any] = kwargs
"""
其他传递给 `on` 的参数默认值
"""
def on(self, **kwargs) -> Type[Matcher]: def on(self, **kwargs) -> Type[Matcher]:
""" """注册一个基础事件响应器,可自定义类型。
注册一个基础事件响应器,可自定义类型。
参数: 参数:
type: 事件响应器类型 type: 事件响应器类型
@@ -502,99 +508,91 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() matcher = on(**self._get_final_kwargs(kwargs))
final_kwargs.update(kwargs)
matcher = on(**final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_metaevent(self, **kwargs) -> Type[Matcher]: def on_metaevent(self, **kwargs) -> Type[Matcher]:
""" """注册一个元事件响应器。
注册一个元事件响应器。
参数:
rule: 事件响应规则
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
final_kwargs.pop("permission", None)
matcher = on_metaevent(**final_kwargs, _depth=1)
self.matchers.append(matcher)
return matcher
def on_message(self, **kwargs) -> Type[Matcher]:
"""
注册一个消息事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
final_kwargs.update(kwargs) matcher = on_metaevent(**final_kwargs)
final_kwargs.pop("type", None) self.matchers.append(matcher)
matcher = on_message(**final_kwargs, _depth=1) return matcher
def on_message(self, **kwargs) -> Type[Matcher]:
"""注册一个消息事件响应器。
参数:
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_message(**final_kwargs)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_notice(self, **kwargs) -> Type[Matcher]: def on_notice(self, **kwargs) -> Type[Matcher]:
""" """注册一个通知事件响应器。
注册一个通知事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
final_kwargs.update(kwargs) matcher = on_notice(**final_kwargs)
final_kwargs.pop("type", None)
matcher = on_notice(**final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_request(self, **kwargs) -> Type[Matcher]: def on_request(self, **kwargs) -> Type[Matcher]:
""" """注册一个请求事件响应器。
注册一个请求事件响应器。
参数: 参数:
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
final_kwargs.update(kwargs) matcher = on_request(**final_kwargs)
final_kwargs.pop("type", None)
matcher = on_request(**final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_startswith( def on_startswith(
self, msg: Union[str, Tuple[str, ...]], **kwargs self, msg: Union[str, Tuple[str, ...]], **kwargs
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
参数: 参数:
msg: 指定消息开头内容 msg: 指定消息开头内容
@@ -603,20 +601,18 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_startswith(msg, **final_kwargs)
final_kwargs.pop("type", None)
matcher = on_startswith(msg, **final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]: def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
参数: 参数:
msg: 指定消息结尾内容 msg: 指定消息结尾内容
@@ -625,20 +621,18 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_endswith(msg, **final_kwargs)
final_kwargs.pop("type", None)
matcher = on_endswith(msg, **final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]: def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
参数: 参数:
msg: 指定消息全匹配内容 msg: 指定消息全匹配内容
@@ -647,20 +641,18 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_fullmatch(msg, **final_kwargs)
final_kwargs.pop("type", None)
matcher = on_fullmatch(msg, **final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]: def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
参数: 参数:
keywords: 关键词列表 keywords: 关键词列表
@@ -668,14 +660,13 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_keyword(keywords, **final_kwargs)
final_kwargs.pop("type", None)
matcher = on_keyword(keywords, **final_kwargs, _depth=1)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
@@ -683,28 +674,30 @@ class MatcherGroup:
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None, aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
force_whitespace: Optional[Union[str, bool]] = None,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息以指定命令开头时响应。
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_ 命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
参数: 参数:
cmd: 指定命令内容 cmd: 指定命令内容
aliases: 命令别名 aliases: 命令别名
force_whitespace: 是否强制命令后必须有指定空白符
rule: 事件响应规则 rule: 事件响应规则
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_command(
final_kwargs.pop("type", None) cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs
matcher = on_command(cmd, aliases=aliases, **final_kwargs, _depth=1) )
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
@@ -715,8 +708,7 @@ class MatcherGroup:
parser: Optional[ArgumentParser] = None, parser: Optional[ArgumentParser] = None,
**kwargs, **kwargs,
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
@@ -730,24 +722,20 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs)
final_kwargs.pop("type", None)
matcher = on_shell_command(
cmd, aliases=aliases, parser=parser, **final_kwargs, _depth=1
)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher
def on_regex( def on_regex(
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
) -> Type[Matcher]: ) -> Type[Matcher]:
""" """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_ 命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
@@ -758,13 +746,33 @@ class MatcherGroup:
permission: 事件响应权限 permission: 事件响应权限
handlers: 事件处理函数列表 handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次) temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级 priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递 block: 是否阻止事件向更低优先级传递
state: 默认 state state: 默认 state
""" """
final_kwargs = self.base_kwargs.copy() final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
final_kwargs.update(kwargs) matcher = on_regex(pattern, flags=flags, **final_kwargs)
final_kwargs.pop("type", None) self.matchers.append(matcher)
matcher = on_regex(pattern, flags=flags, **final_kwargs, _depth=1) return matcher
def on_type(
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
) -> Type[Matcher]:
"""注册一个事件响应器,并且当事件为指定类型时响应。
参数:
types: 事件类型
rule: 事件响应规则
permission: 事件响应权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器(仅执行一次)
expire_time: 事件响应器最终有效时间点,过时即被删除
priority: 事件响应器优先级
block: 是否阻止事件向更低优先级传递
state: 默认 state
"""
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
matcher = on_type(types, **final_kwargs)
self.matchers.append(matcher) self.matchers.append(matcher)
return matcher return matcher

View File

@@ -1,12 +1,20 @@
import re import re
from types import ModuleType
from datetime import datetime, timedelta
from typing import Set, List, Type, Tuple, Union, Optional from typing import Set, List, Type, Tuple, Union, Optional
from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.permission import Permission from nonebot.permission import Permission
from nonebot.dependencies import Dependent from nonebot.dependencies import Dependent
from nonebot.rule import Rule, ArgumentParser from nonebot.rule import Rule, ArgumentParser
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker 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( def on(
type: str = "", type: str = "",
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
@@ -14,15 +22,18 @@ def on(
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
) -> Type[Matcher]: ... ) -> Type[Matcher]: ...
def on_metaevent( def on_metaevent(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -33,24 +44,29 @@ def on_message(
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
) -> Type[Matcher]: ... ) -> Type[Matcher]: ...
def on_notice( def on_notice(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
) -> Type[Matcher]: ... ) -> Type[Matcher]: ...
def on_request( def on_request(
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
*, *,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -63,6 +79,7 @@ def on_startswith(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -75,6 +92,7 @@ def on_endswith(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -87,6 +105,7 @@ def on_fullmatch(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -98,6 +117,7 @@ def on_keyword(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -106,10 +126,12 @@ def on_command(
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
force_whitespace: Optional[Union[str, bool]] = ...,
*, *,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -123,6 +145,7 @@ def on_shell_command(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -135,6 +158,19 @@ def on_regex(
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_type(
types: Union[Type[Event], Tuple[Type[Event], ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
*,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -149,6 +185,7 @@ class CommandGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -157,11 +194,13 @@ class CommandGroup:
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
*, *,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
rule: Optional[Union[Rule, T_RuleChecker]] = ..., 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]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -171,11 +210,12 @@ class CommandGroup:
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]], aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
parser: Optional[ArgumentParser] = ..., parser: Optional[ArgumentParser] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -190,6 +230,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -202,6 +243,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -210,8 +252,10 @@ class MatcherGroup:
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -223,6 +267,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -231,8 +276,10 @@ class MatcherGroup:
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -241,8 +288,10 @@ class MatcherGroup:
self, self,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -256,6 +305,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -269,6 +319,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -282,6 +333,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -294,6 +346,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -302,11 +355,13 @@ class MatcherGroup:
self, self,
cmd: Union[str, Tuple[str, ...]], cmd: Union[str, Tuple[str, ...]],
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
force_whitespace: Optional[Union[str, bool]] = ...,
*, *,
rule: Optional[Union[Rule, T_RuleChecker]] = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -321,6 +376,7 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,
@@ -334,6 +390,20 @@ class MatcherGroup:
permission: Optional[Union[Permission, T_PermissionChecker]] = ..., permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ..., temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
) -> Type[Matcher]: ...
def on_type(
self,
types: Union[Type[Event], Tuple[Type[Event]]],
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
temp: bool = ...,
expire_time: Optional[Union[datetime, timedelta]] = ...,
priority: int = ..., priority: int = ...,
block: bool = ..., block: bool = ...,
state: Optional[T_State] = ..., state: Optional[T_State] = ...,

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,27 @@ FrontMatter:
import re import re
import shlex import shlex
from itertools import product from argparse import Action
from argparse import Namespace from argparse import ArgumentError
from typing_extensions import TypedDict from contextvars import ContextVar
from itertools import chain, product
from argparse import Namespace as Namespace
from argparse import ArgumentParser as ArgParser from argparse import ArgumentParser as ArgParser
from typing import Any, List, Tuple, Union, Optional, Sequence, NamedTuple from typing import (
IO,
TYPE_CHECKING,
List,
Type,
Tuple,
Union,
TypeVar,
Optional,
Sequence,
TypedDict,
NamedTuple,
cast,
overload,
)
from pygtrie import CharTrie from pygtrie import CharTrie
@@ -24,27 +40,28 @@ from nonebot.typing import T_State
from nonebot.exception import ParserExit from nonebot.exception import ParserExit
from nonebot.internal.rule import Rule as Rule from nonebot.internal.rule import Rule as Rule
from nonebot.adapters import Bot, Event, Message, MessageSegment from nonebot.adapters import Bot, Event, Message, MessageSegment
from nonebot.params import ( from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
Command,
EventToMe,
EventType,
CommandArg,
EventMessage,
EventPlainText,
)
from nonebot.consts import ( from nonebot.consts import (
CMD_KEY, CMD_KEY,
REGEX_STR,
PREFIX_KEY, PREFIX_KEY,
REGEX_DICT, REGEX_DICT,
SHELL_ARGS, SHELL_ARGS,
SHELL_ARGV, SHELL_ARGV,
CMD_ARG_KEY, CMD_ARG_KEY,
KEYWORD_KEY,
RAW_CMD_KEY, RAW_CMD_KEY,
REGEX_GROUP, REGEX_GROUP,
ENDSWITH_KEY,
CMD_START_KEY, CMD_START_KEY,
FULLMATCH_KEY,
REGEX_MATCHED, REGEX_MATCHED,
STARTSWITH_KEY,
CMD_WHITESPACE_KEY,
) )
T = TypeVar("T")
CMD_RESULT = TypedDict( CMD_RESULT = TypedDict(
"CMD_RESULT", "CMD_RESULT",
{ {
@@ -52,6 +69,7 @@ CMD_RESULT = TypedDict(
"raw_command": Optional[str], "raw_command": Optional[str],
"command_arg": Optional[Message[MessageSegment]], "command_arg": Optional[Message[MessageSegment]],
"command_start": Optional[str], "command_start": Optional[str],
"command_whitespace": Optional[str],
}, },
) )
@@ -59,6 +77,8 @@ TRIE_VALUE = NamedTuple(
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])] "TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
) )
parser_message: ContextVar[str] = ContextVar("parser_message")
class TrieRule: class TrieRule:
prefix: CharTrie = CharTrie() prefix: CharTrie = CharTrie()
@@ -73,7 +93,11 @@ class TrieRule:
@classmethod @classmethod
def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT: def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:
prefix = 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 state[PREFIX_KEY] = prefix
if event.get_type() != "message": if event.get_type() != "message":
@@ -83,15 +107,28 @@ class TrieRule:
message_seg: MessageSegment = message[0] message_seg: MessageSegment = message[0]
if message_seg.is_text(): if message_seg.is_text():
segment_text = str(message_seg).lstrip() segment_text = str(message_seg).lstrip()
pf = cls.prefix.longest_prefix(segment_text) if pf := cls.prefix.longest_prefix(segment_text):
if pf:
value: TRIE_VALUE = pf.value value: TRIE_VALUE = pf.value
prefix[RAW_CMD_KEY] = pf.key prefix[RAW_CMD_KEY] = pf.key
prefix[CMD_START_KEY] = value.command_start prefix[CMD_START_KEY] = value.command_start
prefix[CMD_KEY] = value.command prefix[CMD_KEY] = value.command
msg = message.copy() msg = message.copy()
msg.pop(0) 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): for new_segment in reversed(new_message):
msg.insert(0, new_segment) msg.insert(0, new_segment)
prefix[CMD_ARG_KEY] = msg prefix[CMD_ARG_KEY] = msg
@@ -113,18 +150,32 @@ class StartswithRule:
self.msg = msg self.msg = msg
self.ignorecase = ignorecase self.ignorecase = ignorecase
async def __call__( def __repr__(self) -> str:
self, type: str = EventType(), text: str = EventPlainText() return f"Startswith(msg={self.msg}, ignorecase={self.ignorecase})"
) -> Any:
if type != "message": def __eq__(self, other: object) -> bool:
return (
isinstance(other, StartswithRule)
and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
)
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
try:
text = event.get_plaintext()
except Exception:
return False return False
return bool( if match := re.match(
re.match(
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})", f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
text, text,
re.IGNORECASE if self.ignorecase else 0, re.IGNORECASE if self.ignorecase else 0,
) ):
) state[STARTSWITH_KEY] = match.group()
return True
return False
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule: def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
@@ -154,18 +205,32 @@ class EndswithRule:
self.msg = msg self.msg = msg
self.ignorecase = ignorecase self.ignorecase = ignorecase
async def __call__( def __repr__(self) -> str:
self, type: str = EventType(), text: str = EventPlainText() return f"Endswith(msg={self.msg}, ignorecase={self.ignorecase})"
) -> Any:
if type != "message": def __eq__(self, other: object) -> bool:
return (
isinstance(other, EndswithRule)
and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
)
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
try:
text = event.get_plaintext()
except Exception:
return False return False
return bool( if match := re.search(
re.search( f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$",
f"(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})$",
text, text,
re.IGNORECASE if self.ignorecase else 0, re.IGNORECASE if self.ignorecase else 0,
) ):
) state[ENDSWITH_KEY] = match.group()
return True
return False
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule: def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
@@ -192,17 +257,35 @@ class FullmatchRule:
__slots__ = ("msg", "ignorecase") __slots__ = ("msg", "ignorecase")
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False): def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
self.msg = frozenset(map(str.casefold, msg) if ignorecase else msg) self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
self.ignorecase = ignorecase self.ignorecase = ignorecase
async def __call__( def __repr__(self) -> str:
self, type_: str = EventType(), text: str = EventPlainText() return f"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})"
) -> bool:
def __eq__(self, other: object) -> bool:
return ( return (
type_ == "message" isinstance(other, FullmatchRule)
and (text.casefold() if self.ignorecase else text) in self.msg and frozenset(self.msg) == frozenset(other.msg)
and self.ignorecase == other.ignorecase
) )
def __hash__(self) -> int:
return hash((frozenset(self.msg), self.ignorecase))
async def __call__(self, event: Event, state: T_State) -> bool:
try:
text = event.get_plaintext()
except Exception:
return False
if not text:
return False
text = text.casefold() if self.ignorecase else text
if text in self.msg:
state[FULLMATCH_KEY] = text
return True
return False
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule: def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
"""完全匹配消息。 """完全匹配消息。
@@ -229,12 +312,28 @@ class KeywordsRule:
def __init__(self, *keywords: str): def __init__(self, *keywords: str):
self.keywords = keywords self.keywords = keywords
async def __call__( def __repr__(self) -> str:
self, type: str = EventType(), text: str = EventPlainText() return f"Keywords(keywords={self.keywords})"
) -> bool:
if type != "message": def __eq__(self, other: object) -> bool:
return isinstance(other, KeywordsRule) and frozenset(
self.keywords
) == frozenset(other.keywords)
def __hash__(self) -> int:
return hash(frozenset(self.keywords))
async def __call__(self, event: Event, state: T_State) -> bool:
try:
text = event.get_plaintext()
except Exception:
return False
if not text:
return False
if key := next((k for k in self.keywords if k in text), None):
state[KEYWORD_KEY] = key
return True
return False return False
return bool(text and any(keyword in text for keyword in self.keywords))
def keyword(*keywords: str) -> Rule: def keyword(*keywords: str) -> Rule:
@@ -252,21 +351,48 @@ class CommandRule:
参数: 参数:
cmds: 指定命令元组列表 cmds: 指定命令元组列表
force_whitespace: 是否强制命令后必须有指定空白符
""" """
__slots__ = ("cmds",) __slots__ = ("cmds", "force_whitespace")
def __init__(self, cmds: List[Tuple[str, ...]]): def __init__(
self.cmds = cmds self,
cmds: List[Tuple[str, ...]],
force_whitespace: Optional[Union[str, bool]] = None,
):
self.cmds = tuple(cmds)
self.force_whitespace = force_whitespace
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool: def __repr__(self) -> str:
return cmd in self.cmds return f"Command(cmds={self.cmds})"
def __repr__(self): def __eq__(self, other: object) -> bool:
return f"<Command {self.cmds}>" return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset(
other.cmds
)
def __hash__(self) -> int:
return hash((frozenset(self.cmds),))
async def __call__(
self,
cmd: Optional[Tuple[str, ...]] = Command(),
cmd_whitespace: Optional[str] = CommandWhitespace(),
) -> bool:
if cmd not in self.cmds:
return False
if self.force_whitespace is None:
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>`, 根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
@@ -278,6 +404,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
参数: 参数:
cmds: 命令文本或命令元组 cmds: 命令文本或命令元组
force_whitespace: 是否强制命令后必须有指定空白符
用法: 用法:
使用默认 `command_start`, `command_sep` 配置 使用默认 `command_start`, `command_sep` 配置
@@ -309,7 +436,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
f"{start}{sep.join(command)}", TRIE_VALUE(start, command) f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
) )
return Rule(CommandRule(commands)) return Rule(CommandRule(commands, force_whitespace))
class ArgumentParser(ArgParser): class ArgumentParser(ArgParser):
@@ -320,25 +447,50 @@ class ArgumentParser(ArgParser):
参考文档: [argparse](https://docs.python.org/3/library/argparse.html) 参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
""" """
def _print_message(self, message, file=None): if TYPE_CHECKING:
old_message: str = getattr(self, "message", "")
if old_message:
old_message += "\n"
old_message += message
setattr(self, "message", old_message)
def exit(self, status: int = 0, message: Optional[str] = None): @overload
raise ParserExit( def parse_args(
status=status, message=message or getattr(self, "message", None) self, args: Optional[Sequence[Union[str, MessageSegment]]] = ...
) ) -> Namespace:
...
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: None
) -> Namespace:
... # type: ignore[misc]
@overload
def parse_args(
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
) -> T:
...
def parse_args( def parse_args(
self, self,
args: Optional[Sequence[str]] = None, args: Optional[Sequence[Union[str, MessageSegment]]] = None,
namespace: Optional[Namespace] = None, namespace: Optional[T] = None,
) -> Namespace: ) -> Union[Namespace, T]:
setattr(self, "message", "") ...
return super().parse_args(args=args, namespace=namespace) # type: ignore
def _parse_optional(
self, arg_string: Union[str, MessageSegment]
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
return (
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
)
def _print_message(self, message: str, file: Optional[IO[str]] = None):
if (msg := parser_message.get(None)) is not None:
parser_message.set(msg + message)
else:
super()._print_message(message, file)
def exit(self, status: int = 0, message: Optional[str] = None):
if message:
self._print_message(message)
raise ParserExit(status=status, message=parser_message.get(None))
class ShellCommandRule: class ShellCommandRule:
@@ -352,27 +504,50 @@ class ShellCommandRule:
__slots__ = ("cmds", "parser") __slots__ = ("cmds", "parser")
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]): def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
self.cmds = cmds self.cmds = tuple(cmds)
self.parser = parser self.parser = parser
def __repr__(self) -> str:
return f"ShellCommand(cmds={self.cmds}, parser={self.parser})"
def __eq__(self, other: object) -> bool:
return (
isinstance(other, ShellCommandRule)
and frozenset(self.cmds) == frozenset(other.cmds)
and self.parser is other.parser
)
def __hash__(self) -> int:
return hash((frozenset(self.cmds), self.parser))
async def __call__( async def __call__(
self, self,
state: T_State, state: T_State,
cmd: Optional[Tuple[str, ...]] = Command(), cmd: Optional[Tuple[str, ...]] = Command(),
msg: Optional[Message] = CommandArg(), msg: Optional[Message] = CommandArg(),
) -> bool: ) -> bool:
if cmd in self.cmds and msg is not None: if cmd not in self.cmds or msg is None:
message = str(msg) return False
state[SHELL_ARGV] = shlex.split(message)
state[SHELL_ARGV] = list(
chain.from_iterable(
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
for seg in msg
)
)
if self.parser: if self.parser:
t = parser_message.set("")
try: try:
args = self.parser.parse_args(state[SHELL_ARGV]) args = self.parser.parse_args(state[SHELL_ARGV])
state[SHELL_ARGS] = args state[SHELL_ARGS] = args
except ArgumentError as e: # pragma: py-gte-39
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
except ParserExit as e: except ParserExit as e:
state[SHELL_ARGS] = e state[SHELL_ARGS] = e
finally:
parser_message.reset(t)
return True return True
else:
return False
def shell_command( def shell_command(
@@ -452,17 +627,27 @@ class RegexRule:
self.regex = regex self.regex = regex
self.flags = flags self.flags = flags
async def __call__( def __repr__(self) -> str:
self, return f"Regex(regex={self.regex!r}, flags={self.flags})"
state: T_State,
type: str = EventType(), def __eq__(self, other: object) -> bool:
msg: Message = EventMessage(), return (
) -> bool: isinstance(other, RegexRule)
if type != "message": and self.regex == other.regex
and self.flags == other.flags
)
def __hash__(self) -> int:
return hash((self.regex, self.flags))
async def __call__(self, event: Event, state: T_State) -> bool:
try:
msg = event.get_message()
except Exception:
return False return False
matched = re.search(self.regex, str(msg), self.flags) if matched := re.search(self.regex, str(msg), self.flags):
if matched:
state[REGEX_MATCHED] = matched.group() state[REGEX_MATCHED] = matched.group()
state[REGEX_STR] = matched.group()
state[REGEX_GROUP] = matched.groups() state[REGEX_GROUP] = matched.groups()
state[REGEX_DICT] = matched.groupdict() state[REGEX_DICT] = matched.groupdict()
return True return True
@@ -473,7 +658,7 @@ class RegexRule:
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule: def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
"""匹配符合正则表达式的消息字符串。 """匹配符合正则表达式的消息字符串。
可以通过 {ref}`nonebot.params.RegexMatched` 获取匹配成功的字符串, 可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组, 通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组,
通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。 通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。
@@ -498,6 +683,15 @@ class ToMeRule:
__slots__ = () __slots__ = ()
def __repr__(self) -> str:
return "ToMe()"
def __eq__(self, other: object) -> bool:
return isinstance(other, ToMeRule)
def __hash__(self) -> int:
return hash((self.__class__,))
async def __call__(self, to_me: bool = EventToMe()) -> bool: async def __call__(self, to_me: bool = EventToMe()) -> bool:
return to_me return to_me
@@ -508,6 +702,37 @@ def to_me() -> Rule:
return Rule(ToMeRule()) return Rule(ToMeRule())
class IsTypeRule:
"""检查事件类型是否为指定类型。"""
__slots__ = ("types",)
def __init__(self, *types: Type[Event]):
self.types = types
def __repr__(self) -> str:
return f"IsType(types={tuple(type.__name__ for type in self.types)})"
def __eq__(self, other: object) -> bool:
return isinstance(other, IsTypeRule) and self.types == other.types
def __hash__(self) -> int:
return hash((self.types,))
async def __call__(self, event: Event) -> bool:
return isinstance(event, self.types)
def is_type(*types: Type[Event]) -> Rule:
"""匹配事件类型。
参数:
types: 事件类型
"""
return Rule(IsTypeRule(*types))
__autodoc__ = { __autodoc__ = {
"Rule": True, "Rule": True,
"Rule.__call__": True, "Rule.__call__": True,

View File

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

View File

@@ -9,7 +9,9 @@ import re
import json import json
import asyncio import asyncio
import inspect import inspect
import importlib
import dataclasses import dataclasses
from pathlib import Path
from functools import wraps, partial from functools import wraps, partial
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing_extensions import ParamSpec, get_args, get_origin from typing_extensions import ParamSpec, get_args, get_origin
@@ -24,6 +26,7 @@ from typing import (
Coroutine, Coroutine,
AsyncGenerator, AsyncGenerator,
ContextManager, ContextManager,
overload,
) )
from pydantic.typing import is_union, is_none_type from pydantic.typing import is_union, is_none_type
@@ -62,12 +65,10 @@ def generic_check_issubclass(
except TypeError: except TypeError:
origin = get_origin(cls) origin = get_origin(cls)
if is_union(origin): if is_union(origin):
for type_ in get_args(cls): return all(
if not is_none_type(type_) and not generic_check_issubclass( is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
type_, class_or_tuple for type_ in get_args(cls)
): )
return False
return True
elif origin: elif origin:
return issubclass(origin, class_or_tuple) return issubclass(origin, class_or_tuple)
return False return False
@@ -131,11 +132,28 @@ async def run_sync_ctx_manager(
await run_sync(cm.__exit__)(None, None, None) await run_sync(cm.__exit__)(None, None, None)
@overload
async def run_coro_with_catch( async def run_coro_with_catch(
coro: Coroutine[Any, Any, T], coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...], exc: Tuple[Type[Exception], ...],
return_on_err: R = None, ) -> Union[T, None]:
...
@overload
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: R,
) -> Union[T, R]: ) -> Union[T, R]:
...
async def run_coro_with_catch(
coro: Coroutine[Any, Any, T],
exc: Tuple[Type[Exception], ...],
return_on_err: Optional[R] = None,
) -> Optional[Union[T, R]]:
try: try:
return await coro return await coro
except exc: except exc:
@@ -149,6 +167,31 @@ def get_name(obj: Any) -> str:
return obj.__class__.__name__ return obj.__class__.__name__
def path_to_module_name(path: Path) -> str:
"""转换路径为模块名"""
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): class DataclassEncoder(json.JSONEncoder):
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`""" """在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
@@ -175,7 +218,7 @@ def logger_wrapper(logger_name: str):
def log(level: str, message: str, exception: Optional[Exception] = None): def log(level: str, message: str, exception: Optional[Exception] = None):
logger.opt(colors=True, exception=exception).log( logger.opt(colors=True, exception=exception).log(
level, f"<m>{escape_tag(logger_name)}</m> | " + message level, f"<m>{escape_tag(logger_name)}</m> | {message}"
) )
return log return log

View File

@@ -11,7 +11,7 @@
"start": "yarn workspace nonebot start", "start": "yarn workspace nonebot start",
"serve": "yarn workspace nonebot serve", "serve": "yarn workspace nonebot serve",
"clear": "yarn workspace nonebot clear", "clear": "yarn workspace nonebot clear",
"prettier": "prettier --config ./.prettierrc --write \"./website/**/*.md\"" "prettier": "prettier --config ./.prettierrc --write \"./website/\""
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

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

View File

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

3948
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot2" name = "nonebot2"
version = "2.0.0-beta.3" version = "2.0.0rc4"
description = "An asynchronous python bot framework." description = "An asynchronous python bot framework."
authors = ["yanyongyu <yyy@nonebot.dev>"] authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT" license = "MIT"
@@ -22,49 +22,57 @@ packages = [
include = ["nonebot/py.typed"] include = ["nonebot/py.typed"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7.3" python = "^3.8"
yarl = "^1.7.2" yarl = "^1.7.2"
loguru = "^0.6.0" loguru = "^0.6.0"
pygtrie = "^2.4.1" pygtrie = "^2.4.1"
tomlkit = "^0.10.0" typing-extensions = ">=4.0.0,<5.0.0"
fastapi = "^0.78.0" tomli = { version = "^2.0.1", python = "<3.11" }
typing-extensions = ">=3.10.0,<5.0.0" pydantic = { version = "^1.10.0", extras = ["dotenv"] }
Quart = { version = "^0.17.0", optional = true }
websockets = { version="^10.0", optional = true }
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
uvicorn = { version = "^0.17.0", extras = ["standard"] }
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
[tool.poetry.dev-dependencies] websockets = { version = "^10.0", optional = true }
Quart = { version = ">=0.18.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 }
[tool.poetry.group.dev.dependencies]
pycln = "^2.1.2"
isort = "^5.10.1" isort = "^5.10.1"
black = "^22.1.0" black = "^23.1.0"
pytest-cov = "^3.0.0" nonemoji = "^0.1.2"
pre-commit = "^2.16.0" pre-commit = "^3.0.0"
pytest-xdist = "^2.5.0"
pytest-asyncio = "^0.18.1" [tool.poetry.group.test.dependencies]
nonebug = { git = "https://github.com/nonebot/nonebug.git" } nonebug = "^0.3.0"
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" } pytest-cov = "^4.0.0"
pytest-xdist = "^3.0.2"
pytest-asyncio = "^0.21.0"
coverage-conditional-plugin = "^0.8.0"
[tool.poetry.group.docs.dependencies]
nb-autodoc = "^1.0.0a5"
[tool.poetry.extras] [tool.poetry.extras]
quart = ["quart"]
httpx = ["httpx"] httpx = ["httpx"]
aiohttp = ["aiohttp"] aiohttp = ["aiohttp"]
websockets = ["websockets"] websockets = ["websockets"]
all = ["quart", "aiohttp", "httpx", "websockets"] quart = ["quart", "uvicorn"]
fastapi = ["fastapi", "uvicorn"]
# [[tool.poetry.source]] all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
# name = "aliyun"
# url = "https://mirrors.aliyun.com/pypi/simple/"
# default = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
addopts = "--cov=nonebot --cov-report=term-missing" addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]
[tool.black] [tool.black]
line-length = 88 line-length = 88
target-version = ["py37", "py38", "py39", "py310"] target-version = ["py38", "py39", "py310", "py311"]
include = '\.pyi?$' include = '\.pyi?$'
extend-exclude = ''' extend-exclude = '''
''' '''
@@ -78,6 +86,20 @@ force_sort_within_sections = true
src_paths = ["nonebot", "tests"] src_paths = ["nonebot", "tests"]
extra_standard_library = ["typing_extensions"] extra_standard_library = ["typing_extensions"]
[tool.pycln]
path = "."
all = false
[tool.pyright]
reportShadowedImports = false
pythonVersion = "3.8"
pythonPlatform = "All"
executionEnvironments = [
{ root = "./tests", extraPaths = ["./"] },
{ root = "./" },
]
[build-system] [build-system]
requires = ["poetry_core>=1.0.0"] requires = ["poetry_core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@@ -1,8 +1,25 @@
[run]
plugins =
coverage_conditional_plugin
[report] [report]
exclude_lines = exclude_lines =
def __repr__
pragma: no cover pragma: no cover
if TYPE_CHECKING: def __repr__
def __str__
@(typing\.)?overload
if (typing\.)?TYPE_CHECKING( is True)?:
@(abc\.)?abstractmethod @(abc\.)?abstractmethod
raise NotImplementedError raise NotImplementedError
\.\.\.
pass
if __name__ == .__main__.: 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,2 +1,3 @@
ENVIRONMENT=test ENVIRONMENT=test
COMMON_CONFIG=common COMMON_CONFIG=common
COMMON_OVERRIDE=old

View File

@@ -1,4 +1,13 @@
LOG_LEVEL=TRACE LOG_LEVEL=TRACE
NICKNAME=["test"] NICKNAME=["test"]
SUPERUSERS=["test", "fake:faketest"] SUPERUSERS=["test", "fake:faketest"]
COMMON_OVERRIDE=new
CONFIG_FROM_ENV= CONFIG_FROM_ENV=
CONFIG_OVERRIDE=old
NESTED_DICT={"a": 1}
NESTED_DICT__B=2
NESTED_DICT__C__D=3
NESTED_MISSING_DICT__A=1
NESTED_MISSING_DICT__B__C=2
NOT_NESTED=some string
NOT_NESTED__A=1

View File

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

View File

@@ -1,21 +1,30 @@
import os
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Set
import pytest 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: if TYPE_CHECKING:
from nonebot.plugin import Plugin from nonebot.plugin import Plugin
@pytest.fixture def pytest_configure(config: pytest.Config) -> None:
def load_plugin(nonebug_init: None) -> Set["Plugin"]: config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
import nonebot
@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")) return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
@pytest.fixture @pytest.fixture(scope="session", autouse=True)
def load_example(nonebug_init: None) -> Set["Plugin"]: def load_example(nonebug_init: None) -> Set["Plugin"]:
import nonebot # preload example plugins
return nonebot.load_plugins(str(Path(__file__).parent / "examples")) return nonebot.load_plugins(str(Path(__file__).parent / "examples"))

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

View File

View File

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

0
tests/plugins.empty.toml Normal file
View File

View File

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

View File

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

4
tests/plugins.json Normal file
View File

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

3
tests/plugins.toml Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1 @@
from .nested_subplugin2 import a # nopycln: import

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing_extensions import Annotated
from nonebot import on_message from nonebot import on_message
from nonebot.params import Depends 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()): async def class_depend(c: ClassDependency = Depends()):
return c 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,3 +1,5 @@
from typing import Union
from nonebot.adapters import Event, Message from nonebot.adapters import Event, Message
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
@@ -6,14 +8,34 @@ async def event(e: Event) -> Event:
return e return e
class SubEvent(Event): async def legacy_event(event):
return event
async def not_legacy_event(event: int):
... ...
async def sub_event(e: SubEvent) -> SubEvent: class FooEvent(Event):
...
async def sub_event(e: FooEvent) -> FooEvent:
return e return e
class BarEvent(Event):
...
async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]:
return e
async def not_event(e: Union[int, Event]):
...
async def event_type(t: str = EventType()) -> str: async def event_type(t: str = EventType()) -> str:
return t return t

View File

@@ -4,14 +4,20 @@ from nonebot.typing import T_State
from nonebot.adapters import Message from nonebot.adapters import Message
from nonebot.params import ( from nonebot.params import (
Command, Command,
Keyword,
Endswith,
RegexStr,
Fullmatch,
RegexDict, RegexDict,
CommandArg, CommandArg,
RawCommand, RawCommand,
RegexGroup, RegexGroup,
Startswith,
CommandStart, CommandStart,
RegexMatched, RegexMatched,
ShellCommandArgs, ShellCommandArgs,
ShellCommandArgv, ShellCommandArgv,
CommandWhitespace,
) )
@@ -19,6 +25,14 @@ async def state(x: T_State) -> T_State:
return x return x
async def legacy_state(state):
return state
async def not_legacy_state(state: int):
...
async def command(cmd: Tuple[str, ...] = Command()) -> Tuple[str, ...]: async def command(cmd: Tuple[str, ...] = Command()) -> Tuple[str, ...]:
return cmd return cmd
@@ -35,6 +49,10 @@ async def command_start(start: str = CommandStart()) -> str:
return start return start
async def command_whitespace(whitespace: str = CommandWhitespace()) -> str:
return whitespace
async def shell_command_args( async def shell_command_args(
shell_command_args: dict = ShellCommandArgs(), shell_command_args: dict = ShellCommandArgs(),
) -> dict: ) -> dict:
@@ -57,3 +75,23 @@ async def regex_group(regex_group: Tuple = RegexGroup()) -> Tuple:
async def regex_matched(regex_matched: str = RegexMatched()) -> str: async def regex_matched(regex_matched: str = RegexMatched()) -> str:
return regex_matched return regex_matched
async def regex_str(regex_matched: str = RegexStr()) -> str:
return regex_matched
async def startswith(startswith: str = Startswith()) -> str:
return startswith
async def endswith(endswith: str = Endswith()) -> str:
return endswith
async def fullmatch(fullmatch: str = Fullmatch()) -> str:
return fullmatch
async def keyword(keyword: str = Keyword()) -> str:
return keyword

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import pytest
from pydantic import ValidationError, parse_obj_as from pydantic import ValidationError, parse_obj_as
from utils import make_fake_message from utils import make_fake_message
@@ -29,14 +30,15 @@ def test_segment_validate():
MessageSegment = Message.get_segment_class() MessageSegment = Message.get_segment_class()
assert parse_obj_as( assert parse_obj_as(
MessageSegment, {"type": "text", "data": {"text": "text"}} MessageSegment,
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
) == MessageSegment.text("text") ) == MessageSegment.text("text")
try: with pytest.raises(ValidationError):
parse_obj_as(MessageSegment, "some str") parse_obj_as(MessageSegment, "some str")
assert False
except ValidationError: with pytest.raises(ValidationError):
assert True parse_obj_as(MessageSegment, {"data": {}})
def test_segment(): def test_segment():
@@ -85,7 +87,6 @@ def test_message_add():
def test_message_getitem(): def test_message_getitem():
Message = make_fake_message() Message = make_fake_message()
MessageSegment = Message.get_segment_class() MessageSegment = Message.get_segment_class()
@@ -100,7 +101,7 @@ def test_message_getitem():
assert message[0] == MessageSegment.text("test") assert message[0] == MessageSegment.text("test")
assert message[0:2] == Message( assert message[:2] == Message(
[MessageSegment.text("test"), MessageSegment.image("test2")] [MessageSegment.text("test"), MessageSegment.image("test2")]
) )
@@ -129,11 +130,8 @@ def test_message_validate():
assert parse_obj_as(Message, Message([])) == Message([]) assert parse_obj_as(Message, Message([])) == Message([])
try: with pytest.raises(ValidationError):
parse_obj_as(Message, Message_([])) parse_obj_as(Message, Message_([]))
assert False
except ValidationError:
assert True
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")]) assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")])
@@ -146,8 +144,5 @@ def test_message_validate():
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
try: with pytest.raises(ValidationError):
parse_obj_as(Message, object()) parse_obj_as(Message, object())
assert False
except ValidationError:
assert True

View File

@@ -1,9 +1,8 @@
from nonebot.adapters import MessageTemplate
from utils import escape_text, make_fake_message from utils import escape_text, make_fake_message
def test_template_basis(): def test_template_basis():
from nonebot.adapters import MessageTemplate
template = MessageTemplate("{key:.3%}") template = MessageTemplate("{key:.3%}")
formatted = template.format(key=0.123456789) formatted = template.format(key=0.123456789)
assert formatted == "12.346%" assert formatted == "12.346%"
@@ -11,11 +10,11 @@ def test_template_basis():
def test_template_message(): def test_template_message():
Message = make_fake_message() Message = make_fake_message()
template = Message.template("{a:custom}{b:text}{c:image}") template = Message.template("{a:custom}{b:text}{c:image}/{d}")
@template.add_format_spec @template.add_format_spec
def custom(input: str) -> str: def custom(input: str) -> str:
return input + "-custom!" return f"{input}-custom!"
try: try:
template.add_format_spec(custom) template.add_format_spec(custom)
@@ -24,12 +23,17 @@ def test_template_message():
else: else:
raise AssertionError("Should raise ValueError") raise AssertionError("Should raise ValueError")
format_args = {"a": "custom", "b": "text", "c": "https://example.com/test"} format_args = {
"a": "custom",
"b": "text",
"c": "https://example.com/test",
"d": 114,
}
formatted = template.format(**format_args) formatted = template.format(**format_args)
assert template.format_map(format_args) == formatted assert template.format_map(format_args) == formatted
assert formatted.extract_plain_text() == "custom-custom!text" assert formatted.extract_plain_text() == "custom-custom!text/114"
assert str(formatted) == "custom-custom!text[fake:image]" assert str(formatted) == "custom-custom!text[fake:image]/114"
def test_rich_template_message(): def test_rich_template_message():

View File

@@ -1,32 +1,86 @@
from typing import cast import json
import asyncio
from typing import Any, Set, cast
import pytest import pytest
from nonebug import App from nonebug import App
import nonebot
@pytest.mark.asyncio from nonebot.config import Env
@pytest.mark.parametrize( from nonebot.adapters import Bot
"nonebug_init", from nonebot.params import Depends
[ from nonebot import _resolve_combine_expr
pytest.param({"driver": "nonebot.drivers.fastapi:Driver"}, id="fastapi"), from nonebot.dependencies import Dependent
pytest.param({"driver": "nonebot.drivers.quart:Driver"}, id="quart"), from nonebot.exception import WebSocketClosed
], from nonebot.drivers._lifespan import Lifespan
indirect=True, from nonebot.drivers import (
)
async def test_reverse_driver(app: App):
import nonebot
from nonebot.exception import WebSocketClosed
from nonebot.drivers import (
URL, URL,
Driver,
Request, Request,
Response, Response,
WebSocket, WebSocket,
ForwardDriver,
ReverseDriver, ReverseDriver,
HTTPServerSetup, HTTPServerSetup,
WebSocketServerSetup, 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: async def _handle_http(request: Request) -> Response:
assert request.content in (b"test", "test") assert request.content in (b"test", "test")
@@ -59,7 +113,7 @@ async def test_reverse_driver(app: App):
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
driver.setup_websocket_server(ws_setup) 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() client = ctx.get_client()
response = await client.post("/http_test", data="test") response = await client.post("/http_test", data="test")
assert response.status_code == 200 assert response.status_code == 200
@@ -78,3 +132,143 @@ async def test_reverse_driver(app: App):
assert await ws.receive_bytes() == b"pong" assert await ws.receive_bytes() == b"pong"
await ws.close() await ws.close()
await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver",
[
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
],
indirect=True,
)
async def test_http_driver(driver: Driver):
driver = cast(ForwardDriver, driver)
request = Request(
"POST",
"https://httpbin.org/post",
params={"param": "test"},
headers={"X-Test": "test"},
cookies={"session": "test"},
content="test",
)
response = await driver.request(request)
assert response.status_code == 200 and response.content
data = json.loads(response.content)
assert data["args"] == {"param": "test"}
assert data["headers"].get("X-Test") == "test"
assert data["headers"].get("Cookie") == "session=test"
assert data["data"] == "test"
request = Request("POST", "https://httpbin.org/post", data={"form": "test"})
response = await driver.request(request)
assert response.status_code == 200 and response.content
data = json.loads(response.content)
assert data["form"] == {"form": "test"}
request = Request("POST", "https://httpbin.org/post", json={"json": "test"})
response = await driver.request(request)
assert response.status_code == 200 and response.content
data = json.loads(response.content)
assert data["json"] == {"json": "test"}
request = Request(
"POST", "https://httpbin.org/post", files={"test": ("test.txt", b"test")}
)
response = await driver.request(request)
assert response.status_code == 200 and response.content
data = json.loads(response.content)
assert data["files"] == {"test": "test"}
await asyncio.sleep(1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"driver, driver_type",
[
pytest.param(
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
"fastapi+aiohttp",
id="fastapi+aiohttp",
),
pytest.param(
"~httpx:Driver+~websockets",
"none+httpx+websockets",
id="httpx+websockets",
),
],
indirect=["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,13 +1,15 @@
import pytest import pytest
from nonebug import App from nonebug import App
from utils import make_fake_event, make_fake_message
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_weather(app: App, load_example): async def test_weather(app: App):
from examples.weather import weather from examples.weather import weather
from utils import make_fake_event, make_fake_message
# 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型 # 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型
# from nonebot.adapters.console import Message
Message = make_fake_message() Message = make_fake_message()
async with app.test_matcher(weather) as ctx: async with app.test_matcher(weather) as ctx:
@@ -15,6 +17,8 @@ async def test_weather(app: App, load_example):
msg = Message("/天气 上海") msg = Message("/天气 上海")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型 # 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
# from nonebot.adapters.console import MessageEvent
# event = MessageEvent(message=msg, to_me=True, ...)
event = make_fake_event(_message=msg, _to_me=True)() event = make_fake_event(_message=msg, _to_me=True)()
ctx.receive_event(bot, event) ctx.receive_event(bot, event)

View File

@@ -1,50 +1,59 @@
import os
import pytest import pytest
from nonebug import App
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' 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.asyncio
@pytest.mark.parametrize( async def test_init():
"nonebug_init", env = nonebot.get_driver().env
[
{
"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
assert env == "test" assert env == "test"
config = get_driver().config config = nonebot.get_driver().config
assert config.config_from_env == {"test": "test"} assert config.config_from_env == {"test": "test"}
assert config.config_override == "new"
assert config.config_from_init == "init" assert config.config_from_init == "init"
assert config.common_config == "common" assert config.common_config == "common"
assert config.common_override == "new"
assert config.nested_dict == {"a": 1, "b": 2, "c": {"d": 3}}
assert config.nested_missing_dict == {"a": 1, "b": {"c": 2}}
assert config.not_nested == "some string"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear): async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
import nonebot with monkeypatch.context() as m:
from nonebot.drivers import ForwardDriver, ReverseDriver m.setattr(nonebot, "_driver", None)
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_driver() get_driver()
nonebot.init(driver="nonebot.drivers.fastapi")
driver = get_driver() driver = get_driver()
assert isinstance(driver, ReverseDriver) assert isinstance(driver, ReverseDriver)
assert get_asgi() == driver.asgi assert get_asgi() == driver.asgi
assert get_app() == driver.server_app 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 runned = False
def mock_run(*args, **kwargs): def mock_run(*args, **kwargs):
@@ -59,7 +68,7 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_bot() get_bot()
monkeypatch.setattr(driver, "_clients", {"test": "test"}) monkeypatch.setattr(driver, "_bots", {"test": "test"})
assert get_bot() == "test" assert get_bot() == "test"
assert get_bot("test") == "test" assert get_bot("test") == "test"
assert get_bots() == {"test": "test"} assert get_bots() == {"test": "test"}

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