mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
835 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
033c90dd74 | ||
|
|
762b2e6ef1 | ||
|
|
3e3f504c1c | ||
|
|
8f8ce4b853 | ||
|
|
18b6151c91 | ||
|
|
0f552743df | ||
|
|
fdc9c6f056 | ||
|
|
16812e3621 | ||
|
|
17c3c09d86 | ||
|
|
f7fe9fba5c | ||
|
|
cc4d0b61f0 | ||
|
|
19d9014279 | ||
|
|
a63322633a | ||
|
|
5ddb9b295d | ||
|
|
2d4379fcfa | ||
|
|
227fb3b667 | ||
|
|
faba8aae4e | ||
|
|
852f033769 | ||
|
|
4975f4a0c8 | ||
|
|
93eb6cae93 | ||
|
|
dea43fb1ef | ||
|
|
dc83031589 | ||
|
|
ebb4ca4dba | ||
|
|
25fc7a7449 | ||
|
|
f4f72dc2b3 | ||
|
|
ac9ee830c6 | ||
|
|
73710aa311 | ||
|
|
b8c4898eff | ||
|
|
9189711c0a | ||
|
|
2d4c2e472b | ||
|
|
34427f0dd2 | ||
|
|
4f7d3965d4 | ||
|
|
64ce1a64d9 | ||
|
|
ca79f29c60 | ||
|
|
d5cd6427b9 | ||
|
|
aa00ccf3be | ||
|
|
3997c09c34 | ||
|
|
4a06576c5e | ||
|
|
67bca08ee2 | ||
|
|
867766b469 | ||
|
|
1175a4452e | ||
|
|
208533f5ca | ||
|
|
4a6428100b | ||
|
|
32bc2c314a | ||
|
|
ab8dea5a02 | ||
|
|
e06076aa3a | ||
|
|
36d90c0efd | ||
|
|
0fbfa20257 | ||
|
|
6c0d5f3e1d | ||
|
|
0b72c765a7 | ||
|
|
21815b380f | ||
|
|
9fed938de1 | ||
|
|
6df8d5b254 | ||
|
|
aedc541d03 | ||
|
|
bdf8dff08e | ||
|
|
081dc8352d | ||
|
|
6dad4d2a74 | ||
|
|
3528339751 | ||
|
|
efae3c8756 | ||
|
|
3aa1bc7b66 | ||
|
|
f6027bbcd9 | ||
|
|
1dec074232 | ||
|
|
76a455227d | ||
|
|
b33845b936 | ||
|
|
56f1927376 | ||
|
|
232b7134f0 | ||
|
|
980affd31b | ||
|
|
f2a35a7520 | ||
|
|
02c41eb97a | ||
|
|
06682ee36a | ||
|
|
97e3ee32e7 | ||
|
|
a4bc8fe544 | ||
|
|
e0d7e90f4a | ||
|
|
4404a6c74e | ||
|
|
ae8bf488d0 | ||
|
|
9f6e8a1833 | ||
|
|
af03c61f89 | ||
|
|
7a1e9adf33 | ||
|
|
eed42db645 | ||
|
|
7f8b5e9993 | ||
|
|
83552d6995 | ||
|
|
3bf393444d | ||
|
|
fd2ed08009 | ||
|
|
9b421548d6 | ||
|
|
f2d9a3ba6b | ||
|
|
abe90c0074 | ||
|
|
9a0cf5b9dc | ||
|
|
f9f82da58d | ||
|
|
a067d1b1b1 | ||
|
|
a97f0e6da9 | ||
|
|
58b18ada6f | ||
|
|
6f8c4692c8 | ||
|
|
ad4a04dae2 | ||
|
|
d62e59325b | ||
|
|
781f8a67df | ||
|
|
599bb377b7 | ||
|
|
97fd095666 | ||
|
|
3aec8e3acd | ||
|
|
1fdb7a45cf | ||
|
|
9f6c750236 | ||
|
|
4a52662ad8 | ||
|
|
0660ddba28 | ||
|
|
463eddb0f4 | ||
|
|
aa0c113e65 | ||
|
|
babd8b2093 | ||
|
|
23bcab5450 | ||
|
|
d766455d13 | ||
|
|
c9bea5d0ea | ||
|
|
8704ee42f1 | ||
|
|
bf366d8361 | ||
|
|
cf9729aac4 | ||
|
|
58475fe929 | ||
|
|
57c553f971 | ||
|
|
2b8aae4eee | ||
|
|
c5aa5d3deb | ||
|
|
3d8731f41d | ||
|
|
5e86d53e0b | ||
|
|
a50a3398de | ||
|
|
950930a275 | ||
|
|
c7af169a94 | ||
|
|
e17096d8d7 | ||
|
|
00e9e74dfc | ||
|
|
934954d985 | ||
|
|
a4a4991473 | ||
|
|
60acb71033 | ||
|
|
f8b4dfb1b1 | ||
|
|
ee5561046f | ||
|
|
6660c3b471 | ||
|
|
bd5ba84737 | ||
|
|
15c5464069 | ||
|
|
7b136548a9 | ||
|
|
36ed8030d3 | ||
|
|
eff1fe455f | ||
|
|
e3cb4c7907 | ||
|
|
be732cf9d8 | ||
|
|
88a5966a40 | ||
|
|
bdde496332 | ||
|
|
a989a895e4 | ||
|
|
7fc51e9227 | ||
|
|
571fd007ba | ||
|
|
599ef3b253 | ||
|
|
c0c7d141ef | ||
|
|
3be68895e5 | ||
|
|
ff21ceb946 | ||
|
|
bd9befbb55 | ||
|
|
ed91ec9bf5 | ||
|
|
a00def5d86 | ||
|
|
973282587e | ||
|
|
8a6d209942 | ||
|
|
fcd226031b | ||
|
|
fb3f0d5e30 | ||
|
|
56518748d9 | ||
|
|
474398ee3d | ||
|
|
597b104111 | ||
|
|
acec8945ac | ||
|
|
06b5f09371 | ||
|
|
15470cd3bb | ||
|
|
c1c5f57e0b | ||
|
|
533e8794b2 | ||
|
|
05c20a7a86 | ||
|
|
edb416736b | ||
|
|
2a68bb1b6e | ||
|
|
29ffbc630a | ||
|
|
5cf6b93984 | ||
|
|
30011e3fb4 | ||
|
|
36606ab05a | ||
|
|
0aba6b4bb4 | ||
|
|
fab51d9605 | ||
|
|
d7e2cc608b | ||
|
|
b2c5ab3235 | ||
|
|
1668568d1a | ||
|
|
4385934a6b | ||
|
|
4830182050 | ||
|
|
d86a86d4b2 | ||
|
|
f175bc9e80 | ||
|
|
40c2bc636a | ||
|
|
8ad5a8d4d1 | ||
|
|
aed91dcc48 | ||
|
|
de8ffb6c97 | ||
|
|
990cf32304 | ||
|
|
09b3f13e7e | ||
|
|
c39b13b782 | ||
|
|
3ec4611a29 | ||
|
|
9d6832303d | ||
|
|
9fc9f7c384 | ||
|
|
2021e81ed2 | ||
|
|
ada6e1ab64 | ||
|
|
2723a372da | ||
|
|
a56c93cbcc | ||
|
|
230476d8ae | ||
|
|
31a13551be | ||
|
|
82dbacda83 | ||
|
|
badd53b4bb | ||
|
|
94052b5bf7 | ||
|
|
72a6914980 | ||
|
|
a2a604dd85 | ||
|
|
d3d0779d30 | ||
|
|
a45e7d3854 | ||
|
|
4dadef3e51 | ||
|
|
ee643544f1 | ||
|
|
4598a3de9a | ||
|
|
59ad3d4b17 | ||
|
|
ba78c3aef8 | ||
|
|
3ce2b69431 | ||
|
|
d47722d87c | ||
|
|
c4ddfc3df1 | ||
|
|
58c8879cbb | ||
|
|
eb1342b78d | ||
|
|
feb619a85c | ||
|
|
a9ec70e798 | ||
|
|
c96e9dbcb6 | ||
|
|
8742d867e8 | ||
|
|
ed14dcd090 | ||
|
|
7956b53530 | ||
|
|
1140d668b6 | ||
|
|
9351b074b1 | ||
|
|
61dc206935 | ||
|
|
70f62bf4da | ||
|
|
812c0cd624 | ||
|
|
5107729290 | ||
|
|
9b59c16b04 | ||
|
|
fdc1dcace7 | ||
|
|
c6c22e3c29 | ||
|
|
0291b10560 | ||
|
|
6f07ce0060 | ||
|
|
b375575792 | ||
|
|
88765711f3 | ||
|
|
17ba8d70e1 | ||
|
|
e373251092 | ||
|
|
63dcc658da | ||
|
|
3aaf86e9a9 | ||
|
|
2cabeb658e | ||
|
|
0c187cd8c3 | ||
|
|
c0c8e1aa02 | ||
|
|
a8586d7990 | ||
|
|
91b3d3d5e0 | ||
|
|
8ef51154fd | ||
|
|
86c83064e4 | ||
|
|
a4be2c465f | ||
|
|
be4f36036c | ||
|
|
f6c7fb6da6 | ||
|
|
c9b4c3f3c0 | ||
|
|
4af4412cd7 | ||
|
|
4e7f7fb722 | ||
|
|
f01f692fde | ||
|
|
6e94dade69 | ||
|
|
eaef8dfc19 | ||
|
|
dec8b26b89 | ||
|
|
e8c39f9cc8 | ||
|
|
2a8644de81 | ||
|
|
5aaa0d3f12 | ||
|
|
22bb377fcf | ||
|
|
0c0ad0dd5e | ||
|
|
c947bdfef5 | ||
|
|
b197802d9a | ||
|
|
8019a570cc | ||
|
|
25a85330a7 | ||
|
|
35fb4fc18d | ||
|
|
ff50e997d0 | ||
|
|
6cda981aa2 | ||
|
|
dcfbb32363 | ||
|
|
d8a1a0ab38 | ||
|
|
6efd01a575 | ||
|
|
b2f7846eb4 | ||
|
|
8d2a284fe2 | ||
|
|
cce13f682d | ||
|
|
6c1d7ad74b | ||
|
|
4b837343ff | ||
|
|
d1904ba156 | ||
|
|
3ed1bde38a | ||
|
|
5cd82df580 | ||
|
|
fdd0e82099 | ||
|
|
f540245aec | ||
|
|
7b3ca228ef | ||
|
|
aa23adfd8a | ||
|
|
95ee5d54e8 | ||
|
|
eac0e7a656 | ||
|
|
e41ec29867 | ||
|
|
42a922deb6 | ||
|
|
8e70d55d77 | ||
|
|
eeaf823ea9 | ||
|
|
2195e07998 | ||
|
|
70fb8fc8c6 | ||
|
|
b78ae1ef0d | ||
|
|
6af5566466 | ||
|
|
8ceca0a90d | ||
|
|
08fa6dbfc8 | ||
|
|
967aa758d3 | ||
|
|
e3b10fbdc2 | ||
|
|
2115e5c6ec | ||
|
|
41dc908032 | ||
|
|
2b2f24628d | ||
|
|
1cc5d1af33 | ||
|
|
88074cf5c3 | ||
|
|
5d637eed95 | ||
|
|
362c43ce5f | ||
|
|
622b8eb51e | ||
|
|
c369dcf781 | ||
|
|
53d1e1dee9 | ||
|
|
75f5825cff | ||
|
|
d05c90787c | ||
|
|
e07ba36a4a | ||
|
|
f7c05d9a08 | ||
|
|
59c5a1a35d | ||
|
|
3eb653821e | ||
|
|
214bc838c2 | ||
|
|
79c7ea5bab | ||
|
|
b59b1be6ff | ||
|
|
aeb75a6ce3 | ||
|
|
847325a119 | ||
|
|
26eabfaf6f | ||
|
|
40a7b97220 | ||
|
|
91b40748c4 | ||
|
|
013a2f94d6 | ||
|
|
74d280ed75 | ||
|
|
b7d46de10e | ||
|
|
c37b5bbbca | ||
|
|
5e08e73698 | ||
|
|
b27bb92d03 | ||
|
|
6bf8858cc6 | ||
|
|
c97a780645 | ||
|
|
976c1cd8e0 | ||
|
|
26fd6f8a6c | ||
|
|
0020ad28ba | ||
|
|
ba9ca63f10 | ||
|
|
28b5b732c2 | ||
|
|
b944da8445 | ||
|
|
5cab166d6b | ||
|
|
546cdb4229 | ||
|
|
77790fad1f | ||
|
|
bcf849c98f | ||
|
|
f7b3c8af02 | ||
|
|
cced60589c | ||
|
|
62adb32c94 | ||
|
|
6ab752dcdb | ||
|
|
4d6f071739 | ||
|
|
bd140c2ceb | ||
|
|
59d9991aa4 | ||
|
|
55e7f59e40 | ||
|
|
bb83483020 | ||
|
|
5300ef5119 | ||
|
|
5a50d4203c | ||
|
|
01a96f3086 | ||
|
|
0570d779ee | ||
|
|
18d0bc2c81 | ||
|
|
87e0d8148f | ||
|
|
53d8989145 | ||
|
|
5433b4ebdf | ||
|
|
f10cecf16a | ||
|
|
60a3f6f4cc | ||
|
|
f70ae89098 | ||
|
|
2f60c5e9b4 | ||
|
|
015ddd9517 | ||
|
|
f1539d9ec4 | ||
|
|
2d0444ba75 | ||
|
|
ed2c222e83 | ||
|
|
ed048913a4 | ||
|
|
121ba17698 | ||
|
|
d0f5a76c47 | ||
|
|
f809f1d089 | ||
|
|
070ad18781 | ||
|
|
56119ef1cc | ||
|
|
30195a35dc | ||
|
|
0500b7baab | ||
|
|
08473a5c25 | ||
|
|
37ad14c277 | ||
|
|
3e8c6ce541 | ||
|
|
3dd5539dc7 | ||
|
|
559a0320a8 | ||
|
|
8646d885f0 | ||
|
|
84c008cdce | ||
|
|
2671cb5b72 | ||
|
|
379440708f | ||
|
|
4d070f5b48 | ||
|
|
82138454bc | ||
|
|
d98fe53d56 | ||
|
|
278b9e92c2 | ||
|
|
45418ccfae | ||
|
|
2ad2922565 | ||
|
|
84ebcb4ce6 | ||
|
|
6a0caacfd6 | ||
|
|
a8f3940cbc | ||
|
|
15d3910462 | ||
|
|
edfd0eb887 | ||
|
|
fe63717848 | ||
|
|
63424bc3ac | ||
|
|
99b1d0ed96 | ||
|
|
90c7fd4747 | ||
|
|
c1a9758a18 | ||
|
|
17e7a0c029 | ||
|
|
df6a948c08 | ||
|
|
9f19eb7a96 | ||
|
|
2b68428526 | ||
|
|
d62c6561c2 | ||
|
|
fc3bb5ff1f | ||
|
|
76b1bbb443 | ||
|
|
7b724925ba | ||
|
|
62dc2574c7 | ||
|
|
ea40ae3a18 | ||
|
|
f94e7d9b5b | ||
|
|
c8ba973280 | ||
|
|
35e062c588 | ||
|
|
53724487d3 | ||
|
|
a3003b0ff6 | ||
|
|
96ecd415cd | ||
|
|
e8ef4735ea | ||
|
|
b78b08ed81 | ||
|
|
e11ea52276 | ||
|
|
819e7334b2 | ||
|
|
1ebafaa9a5 | ||
|
|
3554292d5f | ||
|
|
ec9ef9a760 | ||
|
|
74663c7c5e | ||
|
|
cbc99be031 | ||
|
|
81e9bdd7ec | ||
|
|
323038ecc6 | ||
|
|
7091beb809 | ||
|
|
010c48d30f | ||
|
|
a5b2dd38d5 | ||
|
|
fa5f295fe7 | ||
|
|
7f7b23bd2f | ||
|
|
0434e12b8a | ||
|
|
425d140161 | ||
|
|
64d8f7843a | ||
|
|
a0a6427540 | ||
|
|
31fe8e6582 | ||
|
|
38e42919b7 | ||
|
|
c769f95688 | ||
|
|
d642897a5b | ||
|
|
d7931f8ec2 | ||
|
|
8a0b989718 | ||
|
|
4fbbb646c3 | ||
|
|
75856e63f6 | ||
|
|
98213f50db | ||
|
|
5bce1db24e | ||
|
|
380ace5780 | ||
|
|
6e5b01a3d4 | ||
|
|
622e8e8af3 | ||
|
|
2bbb83d3f2 | ||
|
|
54756134d4 | ||
|
|
932b212e04 | ||
|
|
3b40e5b20c | ||
|
|
f594db207d | ||
|
|
70e23427e8 | ||
|
|
c1a303fd3d | ||
|
|
a62b9a5e1a | ||
|
|
36eece311a | ||
|
|
29ea5f5787 | ||
|
|
c00e3aacfc | ||
|
|
cf9f78528c | ||
|
|
68d4795de6 | ||
|
|
e689d7f7d2 | ||
|
|
a607f868c2 | ||
|
|
84ac1c4bad | ||
|
|
e11ff528e2 | ||
|
|
047f4d1878 | ||
|
|
0294c33baf | ||
|
|
11a8b6e40b | ||
|
|
cade86b62a | ||
|
|
df836ec1c6 | ||
|
|
12cc00a3d3 | ||
|
|
24aa81f0be | ||
|
|
339706a3a6 | ||
|
|
b43c9adb7a | ||
|
|
c2783039d4 | ||
|
|
c4706e4123 | ||
|
|
8a997540b3 | ||
|
|
045022b22a | ||
|
|
723fa4b3d8 | ||
|
|
41b59cff06 | ||
|
|
bed1b46527 | ||
|
|
ad695ca6e8 | ||
|
|
33e997708c | ||
|
|
56b6ee1d38 | ||
|
|
27b2cf52a5 | ||
|
|
b532130f6e | ||
|
|
d16b8594ad | ||
|
|
ad8442c6de | ||
|
|
4edf7e2c2c | ||
|
|
ea49318809 | ||
|
|
a9a86aba61 | ||
|
|
6e95d5366c | ||
|
|
445711e1cb | ||
|
|
dfd2096fe5 | ||
|
|
d469c6f287 | ||
|
|
9655b941f3 | ||
|
|
4254fdfd8c | ||
|
|
1b3cd7e2e2 | ||
|
|
897498b7f5 | ||
|
|
34770e4463 | ||
|
|
9d14f72249 | ||
|
|
87f6e81ffc | ||
|
|
c3373e141a | ||
|
|
a5f2d97b04 | ||
|
|
80ac6a5ae9 | ||
|
|
496475e0ca | ||
|
|
982dbbccdf | ||
|
|
3f9c20c60b | ||
|
|
cabb3c6c45 | ||
|
|
03bf1fdcfe | ||
|
|
f36f8d1bcc | ||
|
|
5c2c1770a2 | ||
|
|
6810af1e1d | ||
|
|
78ba6ce973 | ||
|
|
15bcb7e374 | ||
|
|
7dd7ccbff5 | ||
|
|
5b17c8de71 | ||
|
|
5cf4ff66a3 | ||
|
|
b6be8a178e | ||
|
|
b77c3b2d0c | ||
|
|
e4a210b47c | ||
|
|
6bf10aafb7 | ||
|
|
e15d544341 | ||
|
|
acdb5787db | ||
|
|
18f0c9b500 | ||
|
|
b36e721274 | ||
|
|
9fdc50cd0e | ||
|
|
41abf077bc | ||
|
|
27a4e5a55b | ||
|
|
65f6a104e9 | ||
|
|
415bd07c0d | ||
|
|
3fd26dd937 | ||
|
|
f5f5d93b64 | ||
|
|
b497bb8c83 | ||
|
|
b0d554eacb | ||
|
|
cbecc7b930 | ||
|
|
5e0921aca9 | ||
|
|
7e8015e828 | ||
|
|
bef5bdf0bf | ||
|
|
c04cd5e83e | ||
|
|
30d3c1bbce | ||
|
|
5e72461391 | ||
|
|
54fdf71d91 | ||
|
|
420d0cfdc4 | ||
|
|
84bfba7a82 | ||
|
|
9fd89a6822 | ||
|
|
4a02dde83f | ||
|
|
e93ee1ffec | ||
|
|
e2b6fb12c7 | ||
|
|
7836073c7e | ||
|
|
3119626d89 | ||
|
|
19bebdd923 | ||
|
|
0b0dd8b552 | ||
|
|
47ce7a633f | ||
|
|
ca32f68787 | ||
|
|
0b972ad302 | ||
|
|
9b4b1526b1 | ||
|
|
7a232c7a4a | ||
|
|
983351f0b7 | ||
|
|
16fb5ac121 | ||
|
|
bb1fbca4a7 | ||
|
|
b7c0b6b8e0 | ||
|
|
485aa62755 | ||
|
|
53e2a86dd9 | ||
|
|
312095d1df | ||
|
|
b498be1092 | ||
|
|
211ea8427f | ||
|
|
407eb69568 | ||
|
|
8a44b4d6ee | ||
|
|
bc58fbb741 | ||
|
|
0c977f5fd7 | ||
|
|
7eeccbcb14 | ||
|
|
020d2a5687 | ||
|
|
83d61fcffd | ||
|
|
c0b222a5fa | ||
|
|
236e4ea9aa | ||
|
|
0622e16d18 | ||
|
|
159ca84e46 | ||
|
|
142a61ce5c | ||
|
|
7f226af541 | ||
|
|
8bf912499a | ||
|
|
a55b10cfa3 | ||
|
|
1a4f889b40 | ||
|
|
f9bc2de4e4 | ||
|
|
5a1c635083 | ||
|
|
76e8567f1e | ||
|
|
9bd349d933 | ||
|
|
5e8a67b605 | ||
|
|
e16799d500 | ||
|
|
a189846194 | ||
|
|
fcd536aada | ||
|
|
c89bafc2c9 | ||
|
|
f5855a9f9a | ||
|
|
a49b4bccc6 | ||
|
|
b434da29b1 | ||
|
|
514b3a5afe | ||
|
|
0d30f81ddb | ||
|
|
9a86c00f62 | ||
|
|
7648138902 | ||
|
|
2055f092f2 | ||
|
|
9ff7f4baba | ||
|
|
92ba99c34c | ||
|
|
876cff4daf | ||
|
|
1ac6a612b0 | ||
|
|
15ecad9f87 | ||
|
|
587d3f7c7e | ||
|
|
10e4ea6743 | ||
|
|
d1601bf2fe | ||
|
|
2994945c64 | ||
|
|
c9e3cad738 | ||
|
|
7c36964812 | ||
|
|
0e02d13c67 | ||
|
|
f7aeea2f3d | ||
|
|
b2da7d4cae | ||
|
|
239f9769c2 | ||
|
|
f5947518b1 | ||
|
|
1a4afa406b | ||
|
|
412b879f39 | ||
|
|
a830346545 | ||
|
|
fbb8320a25 | ||
|
|
14f4a0f701 | ||
|
|
e82e2817d5 | ||
|
|
ffbd1f9aeb | ||
|
|
5ab418a3cf | ||
|
|
a58e00b206 | ||
|
|
a74682bbf6 | ||
|
|
11142253fb | ||
|
|
ef7782167f | ||
|
|
f4a2682e6c | ||
|
|
35cee22cf6 | ||
|
|
fbb55228f2 | ||
|
|
391ac00d81 | ||
|
|
277b744ca3 | ||
|
|
a89c67a50e | ||
|
|
26b30a7b22 | ||
|
|
4dae23d3bb | ||
|
|
07e6c3f977 | ||
|
|
dace63d9d2 | ||
|
|
2ebf956599 | ||
|
|
b20793c67a | ||
|
|
47e9f59cc8 | ||
|
|
e27cac7fef | ||
|
|
5bfda6e2bc | ||
|
|
ef2ab7df48 | ||
|
|
ac1d9147d2 | ||
|
|
f2350909d2 | ||
|
|
f14ef93808 | ||
|
|
45bd4252bf | ||
|
|
6b4456bf0e | ||
|
|
c5e114dc7f | ||
|
|
30ceea4287 | ||
|
|
380f9ff013 | ||
|
|
19ac119714 | ||
|
|
236f70183c | ||
|
|
117bc35653 | ||
|
|
4fcaa8d3d6 | ||
|
|
536889d3df | ||
|
|
bbd13c04cc | ||
|
|
82e4ccb227 | ||
|
|
626cfa474f | ||
|
|
18e9a9afd3 | ||
|
|
41b7d5a3a0 | ||
|
|
16fcd4c639 | ||
|
|
ef3641efa6 | ||
|
|
8d95a32672 | ||
|
|
3a3a718779 | ||
|
|
3d1955211a | ||
|
|
8d87715d6f | ||
|
|
3c535b8e99 | ||
|
|
2c6affecea | ||
|
|
c2d2169a9f | ||
|
|
1153c5ff17 | ||
|
|
6c532f5926 | ||
|
|
7083394bc9 | ||
|
|
7c58410868 | ||
|
|
00c3e3b713 | ||
|
|
9d4a72766d | ||
|
|
82e16b4438 | ||
|
|
56353f2d0a | ||
|
|
4d0eb94a6f | ||
|
|
e1a494ecbd | ||
|
|
6b1e34da63 | ||
|
|
ccf9597102 | ||
|
|
5a6f4b9e1c | ||
|
|
9b09b42f97 | ||
|
|
854345e16f | ||
|
|
e0ee865b87 | ||
|
|
dad0c01335 | ||
|
|
79ef5af19b | ||
|
|
b349959f93 | ||
|
|
2e7f9612af | ||
|
|
8ff2303b22 | ||
|
|
b681fdd6d6 | ||
|
|
b65b3b438c | ||
|
|
580d6bab36 | ||
|
|
90349ddd7d | ||
|
|
dcac421bc0 | ||
|
|
b4f643577f | ||
|
|
411e7168b3 | ||
|
|
fef072a62a | ||
|
|
f529e9cb23 | ||
|
|
cfa3bfd88c | ||
|
|
321c99f12b | ||
|
|
73ad4992ee | ||
|
|
ddbf37c1be | ||
|
|
b9392371c7 | ||
|
|
d3c26a1548 | ||
|
|
31c2a61cce | ||
|
|
f84ba9768b | ||
|
|
1faa935527 | ||
|
|
5f940ff309 | ||
|
|
4c4c0ea0ba | ||
|
|
787b40a99e | ||
|
|
fd6a0ae747 | ||
|
|
298a32c096 | ||
|
|
aecff5ffd6 | ||
|
|
c1a6b7b787 | ||
|
|
0903f19f9c | ||
|
|
51aa23817a | ||
|
|
8f3f385cb6 | ||
|
|
915274081d | ||
|
|
a388c52b3f | ||
|
|
b4d3cd4d4d | ||
|
|
50c03b0675 | ||
|
|
fa3bb96417 | ||
|
|
09bde57835 | ||
|
|
76ac2a8843 | ||
|
|
f6ec6962ab | ||
|
|
28ad6829cd | ||
|
|
7f4b002a87 | ||
|
|
7e073b6ff4 | ||
|
|
fa3781efe5 | ||
|
|
bec74d85cd | ||
|
|
abc3829c64 | ||
|
|
18f5d6eab9 | ||
|
|
00f3e30930 | ||
|
|
97cd21d004 | ||
|
|
09b4d44f23 | ||
|
|
3536bf56bd | ||
|
|
f8eaf5def0 | ||
|
|
6077f85e52 | ||
|
|
e2976a3859 | ||
|
|
1e25fde22e | ||
|
|
55d88b7dae | ||
|
|
de30f8917f | ||
|
|
52653fa005 | ||
|
|
4628358add | ||
|
|
117b08a73e | ||
|
|
700888a8e0 | ||
|
|
ef882927f3 | ||
|
|
af9327de14 | ||
|
|
2881d42bf5 | ||
|
|
dc3a49fe57 | ||
|
|
addabd6396 | ||
|
|
3341c641cc | ||
|
|
363413e1e6 | ||
|
|
b675d27a30 | ||
|
|
796023408a | ||
|
|
983a8512b2 | ||
|
|
6593102632 | ||
|
|
65fff13150 | ||
|
|
edd1a140d7 | ||
|
|
18070baad4 | ||
|
|
acf729f6e7 | ||
|
|
6dbc8eac03 | ||
|
|
35944bcbdc | ||
|
|
3f919f91c1 | ||
|
|
443a20d83d | ||
|
|
2fca26eaae | ||
|
|
ebc8141971 | ||
|
|
5d6bcc9b9b | ||
|
|
55fca332ba | ||
|
|
6b65c5fe69 | ||
|
|
3e4dbe1015 | ||
|
|
20197e64b2 | ||
|
|
94eecaf448 | ||
|
|
fa91e0e79b | ||
|
|
891adc38fc | ||
|
|
af6cc63db2 | ||
|
|
af73e14b64 | ||
|
|
9305fe7875 | ||
|
|
613fde4639 | ||
|
|
61db2c898b | ||
|
|
acf313c420 | ||
|
|
15fca08641 | ||
|
|
e2cbe3c1f8 | ||
|
|
d3883ea3ae | ||
|
|
8b2c4b3e60 | ||
|
|
65d0d00591 | ||
|
|
97a57c2f6e | ||
|
|
6559b2ff27 | ||
|
|
4c1deeb899 | ||
|
|
a65ea6805d | ||
|
|
effe65b034 | ||
|
|
37296cf048 | ||
|
|
1b597c1301 | ||
|
|
c2454d0689 | ||
|
|
9b60b44554 | ||
|
|
75516bdafb | ||
|
|
12f5a487c1 | ||
|
|
8d128d5035 | ||
|
|
cfa7117e64 | ||
|
|
7880bf0dc1 | ||
|
|
0054041829 | ||
|
|
99931f785a | ||
|
|
5e121269f0 | ||
|
|
38ced0243f | ||
|
|
869db878e1 | ||
|
|
e6c6e355e1 | ||
|
|
6221b9a5fd | ||
|
|
5f2c9c935b | ||
|
|
76559b253c | ||
|
|
3c54655c39 | ||
|
|
7a851ac199 | ||
|
|
b2ba5dfcd1 | ||
|
|
4a4fae8f8c | ||
|
|
de894ce7b2 | ||
|
|
09c4a955c9 | ||
|
|
db1581a0a2 | ||
|
|
db9d7b3060 | ||
|
|
7e0c29472e | ||
|
|
d13492070d | ||
|
|
695ede51ea | ||
|
|
168f382aa6 | ||
|
|
5bd433318d | ||
|
|
d1cd2a793e | ||
|
|
5a4464f338 | ||
|
|
561d25320b | ||
|
|
b225c2dd3b | ||
|
|
2a2e357513 | ||
|
|
28bfe1ecb8 | ||
|
|
cc12f0af7e | ||
|
|
da831a1b08 | ||
|
|
eb97be17dd | ||
|
|
2dd1c9b2ad | ||
|
|
41191db863 | ||
|
|
ee20204b22 | ||
|
|
f1032804bb | ||
|
|
ba1540d75b | ||
|
|
f5c87f80e1 | ||
|
|
d2d7603ff5 | ||
|
|
56013dca48 | ||
|
|
d33ed4a69f | ||
|
|
ed753b5564 | ||
|
|
7e65552d01 |
@@ -4,18 +4,16 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
"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",
|
"postCreateCommand": "./scripts/setup-envs.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.analysis.diagnosticMode": "workspace",
|
"python.analysis.diagnosticMode": "workspace",
|
||||||
"python.analysis.typeCheckingMode": "basic",
|
|
||||||
"ruff.organizeImports": false,
|
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.ruff": true,
|
"source.fixAll.ruff": "explicit",
|
||||||
"source.organizeImports": true
|
"source.organizeImports": "explicit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
@@ -44,8 +42,6 @@
|
|||||||
"extensions": [
|
"extensions": [
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
"ms-python.isort",
|
|
||||||
"ms-python.black-formatter",
|
|
||||||
"charliermarsh.ruff",
|
"charliermarsh.ruff",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
|||||||
open_collective: nonebot
|
open_collective: nonebot
|
||||||
custom: ["https://afdian.net/@nonebot"]
|
custom: ["https://afdian.com/@nonebot"]
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: 发布适配器
|
name: 发布适配器
|
||||||
title: "Adapter: {name}"
|
title: "Adapter: {name}"
|
||||||
description: 发布适配器到 NoneBot 官方商店
|
description: 发布适配器到 NoneBot 官方商店
|
||||||
labels: ["Adapter"]
|
labels: ["Adapter", "Publish"]
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
id: name
|
id: name
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: 发布机器人
|
name: 发布机器人
|
||||||
title: "Bot: {name}"
|
title: "Bot: {name}"
|
||||||
description: 发布机器人到 NoneBot 官方商店
|
description: 发布机器人到 NoneBot 官方商店
|
||||||
labels: ["Bot"]
|
labels: ["Bot", "Publish"]
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
id: name
|
id: name
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
2
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: 发布插件
|
name: 发布插件
|
||||||
title: "Plugin: {name}"
|
title: "Plugin: {name}"
|
||||||
description: 发布插件到 NoneBot 官方商店
|
description: 发布插件到 NoneBot 官方商店
|
||||||
labels: ["Plugin"]
|
labels: ["Plugin", "Publish"]
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
id: pypi
|
id: pypi
|
||||||
|
|||||||
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@@ -4,7 +4,7 @@ description: Setup Node
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|||||||
22
.github/actions/setup-python/action.yml
vendored
22
.github/actions/setup-python/action.yml
vendored
@@ -6,6 +6,14 @@ inputs:
|
|||||||
description: Python version
|
description: Python version
|
||||||
required: false
|
required: false
|
||||||
default: "3.10"
|
default: "3.10"
|
||||||
|
env-dir:
|
||||||
|
description: Environment directory
|
||||||
|
required: false
|
||||||
|
default: "."
|
||||||
|
no-root:
|
||||||
|
description: Do not install package in the environment
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -14,11 +22,19 @@ runs:
|
|||||||
run: pipx install poetry
|
run: pipx install poetry
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
architecture: "x64"
|
|
||||||
cache: "poetry"
|
cache: "poetry"
|
||||||
|
cache-dependency-path: |
|
||||||
|
./poetry.lock
|
||||||
|
${{ inputs.env-dir }}/poetry.lock
|
||||||
|
|
||||||
- run: poetry install -E all
|
- run: |
|
||||||
|
cd ${{ inputs.env-dir }}
|
||||||
|
if [ "${{ inputs.no-root }}" = "true" ]; then
|
||||||
|
poetry install --all-extras --no-root
|
||||||
|
else
|
||||||
|
poetry install --all-extras
|
||||||
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -35,3 +35,12 @@ updates:
|
|||||||
actions:
|
actions:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: devcontainers
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
devcontainers:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|||||||
37
.github/workflows/codecov.yml
vendored
37
.github/workflows/codecov.yml
vendored
@@ -6,25 +6,32 @@ on:
|
|||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- "envs/**"
|
||||||
- "nonebot/**"
|
- "nonebot/**"
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
|
- ".github/actions/setup-python/**"
|
||||||
|
- ".github/workflows/codecov.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "poetry.lock"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Test Coverage
|
name: Test Coverage
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
concurrency:
|
concurrency:
|
||||||
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
|
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.env }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
env: [pydantic-v1, pydantic-v2]
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
PYTHON_VERSION: ${{ matrix.python-version }}
|
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
PYDANTIC_VERSION: ${{ matrix.env }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -33,15 +40,29 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
env-dir: ./envs/${{ matrix.env }}
|
||||||
|
no-root: true
|
||||||
|
|
||||||
- name: Run Pytest
|
- name: Run Pytest
|
||||||
run: |
|
run: |
|
||||||
cd tests/
|
cd ./envs/${{ matrix.env }}
|
||||||
poetry run pytest -n auto --cov-report xml
|
poetry run bash "../../scripts/run-tests.sh"
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: codecov/test-results-action@v1
|
||||||
|
with:
|
||||||
|
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||||
|
files: ./tests/junit.xml
|
||||||
|
flags: unittests
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
env_vars: OS,PYTHON_VERSION
|
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||||
files: ./tests/coverage.xml
|
files: ./tests/coverage.xml
|
||||||
flags: unittests
|
flags: unittests
|
||||||
|
fail_ci_if_error: true
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
49
.github/workflows/noneflow.yml
vendored
49
.github/workflows/noneflow.yml
vendored
@@ -15,9 +15,10 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
plugin_test:
|
noneflow:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: nonebot2 plugin test
|
name: noneflow
|
||||||
|
# do not run on forked PRs, do not run on not related issues, do not run on pr comments
|
||||||
if: |
|
if: |
|
||||||
!(
|
!(
|
||||||
(
|
(
|
||||||
@@ -35,30 +36,6 @@ jobs:
|
|||||||
github.event_name == 'issue_comment' && github.event.issue.pull_request
|
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 }}
|
|
||||||
metadata: ${{ steps.plugin-test.outputs.METADATA }}
|
|
||||||
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.x"
|
|
||||||
|
|
||||||
- name: Test Plugin
|
|
||||||
id: plugin-test
|
|
||||||
run: |
|
|
||||||
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
|
|
||||||
noneflow:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: noneflow
|
|
||||||
needs: plugin_test
|
|
||||||
steps:
|
steps:
|
||||||
- name: Generate token
|
- name: Generate token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
@@ -72,29 +49,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Cache pre-commit hooks
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: .cache/.pre-commit
|
|
||||||
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
|
||||||
|
|
||||||
- name: NoneFlow
|
- name: NoneFlow
|
||||||
uses: docker://ghcr.io/nonebot/noneflow:latest
|
uses: docker://ghcr.io/nonebot/noneflow:latest
|
||||||
with:
|
with:
|
||||||
config: >
|
config: >
|
||||||
{
|
{
|
||||||
"base": "master",
|
"base": "master",
|
||||||
"plugin_path": "website/static/plugins.json",
|
"plugin_path": "assets/plugins.json5",
|
||||||
"bot_path": "website/static/bots.json",
|
"bot_path": "assets/bots.json5",
|
||||||
"adapter_path": "website/static/adapters.json"
|
"adapter_path": "assets/adapters.json5",
|
||||||
|
"registry_repository": "nonebot/registry"
|
||||||
}
|
}
|
||||||
env:
|
env:
|
||||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
|
||||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
|
||||||
PLUGIN_TEST_METADATA: ${{ needs.plugin_test.outputs.metadata }}
|
|
||||||
APP_ID: ${{ secrets.APP_ID }}
|
APP_ID: ${{ secrets.APP_ID }}
|
||||||
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
||||||
PRE_COMMIT_HOME: /github/workspace/.cache/.pre-commit
|
|
||||||
|
|
||||||
- name: Fix permission
|
|
||||||
run: sudo chown -R $(whoami):$(id -ng) .cache/.pre-commit
|
|
||||||
|
|||||||
25
.github/workflows/pyright.yml
vendored
25
.github/workflows/pyright.yml
vendored
@@ -6,21 +6,42 @@ on:
|
|||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- "envs/**"
|
||||||
- "nonebot/**"
|
- "nonebot/**"
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
|
- ".github/actions/setup-python/**"
|
||||||
|
- ".github/workflows/pyright.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "poetry.lock"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pyright:
|
pyright:
|
||||||
name: Pyright Lint
|
name: Pyright Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pyright-${{ github.ref }}-${{ matrix.env }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
env: [pydantic-v1, pydantic-v2]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
with:
|
||||||
|
env-dir: ./envs/${{ matrix.env }}
|
||||||
|
no-root: true
|
||||||
|
|
||||||
- run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH
|
- run: |
|
||||||
|
(cd ./envs/${{ matrix.env }} && echo "$(poetry env info --path)/bin" >> $GITHUB_PATH)
|
||||||
|
if [ "${{ matrix.env }}" = "pydantic-v1" ]; then
|
||||||
|
sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Run Pyright
|
- name: Run Pyright
|
||||||
uses: jakebailey/pyright-action@v1
|
uses: jakebailey/pyright-action@v2
|
||||||
|
|||||||
8
.github/workflows/release-drafter.yml
vendored
8
.github/workflows/release-drafter.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Setup Node Environment
|
- name: Setup Node Environment
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v6
|
||||||
id: release-drafter
|
id: release-drafter
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Update Changelog
|
- name: Update Changelog
|
||||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||||
with:
|
with:
|
||||||
changelog_file: website/src/pages/changelog.md
|
changelog_file: website/src/changelog/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## )'
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
|
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v6
|
||||||
with:
|
with:
|
||||||
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
||||||
tag: ${{ steps.version.outputs.TAG_NAME }}
|
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
- name: Publish Doc Package to PyPI
|
- name: Publish Doc Package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
packages-dir: packages/nonebot-plugin-docs/
|
packages-dir: packages/nonebot-plugin-docs/dist/
|
||||||
|
|
||||||
- name: Publish Doc Package to GitHub
|
- name: Publish Doc Package to GitHub
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Archive Changelog
|
- name: Archive Changelog
|
||||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||||
with:
|
with:
|
||||||
changelog_file: website/src/pages/changelog.md
|
changelog_file: website/src/changelog/changelog.md
|
||||||
archive_regex: '(?<=## )最近更新(?=\n)'
|
archive_regex: '(?<=## )最近更新(?=\n)'
|
||||||
archive_title: ${{ env.TAG_NAME }}
|
archive_title: ${{ env.TAG_NAME }}
|
||||||
commit_and_push: false
|
commit_and_push: false
|
||||||
|
|||||||
9
.github/workflows/ruff.yml
vendored
9
.github/workflows/ruff.yml
vendored
@@ -6,14 +6,23 @@ on:
|
|||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- "envs/**"
|
||||||
- "nonebot/**"
|
- "nonebot/**"
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
|
- ".github/actions/setup-python/**"
|
||||||
|
- ".github/workflows/ruff.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "poetry.lock"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ruff:
|
ruff:
|
||||||
name: Ruff Lint
|
name: Ruff Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pyright-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/website-deploy.yml
vendored
2
.github/workflows/website-deploy.yml
vendored
@@ -33,7 +33,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@v2
|
uses: nwtgck/actions-netlify@v3
|
||||||
with:
|
with:
|
||||||
publish-dir: "./website/build"
|
publish-dir: "./website/build"
|
||||||
production-deploy: true
|
production-deploy: true
|
||||||
|
|||||||
104
.github/workflows/website-preview-cd.yml
vendored
Normal file
104
.github/workflows/website-preview-cd.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
name: Site Deploy (Preview CD)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Site Deploy (Preview CI)"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
preview-cd:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pull-request-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
|
||||||
|
environment: pull request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
statuses: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set Commit Status
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.repos.createCommitStatus({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
sha: context.payload.workflow_run.head_sha,
|
||||||
|
context: 'Website Preview',
|
||||||
|
description: 'Deploying...',
|
||||||
|
state: 'pending',
|
||||||
|
})
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: website-preview
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|
||||||
|
- name: Restore Context
|
||||||
|
run: |
|
||||||
|
PR_NUMBER=$(cat ./pr-number)
|
||||||
|
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Invalid PR number: ${PR_NUMBER}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PR_NUMBER=${PR_NUMBER}" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Set Deploy Name
|
||||||
|
run: |
|
||||||
|
echo "DEPLOY_NAME=deploy-preview-${PR_NUMBER}" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
id: deploy
|
||||||
|
uses: nwtgck/actions-netlify@v3
|
||||||
|
with:
|
||||||
|
publish-dir: ./website/build
|
||||||
|
production-deploy: false
|
||||||
|
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.event.workflow_run.head_sha }}"
|
||||||
|
alias: ${{ env.DEPLOY_NAME }}
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
||||||
|
|
||||||
|
# action netlify has no pull request context, so we need to comment by ourselves
|
||||||
|
- name: Comment on Pull Request
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: website
|
||||||
|
number: ${{ env.PR_NUMBER }}
|
||||||
|
message: |
|
||||||
|
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
|
||||||
|
|
||||||
|
- name: Set Commit Status
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
if (`${{ job.status }}` === 'success') {
|
||||||
|
github.rest.repos.createCommitStatus({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
sha: context.payload.workflow_run.head_sha,
|
||||||
|
context: 'Website Preview',
|
||||||
|
description: `Deployed to ${{ steps.deploy.outputs.deploy-url }}`,
|
||||||
|
state: 'success',
|
||||||
|
target_url: `${{ steps.deploy.outputs.deploy-url }}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
github.rest.repos.createCommitStatus({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
sha: context.payload.workflow_run.head_sha,
|
||||||
|
context: 'Website Preview',
|
||||||
|
description: `Deploy ${{ job.status }}`,
|
||||||
|
state: 'failure',
|
||||||
|
})
|
||||||
|
}
|
||||||
42
.github/workflows/website-preview-ci.yml
vendored
Normal file
42
.github/workflows/website-preview-ci.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Site Deploy (Preview CI)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
preview-ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pull-request-preview-${{ github.event.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python Environment
|
||||||
|
uses: ./.github/actions/setup-python
|
||||||
|
|
||||||
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
|
- name: Build API Doc
|
||||||
|
uses: ./.github/actions/build-api-doc
|
||||||
|
|
||||||
|
- name: Build Doc
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Export Context
|
||||||
|
run: |
|
||||||
|
echo "${{ github.event.pull_request.number }}" > ./pr-number
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: website-preview
|
||||||
|
path: |
|
||||||
|
./website/build
|
||||||
|
./pr-number
|
||||||
|
retention-days: 1
|
||||||
46
.github/workflows/website-preview.yml
vendored
46
.github/workflows/website-preview.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
name: Site Deploy(Preview)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
preview:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: pull-request-preview-${{ github.event.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Python Environment
|
|
||||||
uses: ./.github/actions/setup-python
|
|
||||||
|
|
||||||
- name: Setup Node Environment
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
|
|
||||||
- name: Build API Doc
|
|
||||||
uses: ./.github/actions/build-api-doc
|
|
||||||
|
|
||||||
- name: Build Doc
|
|
||||||
run: yarn build
|
|
||||||
|
|
||||||
- name: Get Deploy Name
|
|
||||||
run: |
|
|
||||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
uses: nwtgck/actions-netlify@v2
|
|
||||||
with:
|
|
||||||
publish-dir: "./website/build"
|
|
||||||
production-deploy: false
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
|
|
||||||
enable-commit-comment: false
|
|
||||||
alias: ${{ env.DEPLOY_NAME }}
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ docs_build/_build
|
|||||||
!tests/.env
|
!tests/.env
|
||||||
.docusaurus
|
.docusaurus
|
||||||
website/docs/api/**/*.md
|
website/docs/api/**/*.md
|
||||||
|
website/src/pages/changelog/**/*
|
||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
|
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||||
|
|||||||
@@ -7,30 +7,13 @@ ci:
|
|||||||
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/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.0.291
|
rev: v0.8.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix]
|
||||||
stages: [commit]
|
stages: [pre-commit]
|
||||||
|
- id: ruff-format
|
||||||
- repo: https://github.com/pycqa/isort
|
stages: [pre-commit]
|
||||||
rev: 5.12.0
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
stages: [commit]
|
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 23.9.1
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
stages: [commit]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
||||||
rev: v3.0.3
|
|
||||||
hooks:
|
|
||||||
- id: prettier
|
|
||||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
|
||||||
stages: [commit]
|
|
||||||
|
|
||||||
- repo: https://github.com/nonebot/nonemoji
|
- repo: https://github.com/nonebot/nonemoji
|
||||||
rev: v0.1.4
|
rev: v0.1.4
|
||||||
|
|||||||
14
.prettierrc
14
.prettierrc
@@ -5,5 +5,17 @@
|
|||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"semi": true
|
"semi": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/devcontainer.json",
|
||||||
|
"**/tsconfig.json",
|
||||||
|
"**/tsconfig.*.json"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"parser": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>
|
See [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog>
|
||||||
|
|||||||
26
CITATION.cff
Normal file
26
CITATION.cff
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# This CITATION.cff file was generated with cffinit.
|
||||||
|
# Visit https://bit.ly/cffinit to generate yours today!
|
||||||
|
|
||||||
|
cff-version: 1.2.0
|
||||||
|
title: NoneBot
|
||||||
|
message: >-
|
||||||
|
If you use this software, please cite it using the
|
||||||
|
metadata from this file.
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- given-names: Yongyu
|
||||||
|
family-names: Yan
|
||||||
|
email: yyy@nonebot.dev
|
||||||
|
- name: NoneBot Team
|
||||||
|
email: contact@nonebot.dev
|
||||||
|
website: 'https://github.com/nonebot'
|
||||||
|
repository-code: 'https://github.com/nonebot/nonebot2'
|
||||||
|
url: 'https://nonebot.dev/'
|
||||||
|
abstract: >-
|
||||||
|
NoneBot, an asynchronous multi-platform chatbot framework
|
||||||
|
written in Python
|
||||||
|
keywords:
|
||||||
|
- nonebot
|
||||||
|
- chatbot
|
||||||
|
- pydantic
|
||||||
|
license: MIT
|
||||||
98
README.md
98
README.md
@@ -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?logo=python&logoColor=edb641" alt="pypi">
|
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
|
<img src="https://img.shields.io/badge/python-3.9+-blue?logo=python&logoColor=edb641" alt="python">
|
||||||
<a href="https://github.com/psf/black">
|
<a href="https://github.com/psf/black">
|
||||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
||||||
</a>
|
</a>
|
||||||
@@ -54,6 +54,9 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<a href="https://onebot.dev/">
|
<a href="https://onebot.dev/">
|
||||||
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
<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://bot.q.qq.com/wiki/">
|
||||||
|
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
|
||||||
|
</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" alt="telegram">
|
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||||
</a>
|
</a>
|
||||||
@@ -63,9 +66,6 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<a href="https://docs.github.com/en/developers/apps">
|
<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"/>
|
<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/">
|
|
||||||
<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>
|
|
||||||
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
<!-- <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"> -->
|
<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>
|
||||||
@@ -94,7 +94,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://asciinema.org/a/569440">
|
<a href="https://asciinema.org/a/569440">
|
||||||
<img src="https://nonebot.dev/img/setup.svg">
|
<img src="https://nonebot.dev/img/setup.svg" alt="setup" >
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -110,24 +110,30 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
|||||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||||
|
|
||||||
| 协议名称 | 状态 | 注释 |
|
| 协议名称 | 状态 | 注释 |
|
||||||
| :--------------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
||||||
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
||||||
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
||||||
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
||||||
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
||||||
| QQ 频道([仓库](https://github.com/nonebot/adapter-qqguild),[协议](https://bot.q.qq.com/wiki/)) | ✅ | 官方接口调整较多 |
|
| QQ([仓库](https://github.com/nonebot/adapter-qq),[协议](https://bot.q.qq.com/wiki/)) | ✅ | QQ 官方接口调整较多 |
|
||||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
||||||
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
| Red([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
||||||
| Red ([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
| Satori([仓库](https://github.com/nonebot/adapter-satori),[协议](https://satori.js.org/zh-CN)) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 |
|
||||||
| Discord ([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
| Discord([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
||||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
| DoDo([仓库](https://github.com/nonebot/adapter-dodo),[协议](https://open.imdodo.com/)) | ✅ | DoDo Bot 协议 |
|
||||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
| Kritor([仓库](https://github.com/nonebot/adapter-kritor),[协议](https://github.com/KarinJS/kritor)) | ✅ | Kritor (OnebotX) 协议,QQ 机器人接口标准 |
|
||||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
| Mirai([仓库](https://github.com/nonebot/adapter-mirai),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ✅ | QQ 协议 |
|
||||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||||
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||||
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa),[协议](https://webstatic.mihoyo.com/vila/bot/doc/)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||||
|
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||||
|
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
||||||
|
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ❌ | 米游社大别野 Bot 协议,官方已下线 |
|
||||||
|
| Rocket.Chat([仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat),[协议](https://developer.rocket.chat/)) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 |
|
||||||
|
| Tailchat([仓库](https://github.com/eya46/nonebot-adapter-tailchat),[协议](https://tailchat.msgbyte.com/)) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 |
|
||||||
|
| Mail([仓库](https://github.com/mobyw/nonebot-adapter-mail)) | ↗️ | 邮件收发协议,由社区贡献 |
|
||||||
|
|
||||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||||
|
|
||||||
@@ -230,10 +236,52 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
### 赞助者
|
### 赞助者
|
||||||
|
|
||||||
|
感谢以下产品对 NoneBot 项目提供的赞助:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/github-dark.png">
|
||||||
|
<img src="https://assets.nonebot.dev/github-light.png" height="50" alt="GitHub">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.netlify.com/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/netlify-dark.svg">
|
||||||
|
<img src="https://assets.nonebot.dev/netlify-light.svg" height="50" alt="netlify">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://sentry.io/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/sentry-dark.svg">
|
||||||
|
<img src="https://assets.nonebot.dev/sentry-light.svg" height="50" alt="sentry">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.docker.com/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/docker-dark.svg">
|
||||||
|
<img src="https://assets.nonebot.dev/docker-light.svg" height="50" alt="docker">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.algolia.com/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.nonebot.dev/algolia-dark.svg">
|
||||||
|
<img src="https://assets.nonebot.dev/algolia-light.svg" height="50" alt="algolia">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.jetbrains.com/">
|
||||||
|
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg" height="80" alt="JetBrains" >
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
感谢以下赞助者对 NoneBot 项目提供的资金支持:
|
感谢以下赞助者对 NoneBot 项目提供的资金支持:
|
||||||
|
|
||||||
<a href="https://assets.nonebot.dev/sponsors.svg">
|
<a href="https://assets.nonebot.dev/sponsors.svg">
|
||||||
<img src='https://assets.nonebot.dev/sponsors.svg'/>
|
<img src="https://assets.nonebot.dev/sponsors.svg" alt="sponsors" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### 开发者
|
### 开发者
|
||||||
@@ -241,5 +289,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&max=1000" />
|
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" alt="contributors" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"project_link": "nonebot-adapter-onebot",
|
"project_link": "nonebot-adapter-onebot",
|
||||||
"name": "OneBot V11",
|
"name": "OneBot V11",
|
||||||
"desc": "OneBot V11 协议",
|
"desc": "OneBot V11 协议",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "https://onebot.adapters.nonebot.dev/",
|
"homepage": "https://onebot.adapters.nonebot.dev/",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"project_link": "nonebot-adapter-ding",
|
"project_link": "nonebot-adapter-ding",
|
||||||
"name": "钉钉",
|
"name": "钉钉",
|
||||||
"desc": "钉钉协议",
|
"desc": "钉钉协议",
|
||||||
"author": "Artin",
|
"author_id": 1184028,
|
||||||
"homepage": "https://github.com/nonebot/adapter-ding",
|
"homepage": "https://github.com/nonebot/adapter-ding",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"project_link": "nonebot-adapter-feishu",
|
"project_link": "nonebot-adapter-feishu",
|
||||||
"name": "飞书",
|
"name": "飞书",
|
||||||
"desc": "飞书协议",
|
"desc": "飞书协议",
|
||||||
"author": "StarHeartHunt",
|
"author_id": 14922941,
|
||||||
"homepage": "https://github.com/nonebot/adapter-feishu",
|
"homepage": "https://github.com/nonebot/adapter-feishu",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -34,18 +34,18 @@
|
|||||||
"project_link": "nonebot-adapter-telegram",
|
"project_link": "nonebot-adapter-telegram",
|
||||||
"name": "Telegram",
|
"name": "Telegram",
|
||||||
"desc": "Telegram 协议",
|
"desc": "Telegram 协议",
|
||||||
"author": "j1g5awi",
|
"author_id": 50312681,
|
||||||
"homepage": "https://github.com/nonebot/adapter-telegram",
|
"homepage": "https://github.com/nonebot/adapter-telegram",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"module_name": "nonebot.adapters.qqguild",
|
"module_name": "nonebot.adapters.qq",
|
||||||
"project_link": "nonebot-adapter-qqguild",
|
"project_link": "nonebot-adapter-qq",
|
||||||
"name": "QQ 频道",
|
"name": "QQ",
|
||||||
"desc": "QQ 频道官方机器人",
|
"desc": "QQ 官方机器人",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "https://github.com/nonebot/adapter-qqguild",
|
"homepage": "https://github.com/nonebot/adapter-qq",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
},
|
},
|
||||||
@@ -54,27 +54,27 @@
|
|||||||
"project_link": "nonebot-adapter-kaiheila",
|
"project_link": "nonebot-adapter-kaiheila",
|
||||||
"name": "开黑啦",
|
"name": "开黑啦",
|
||||||
"desc": "开黑啦协议适配",
|
"desc": "开黑啦协议适配",
|
||||||
"author": "Tian-que",
|
"author_id": 37477320,
|
||||||
"homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila",
|
"homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"module_name": "nonebot.adapters.mirai2",
|
"module_name": "nonebot.adapters.mirai",
|
||||||
"project_link": "nonebot_adapter_mirai2",
|
"project_link": "nonebot-adapter-mirai",
|
||||||
"name": "mirai2",
|
"name": "Mirai",
|
||||||
"desc": "为 nonebot2 添加 mirai_api_http2.x的兼容适配器",
|
"desc": "mirai-api-http v2 协议适配",
|
||||||
"author": "ieew",
|
"author_id": 42648639,
|
||||||
"homepage": "https://github.com/ieew/nonebot_adapter_mirai2",
|
"homepage": "https://github.com/nonebot/adapter-mirai",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"module_name": "nonebot.adapters.onebot.v12",
|
"module_name": "nonebot.adapters.onebot.v12",
|
||||||
"project_link": "nonebot-adapter-onebot",
|
"project_link": "nonebot-adapter-onebot",
|
||||||
"name": "OneBot V12",
|
"name": "OneBot V12",
|
||||||
"desc": "OneBot V12 协议",
|
"desc": "OneBot V12 协议",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "https://onebot.adapters.nonebot.dev/",
|
"homepage": "https://onebot.adapters.nonebot.dev/",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
"project_link": "nonebot-adapter-console",
|
"project_link": "nonebot-adapter-console",
|
||||||
"name": "Console",
|
"name": "Console",
|
||||||
"desc": "基于终端的交互式适配器",
|
"desc": "基于终端的交互式适配器",
|
||||||
"author": "Melodyknit",
|
"author_id": 50488999,
|
||||||
"homepage": "https://github.com/nonebot/adapter-console",
|
"homepage": "https://github.com/nonebot/adapter-console",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
"project_link": "nonebot-adapter-github",
|
"project_link": "nonebot-adapter-github",
|
||||||
"name": "GitHub",
|
"name": "GitHub",
|
||||||
"desc": "GitHub APP & OAuth APP integration",
|
"desc": "GitHub APP & OAuth APP integration",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "https://github.com/nonebot/adapter-github",
|
"homepage": "https://github.com/nonebot/adapter-github",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"project_link": "nonebot-adapter-ntchat",
|
"project_link": "nonebot-adapter-ntchat",
|
||||||
"name": "Ntchat",
|
"name": "Ntchat",
|
||||||
"desc": "pc hook的微信客户端适配",
|
"desc": "pc hook的微信客户端适配",
|
||||||
"author": "JustUndertaker",
|
"author_id": 37363867,
|
||||||
"homepage": "https://github.com/JustUndertaker/adapter-ntchat",
|
"homepage": "https://github.com/JustUndertaker/adapter-ntchat",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"project_link": "nonebot-adapter-minecraft",
|
"project_link": "nonebot-adapter-minecraft",
|
||||||
"name": "Minecraft",
|
"name": "Minecraft",
|
||||||
"desc": "MineCraft通信适配,支持Rcon",
|
"desc": "MineCraft通信适配,支持Rcon",
|
||||||
"author": "17TheWord",
|
"author_id": 54731914,
|
||||||
"homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft",
|
"homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
"project_link": "nonebot-adapter-bilibili",
|
"project_link": "nonebot-adapter-bilibili",
|
||||||
"name": "BilibiliLive",
|
"name": "BilibiliLive",
|
||||||
"desc": "b站直播间ws协议",
|
"desc": "b站直播间ws协议",
|
||||||
"author": "wwweww",
|
"author_id": 39620657,
|
||||||
"homepage": "https://github.com/wwweww/adapter-bilibili",
|
"homepage": "https://github.com/wwweww/adapter-bilibili",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"project_link": "nonebot-adapter-walleq",
|
"project_link": "nonebot-adapter-walleq",
|
||||||
"name": "Walle-Q",
|
"name": "Walle-Q",
|
||||||
"desc": "内置 QQ 协议实现",
|
"desc": "内置 QQ 协议实现",
|
||||||
"author": "abrahum",
|
"author_id": 18395948,
|
||||||
"homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq",
|
"homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"project_link": "nonebot-adapter-villa",
|
"project_link": "nonebot-adapter-villa",
|
||||||
"name": "大别野",
|
"name": "大别野",
|
||||||
"desc": "米游社大别野官方Bot适配",
|
"desc": "米游社大别野官方Bot适配",
|
||||||
"author": "CMHopeSunshine",
|
"author_id": 63870437,
|
||||||
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
|
"homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
"project_link": "nonebot-adapter-red",
|
"project_link": "nonebot-adapter-red",
|
||||||
"name": "RedProtocol",
|
"name": "RedProtocol",
|
||||||
"desc": "QQNT RedProtocol 适配",
|
"desc": "QQNT RedProtocol 适配",
|
||||||
"author": "zhaomaoniu",
|
"author_id": 55650833,
|
||||||
"homepage": "https://github.com/nonebot/adapter-red",
|
"homepage": "https://github.com/nonebot/adapter-red",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -184,9 +184,79 @@
|
|||||||
"project_link": "nonebot-adapter-discord",
|
"project_link": "nonebot-adapter-discord",
|
||||||
"name": "Discord",
|
"name": "Discord",
|
||||||
"desc": "Discord 官方 Bot 协议适配",
|
"desc": "Discord 官方 Bot 协议适配",
|
||||||
"author": "CMHopeSunshine",
|
"author_id": 63870437,
|
||||||
"homepage": "https://github.com/nonebot/adapter-discord",
|
"homepage": "https://github.com/nonebot/adapter-discord",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.satori",
|
||||||
|
"project_link": "nonebot-adapter-satori",
|
||||||
|
"name": "Satori",
|
||||||
|
"desc": "Satori 协议适配器",
|
||||||
|
"author_id": 42648639,
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-satori",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "跨平台",
|
||||||
|
"color": "#bf40bf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.dodo",
|
||||||
|
"project_link": "nonebot-adapter-dodo",
|
||||||
|
"name": "DoDo",
|
||||||
|
"desc": "DoDo Bot 协议适配器",
|
||||||
|
"author_id": 63870437,
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-dodo",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.rocketchat",
|
||||||
|
"project_link": "nonebot-adapter-rocketchat",
|
||||||
|
"name": "RocketChat",
|
||||||
|
"desc": "RocketChat adapter for nonebot2",
|
||||||
|
"author_id": 78360471,
|
||||||
|
"homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.kritor",
|
||||||
|
"project_link": "nonebot-adapter-kritor",
|
||||||
|
"name": "Kritor",
|
||||||
|
"desc": "Kritor 协议适配",
|
||||||
|
"author_id": 42648639,
|
||||||
|
"homepage": "https://github.com/nonebot/adapter-kritor",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "QQNT",
|
||||||
|
"color": "#35a7c9"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_adapter_tailchat",
|
||||||
|
"project_link": "nonebot-adapter-tailchat",
|
||||||
|
"name": "Tailchat",
|
||||||
|
"desc": "Tailchat 适配器",
|
||||||
|
"author_id": 61458340,
|
||||||
|
"homepage": "https://github.com/eya46/nonebot-adapter-tailchat",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.mail",
|
||||||
|
"project_link": "nonebot-adapter-mail",
|
||||||
|
"name": "Mail",
|
||||||
|
"desc": "邮件收发协议",
|
||||||
|
"author_id": 44370805,
|
||||||
|
"homepage": "https://github.com/mobyw/nonebot-adapter-mail",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"name": "HarukaBot",
|
"name": "HarukaBot",
|
||||||
"desc": "将B站UP主的动态和直播信息推送至QQ",
|
"desc": "将B站UP主的动态和直播信息推送至QQ",
|
||||||
"author": "SK-415",
|
"author_id": 36433929,
|
||||||
"homepage": "https://github.com/SK-415/HarukaBot",
|
"homepage": "https://github.com/SK-415/HarukaBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Omega Miya",
|
"name": "Omega Miya",
|
||||||
"desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能",
|
"desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能",
|
||||||
"author": "Ailitonia",
|
"author_id": 41713304,
|
||||||
"homepage": "https://github.com/Ailitonia/omega-miya",
|
"homepage": "https://github.com/Ailitonia/omega-miya",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Github Bot",
|
"name": "Github Bot",
|
||||||
"desc": "在QQ获取/处理Github repo/pr/issue",
|
"desc": "在QQ获取/处理Github repo/pr/issue",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "https://github.com/cscs181/QQ-GitHub-Bot",
|
"homepage": "https://github.com/cscs181/QQ-GitHub-Bot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
{
|
{
|
||||||
"name": "YanXiBot",
|
"name": "YanXiBot",
|
||||||
"desc": "动漫资源查找与娱乐机器人",
|
"desc": "动漫资源查找与娱乐机器人",
|
||||||
"author": "Melodyknit",
|
"author_id": 50488999,
|
||||||
"homepage": "https://github.com/Melodyknit/YanXiBot",
|
"homepage": "https://github.com/Melodyknit/YanXiBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
{
|
{
|
||||||
"name": "绪山真寻bot",
|
"name": "绪山真寻bot",
|
||||||
"desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P",
|
"desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P",
|
||||||
"author": "HibiKier",
|
"author_id": 45528451,
|
||||||
"homepage": "https://github.com/HibiKier/zhenxun_bot",
|
"homepage": "https://github.com/HibiKier/zhenxun_bot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ATRI",
|
"name": "ATRI",
|
||||||
"desc": "高性能文爱萝卜子,糅杂了各类有趣小功能",
|
"desc": "高性能文爱萝卜子,糅杂了各类有趣小功能",
|
||||||
"author": "Kyomotoi",
|
"author_id": 37587870,
|
||||||
"homepage": "https://github.com/Kyomotoi/ATRI",
|
"homepage": "https://github.com/Kyomotoi/ATRI",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
{
|
{
|
||||||
"name": "dumbot傻瓜机器人",
|
"name": "dumbot傻瓜机器人",
|
||||||
"desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等,含一键启动及docker容器部署就绪",
|
"desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等,含一键启动及docker容器部署就绪",
|
||||||
"author": "ffreemt",
|
"author_id": 52522252,
|
||||||
"homepage": "https://github.com/ffreemt/koyeb-nb2",
|
"homepage": "https://github.com/ffreemt/koyeb-nb2",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{
|
{
|
||||||
"name": "DicePP",
|
"name": "DicePP",
|
||||||
"desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~",
|
"desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~",
|
||||||
"author": "pear-studio",
|
"author_id": 88259371,
|
||||||
"homepage": "https://github.com/pear-studio/nonebot-dicepp",
|
"homepage": "https://github.com/pear-studio/nonebot-dicepp",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{
|
{
|
||||||
"name": "SetuBot",
|
"name": "SetuBot",
|
||||||
"desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜",
|
"desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜",
|
||||||
"author": "yuban10703",
|
"author_id": 39484884,
|
||||||
"homepage": "https://github.com/yuban10703/setu-nonebot2",
|
"homepage": "https://github.com/yuban10703/setu-nonebot2",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{
|
{
|
||||||
"name": "剑网三bot",
|
"name": "剑网三bot",
|
||||||
"desc": "网络游戏《剑侠情缘三》的群聊机器人,数据使用:www.jx3api.com",
|
"desc": "网络游戏《剑侠情缘三》的群聊机器人,数据使用:www.jx3api.com",
|
||||||
"author": "JustUndertaker",
|
"author_id": 37363867,
|
||||||
"homepage": "https://github.com/JustUndertaker/mini_jx3_bot",
|
"homepage": "https://github.com/JustUndertaker/mini_jx3_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
{
|
{
|
||||||
"name": "PixivBot",
|
"name": "PixivBot",
|
||||||
"desc": "顾名思义是Pixiv的bot(随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画)",
|
"desc": "顾名思义是Pixiv的bot(随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画)",
|
||||||
"author": "ssttkkl",
|
"author_id": 17331698,
|
||||||
"homepage": "https://github.com/ssttkkl/PixivBot",
|
"homepage": "https://github.com/ssttkkl/PixivBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
{
|
{
|
||||||
"name": "SeaBot_QQ",
|
"name": "SeaBot_QQ",
|
||||||
"desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。",
|
"desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。",
|
||||||
"author": "B1ue1nWh1te",
|
"author_id": 31682561,
|
||||||
"homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ",
|
"homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
"name": "琪露诺Bot",
|
"name": "琪露诺Bot",
|
||||||
"desc": "用QQ机器人控制Minecraft服务器!服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能,三步即可成功部署的QQ bot",
|
"desc": "用QQ机器人控制Minecraft服务器!服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能,三步即可成功部署的QQ bot",
|
||||||
"author": "summerkirakira",
|
"author_id": 56951617,
|
||||||
"homepage": "https://github.com/summerkirakira/CirnoBot",
|
"homepage": "https://github.com/summerkirakira/CirnoBot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Inkar Suki",
|
"name": "Inkar Suki",
|
||||||
"desc": "一个十分方便的Bot,支持包括Webhook、群管、剑网3等一系列功能,持续更新中……",
|
"desc": "一个十分方便的Bot,支持包括Webhook、群管、剑网3等一系列功能,持续更新中……",
|
||||||
"author": "HornCopper",
|
"author_id": 68726147,
|
||||||
"homepage": "https://github.com/HornCopper/Inkar-Suki",
|
"homepage": "https://github.com/HornCopper/Inkar-Suki",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
{
|
{
|
||||||
"name": "屑岛风Bot",
|
"name": "屑岛风Bot",
|
||||||
"desc": "自家用屑Bot",
|
"desc": "自家用屑Bot",
|
||||||
"author": "kexue-z",
|
"author_id": 71873002,
|
||||||
"homepage": "https://github.com/kexue-z/Dao-bot",
|
"homepage": "https://github.com/kexue-z/Dao-bot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
{
|
{
|
||||||
"name": "LiteyukiBot-轻雪机器人",
|
"name": "LiteyukiBot-轻雪机器人",
|
||||||
"desc": "一个有各种琐事功能的bot,有AI接口,能陪聊",
|
"desc": "一个有各种琐事功能的bot,有AI接口,能陪聊",
|
||||||
"author": "snowyfirefly",
|
"author_id": 79104275,
|
||||||
"homepage": "https://github.com/snowyfirefly/Liteyuki",
|
"homepage": "https://github.com/snowyfirefly/Liteyuki",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nya_bot",
|
"name": "nya_bot",
|
||||||
"desc": "喵服——战魂铭人联机服务器兼机器人",
|
"desc": "喵服——战魂铭人联机服务器兼机器人",
|
||||||
"author": "nikissXI",
|
"author_id": 31379266,
|
||||||
"homepage": "https://github.com/nikissXI/nya_bot",
|
"homepage": "https://github.com/nikissXI/nya_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
{
|
{
|
||||||
"name": "真宵Bot",
|
"name": "真宵Bot",
|
||||||
"desc": "专注群聊的QQ机器人",
|
"desc": "专注群聊的QQ机器人",
|
||||||
"author": "Shine-Light",
|
"author_id": 71173418,
|
||||||
"homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey",
|
"homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
{
|
{
|
||||||
"name": "SkadiBot",
|
"name": "SkadiBot",
|
||||||
"desc": "明日方舟主题机器人—斯卡蒂",
|
"desc": "明日方舟主题机器人—斯卡蒂",
|
||||||
"author": "yuyuziYYZ",
|
"author_id": 101615359,
|
||||||
"homepage": "https://github.com/yuyuziYYZ/skadi_bot",
|
"homepage": "https://github.com/yuyuziYYZ/skadi_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
{
|
{
|
||||||
"name": "小白机器人",
|
"name": "小白机器人",
|
||||||
"desc": "一个高度依赖数据库的群管理机器人",
|
"desc": "一个高度依赖数据库的群管理机器人",
|
||||||
"author": "SDIJF1521",
|
"author_id": 69745333,
|
||||||
"homepage": "https://github.com/SDIJF1521/qqai",
|
"homepage": "https://github.com/SDIJF1521/qqai",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
{
|
{
|
||||||
"name": "LittlePaimon",
|
"name": "LittlePaimon",
|
||||||
"desc": "小派蒙,多功能原神机器人。",
|
"desc": "小派蒙,多功能原神机器人。",
|
||||||
"author": "CMHopeSunshine",
|
"author_id": 63870437,
|
||||||
"homepage": "https://github.com/CMHopeSunshine/LittlePaimon",
|
"homepage": "https://github.com/CMHopeSunshine/LittlePaimon",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
{
|
{
|
||||||
"name": "IdhagnBot",
|
"name": "IdhagnBot",
|
||||||
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)",
|
"desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)",
|
||||||
"author": "su226",
|
"author_id": 17371317,
|
||||||
"homepage": "https://github.com/su226/IdhagnBot",
|
"homepage": "https://github.com/su226/IdhagnBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
{
|
{
|
||||||
"name": "hsbot",
|
"name": "hsbot",
|
||||||
"desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用:HSreplay, Fbigame, Hearthstone API",
|
"desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用:HSreplay, Fbigame, Hearthstone API",
|
||||||
"author": "gzy02",
|
"author_id": 67055520,
|
||||||
"homepage": "https://github.com/gzy02/hsbot",
|
"homepage": "https://github.com/gzy02/hsbot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Bread Dog Bot",
|
"name": "Bread Dog Bot",
|
||||||
"desc": "Terraria TShock QQ 机器人",
|
"desc": "Terraria TShock QQ 机器人",
|
||||||
"author": "Qianyiovo",
|
"author_id": 160252668,
|
||||||
"homepage": "https://github.com/Qianyiovo/bread_dog_bot",
|
"homepage": "https://github.com/Qianyiovo/bread_dog_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
{
|
{
|
||||||
"name": "RanBot",
|
"name": "RanBot",
|
||||||
"desc": "不@会很安静的Bot",
|
"desc": "不@会很安静的Bot",
|
||||||
"author": "IAXRetailer",
|
"author_id": 88923783,
|
||||||
"homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot",
|
"homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
{
|
{
|
||||||
"name": "辞辞(cici)Bot",
|
"name": "辞辞(cici)Bot",
|
||||||
"desc": "一个集成娱乐和群管为一体的机器人",
|
"desc": "一个集成娱乐和群管为一体的机器人",
|
||||||
"author": "mengxinyuan638",
|
"author_id": 90902259,
|
||||||
"homepage": "https://github.com/mengxinyuan638/cici-bot",
|
"homepage": "https://github.com/mengxinyuan638/cici-bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
{
|
{
|
||||||
"name": "SuzunoBot",
|
"name": "SuzunoBot",
|
||||||
"desc": "多功能音游bot,主要服务maimaiDX、Arcaea",
|
"desc": "多功能音游bot,主要服务maimaiDX、Arcaea",
|
||||||
"author": "Rinfair-CSP-A016",
|
"author_id": 29980586,
|
||||||
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
|
"homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
{
|
{
|
||||||
"name": "青岚",
|
"name": "青岚",
|
||||||
"desc": "基于NoneBot的与Minecraft Server互通消息的机器人",
|
"desc": "基于NoneBot的与Minecraft Server互通消息的机器人",
|
||||||
"author": "17TheWord",
|
"author_id": 54731914,
|
||||||
"homepage": "https://github.com/17TheWord/qinglan_bot",
|
"homepage": "https://github.com/17TheWord/qinglan_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ChensQBOTv2",
|
"name": "ChensQBOTv2",
|
||||||
"desc": "多功能QQ群机器人,权限管理/联ban/社工等等等等,以及拥有一个强大的开发者",
|
"desc": "多功能QQ群机器人,权限管理/联ban/社工等等等等,以及拥有一个强大的开发者",
|
||||||
"author": "cnchens",
|
"author_id": 116929900,
|
||||||
"homepage": "https://github.com/cnchens/ChensQBOTv2",
|
"homepage": "https://github.com/cnchens/ChensQBOTv2",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
{
|
{
|
||||||
"name": "koishi",
|
"name": "koishi",
|
||||||
"desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。",
|
"desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。",
|
||||||
"author": "CupidsBow",
|
"author_id": 71639222,
|
||||||
"homepage": "https://github.com/CupidsBow/koishi",
|
"homepage": "https://github.com/CupidsBow/koishi",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
{
|
{
|
||||||
"name": "脑积水",
|
"name": "脑积水",
|
||||||
"desc": "一个超级缝合怪...",
|
"desc": "一个超级缝合怪...",
|
||||||
"author": "zhulinyv",
|
"author_id": 66541860,
|
||||||
"homepage": "https://github.com/zhulinyv/NJS",
|
"homepage": "https://github.com/zhulinyv/NJS",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
{
|
{
|
||||||
"name": "LOVE酱",
|
"name": "LOVE酱",
|
||||||
"desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。",
|
"desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。",
|
||||||
"author": "allureluoli",
|
"author_id": 106828088,
|
||||||
"homepage": "https://github.com/allureluoli/LOVE-",
|
"homepage": "https://github.com/allureluoli/LOVE-",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -411,7 +411,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fubot",
|
"name": "fubot",
|
||||||
"desc": "基于nonebot与go-cqhttp的QQ娱乐bot,提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。",
|
"desc": "基于nonebot与go-cqhttp的QQ娱乐bot,提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。",
|
||||||
"author": "HCskia",
|
"author_id": 54059896,
|
||||||
"homepage": "https://github.com/HCskia/fu-Bot",
|
"homepage": "https://github.com/HCskia/fu-Bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
{
|
{
|
||||||
"name": "桃桃酱",
|
"name": "桃桃酱",
|
||||||
"desc": "一个会拆家的高性能缝合萝卜子",
|
"desc": "一个会拆家的高性能缝合萝卜子",
|
||||||
"author": "tkgs0",
|
"author_id": 107618388,
|
||||||
"homepage": "https://github.com/tkgs0/Momoko",
|
"homepage": "https://github.com/tkgs0/Momoko",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
{
|
{
|
||||||
"name": "CoolQBot",
|
"name": "CoolQBot",
|
||||||
"desc": "基于 NoneBot2 的聊天机器人",
|
"desc": "基于 NoneBot2 的聊天机器人",
|
||||||
"author": "he0119",
|
"author_id": 5219550,
|
||||||
"homepage": "https://github.com/he0119/CoolQBot",
|
"homepage": "https://github.com/he0119/CoolQBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
{
|
{
|
||||||
"name": "XDbot2",
|
"name": "XDbot2",
|
||||||
"desc": "简单的QQ功能型机器人",
|
"desc": "简单的QQ功能型机器人",
|
||||||
"author": "This-is-XiaoDeng",
|
"author_id": 104149371,
|
||||||
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
|
"homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -448,7 +448,7 @@
|
|||||||
{
|
{
|
||||||
"name": "March7th",
|
"name": "March7th",
|
||||||
"desc": "三月七 - 崩坏:星穹铁道机器人",
|
"desc": "三月七 - 崩坏:星穹铁道机器人",
|
||||||
"author": "mobyw",
|
"author_id": 44370805,
|
||||||
"homepage": "https://github.com/Mar-7th/March7th",
|
"homepage": "https://github.com/Mar-7th/March7th",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ay机器人",
|
"name": "ay机器人",
|
||||||
"desc": "codeforces和洛谷卷王监视、股票监控、ai聊天",
|
"desc": "codeforces和洛谷卷王监视、股票监控、ai聊天",
|
||||||
"author": "863109569",
|
"author_id": 77315378,
|
||||||
"homepage": "https://github.com/863109569/qqbot",
|
"homepage": "https://github.com/863109569/qqbot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
{
|
{
|
||||||
"name": "狐尾",
|
"name": "狐尾",
|
||||||
"desc": "一个整合了兽云祭api的机器人,支持账号令牌操作,以及上传兽图",
|
"desc": "一个整合了兽云祭api的机器人,支持账号令牌操作,以及上传兽图",
|
||||||
"author": "bingqiu456",
|
"author_id": 99388013,
|
||||||
"homepage": "https://github.com/bingqiu456/shouyun",
|
"homepage": "https://github.com/bingqiu456/shouyun",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -499,7 +499,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ReimeiBot-黎明机器人",
|
"name": "ReimeiBot-黎明机器人",
|
||||||
"desc": "流星飞逝,黎明终将到来。",
|
"desc": "流星飞逝,黎明终将到来。",
|
||||||
"author": "ThirdBlood",
|
"author_id": 65395090,
|
||||||
"homepage": "https://github.com/3rdBit/ReimeiBot",
|
"homepage": "https://github.com/3rdBit/ReimeiBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -507,7 +507,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web_bot",
|
"name": "web_bot",
|
||||||
"desc": "把机器人搬到网络上",
|
"desc": "把机器人搬到网络上",
|
||||||
"author": "wsdtl",
|
"author_id": 63489103,
|
||||||
"homepage": "https://github.com/wsdtl/web_bot",
|
"homepage": "https://github.com/wsdtl/web_bot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
{
|
{
|
||||||
"name": "林汐",
|
"name": "林汐",
|
||||||
"desc": "多平台功能型Bot",
|
"desc": "多平台功能型Bot",
|
||||||
"author": "mute23-code",
|
"author_id": 110453675,
|
||||||
"homepage": "https://github.com/netsora/SoraBot",
|
"homepage": "https://github.com/netsora/SoraBot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -537,7 +537,7 @@
|
|||||||
{
|
{
|
||||||
"name": "米缸",
|
"name": "米缸",
|
||||||
"desc": "基于nonebot2的米缸Bot",
|
"desc": "基于nonebot2的米缸Bot",
|
||||||
"author": "LambdaYH",
|
"author_id": 13503375,
|
||||||
"homepage": "https://github.com/LambdaYH/MigangBot",
|
"homepage": "https://github.com/LambdaYH/MigangBot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
{
|
{
|
||||||
"name": "不正经的妹妹",
|
"name": "不正经的妹妹",
|
||||||
"desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人",
|
"desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人",
|
||||||
"author": "itsevin",
|
"author_id": 104713034,
|
||||||
"homepage": "https://github.com/itsevin/sister_bot",
|
"homepage": "https://github.com/itsevin/sister_bot",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -553,7 +553,7 @@
|
|||||||
{
|
{
|
||||||
"name": "星见Kirami",
|
"name": "星见Kirami",
|
||||||
"desc": "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。",
|
"desc": "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。",
|
||||||
"author": "A-kirami",
|
"author_id": 66513481,
|
||||||
"homepage": "https://kiramibot.dev/",
|
"homepage": "https://kiramibot.dev/",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
{
|
{
|
||||||
"name": "OCNbot",
|
"name": "OCNbot",
|
||||||
"desc": "OI Contest Notifier bot,一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot",
|
"desc": "OI Contest Notifier bot,一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot",
|
||||||
"author": "ACnoway",
|
"author_id": 91535478,
|
||||||
"homepage": "https://github.com/ACnoway/OCNbot",
|
"homepage": "https://github.com/ACnoway/OCNbot",
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
@@ -574,5 +574,93 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"name": "妃爱",
|
||||||
|
"desc": "超可爱的妃爱QQ群聊机器人",
|
||||||
|
"author_id": 52267304,
|
||||||
|
"homepage": "https://github.com/jiangyuxiaoxiao/Hiyori",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "芙芙",
|
||||||
|
"desc": "供 Mooncell Wiki 协作使用的跨平台机器人",
|
||||||
|
"author_id": 14922941,
|
||||||
|
"homepage": "https://github.com/MooncellWiki/BotFooChan",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sakiko",
|
||||||
|
"desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot",
|
||||||
|
"author_id": 55650833,
|
||||||
|
"homepage": "https://github.com/zhaomaoniu/Sakiko",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Minecraft",
|
||||||
|
"color": "#6cc349"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "BanGDream",
|
||||||
|
"color": "#e70050"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Minecraft_QQBot",
|
||||||
|
"desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。",
|
||||||
|
"author_id": 90964775,
|
||||||
|
"homepage": "https://github.com/Minecraft-QQBot/BotServer",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Minecraft",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "娱乐",
|
||||||
|
"color": "#37a7e7"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小安提Bot",
|
||||||
|
"desc": "服务于音游 舞萌DX 的多功能Bot",
|
||||||
|
"author_id": 186144551,
|
||||||
|
"homepage": "https://github.com/Ant1816/Ant1Bot",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "maimaiDX",
|
||||||
|
"color": "#52ea9a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "音游",
|
||||||
|
"color": "#f74b18"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CanrotBot",
|
||||||
|
"desc": "有很多实用功能的bot,也有很多没什么用的娱乐功能;接入了大模型,并且有一部分功能可以被大模型调用。主打一个全都有(",
|
||||||
|
"author_id": 18070676,
|
||||||
|
"homepage": "https://github.com/wangyw15/CanrotBot",
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mio澪",
|
||||||
|
"desc": "超可爱多功能Qbot",
|
||||||
|
"author_id": 50508678,
|
||||||
|
"homepage": "https://github.com/EienSakura/mio",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "娱乐",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"project_link": "",
|
"project_link": "",
|
||||||
"name": "None",
|
"name": "None",
|
||||||
"desc": "None 驱动器",
|
"desc": "None 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"project_link": "nonebot2[fastapi]",
|
"project_link": "nonebot2[fastapi]",
|
||||||
"name": "FastAPI",
|
"name": "FastAPI",
|
||||||
"desc": "FastAPI 驱动器",
|
"desc": "FastAPI 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"project_link": "nonebot2[quart]",
|
"project_link": "nonebot2[quart]",
|
||||||
"name": "Quart",
|
"name": "Quart",
|
||||||
"desc": "Quart 驱动器",
|
"desc": "Quart 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"project_link": "nonebot2[httpx]",
|
"project_link": "nonebot2[httpx]",
|
||||||
"name": "HTTPX",
|
"name": "HTTPX",
|
||||||
"desc": "HTTPX 驱动器",
|
"desc": "HTTPX 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"project_link": "nonebot2[websockets]",
|
"project_link": "nonebot2[websockets]",
|
||||||
"name": "websockets",
|
"name": "websockets",
|
||||||
"desc": "websockets 驱动器",
|
"desc": "websockets 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
@@ -54,9 +54,9 @@
|
|||||||
"project_link": "nonebot2[aiohttp]",
|
"project_link": "nonebot2[aiohttp]",
|
||||||
"name": "AIOHTTP",
|
"name": "AIOHTTP",
|
||||||
"desc": "AIOHTTP 驱动器",
|
"desc": "AIOHTTP 驱动器",
|
||||||
"author": "yanyongyu",
|
"author_id": 42488585,
|
||||||
"homepage": "/docs/advanced/driver",
|
"homepage": "/docs/advanced/driver",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"is_official": true
|
"is_official": true
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
7988
assets/plugins.json5
Normal file
7988
assets/plugins.json5
Normal file
File diff suppressed because it is too large
Load Diff
2462
envs/pydantic-v1/poetry.lock
generated
Normal file
2462
envs/pydantic-v1/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
envs/pydantic-v1/pyproject.toml
Normal file
18
envs/pydantic-v1/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "nonebot-pydantic-v1"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Private pydantic v1 test env for nonebot"
|
||||||
|
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pydantic = "^1.0.0"
|
||||||
|
nonebot-test = { path = "../test/", develop = false }
|
||||||
|
nonebot2 = { path = "../../", extras = ["all"], develop = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
2546
envs/pydantic-v2/poetry.lock
generated
Normal file
2546
envs/pydantic-v2/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
envs/pydantic-v2/pyproject.toml
Normal file
18
envs/pydantic-v2/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "nonebot-pydantic-v2"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Private pydantic v2 test env for nonebot"
|
||||||
|
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pydantic = "^2.0.0"
|
||||||
|
nonebot-test = { path = "../test/", develop = false }
|
||||||
|
nonebot2 = { path = "../../", extras = ["all"], develop = true }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
1
envs/test/nonebot-test.py
Normal file
1
envs/test/nonebot-test.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# fake file to make project installable
|
||||||
1362
envs/test/poetry.lock
generated
Normal file
1362
envs/test/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
envs/test/pyproject.toml
Normal file
21
envs/test/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "nonebot-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Private test env for nonebot"
|
||||||
|
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||||
|
license = "MIT"
|
||||||
|
packages = [{ include = "nonebot-test.py" }]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
trio = "^0.27.0"
|
||||||
|
nonebug = "^0.4.1"
|
||||||
|
wsproto = "^1.2.0"
|
||||||
|
pytest-cov = "^6.0.0"
|
||||||
|
pytest-xdist = "^3.0.2"
|
||||||
|
werkzeug = ">=2.3.6,<4.0.0"
|
||||||
|
coverage-conditional-plugin = "^0.9.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -35,25 +35,28 @@
|
|||||||
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
||||||
- `get_available_plugin_names` =>
|
- `get_available_plugin_names` =>
|
||||||
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
||||||
|
- `get_plugin_config` => {ref}``get_plugin_config` <nonebot.plugin.get_plugin_config>`
|
||||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
description: nonebot 模块
|
description: nonebot 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
|
import os
|
||||||
|
from typing import Any, Optional, TypeVar, Union, overload
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
from pydantic.env_settings import DotenvType
|
|
||||||
|
|
||||||
from nonebot.config import Env, Config
|
from nonebot.adapters import Adapter, Bot
|
||||||
|
from nonebot.compat import model_dump
|
||||||
|
from nonebot.config import DOTENV_TYPE, Config, Env
|
||||||
|
from nonebot.drivers import ASGIMixin, Driver, combine_driver
|
||||||
from nonebot.log import logger as logger
|
from nonebot.log import logger as logger
|
||||||
from nonebot.adapters import Bot, Adapter
|
|
||||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||||
from nonebot.drivers import Driver, ASGIMixin, combine_driver
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = version("nonebot2")
|
__version__ = version("nonebot2")
|
||||||
@@ -99,7 +102,7 @@ def get_adapter(name: str) -> Adapter:
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_adapter(name: Type[A]) -> A:
|
def get_adapter(name: type[A]) -> A:
|
||||||
"""
|
"""
|
||||||
参数:
|
参数:
|
||||||
name: 适配器类型
|
name: 适配器类型
|
||||||
@@ -109,7 +112,7 @@ def get_adapter(name: Type[A]) -> A:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
def get_adapter(name: Union[str, type[Adapter]]) -> Adapter:
|
||||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
@@ -130,7 +133,7 @@ def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
|||||||
return adapters[target]
|
return adapters[target]
|
||||||
|
|
||||||
|
|
||||||
def get_adapters() -> Dict[str, Adapter]:
|
def get_adapters() -> dict[str, Adapter]:
|
||||||
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
@@ -229,7 +232,7 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
|
|||||||
raise ValueError("There are no bots to get.")
|
raise ValueError("There are no bots to get.")
|
||||||
|
|
||||||
|
|
||||||
def get_bots() -> Dict[str, Bot]:
|
def get_bots() -> dict[str, Bot]:
|
||||||
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
@@ -248,7 +251,7 @@ def get_bots() -> Dict[str, Bot]:
|
|||||||
return get_driver().bots
|
return get_driver().bots
|
||||||
|
|
||||||
|
|
||||||
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."
|
||||||
@@ -265,15 +268,16 @@ def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
|||||||
|
|
||||||
|
|
||||||
def _log_patcher(record: "loguru.Record"):
|
def _log_patcher(record: "loguru.Record"):
|
||||||
|
"""使用插件标识优化日志展示"""
|
||||||
record["name"] = (
|
record["name"] = (
|
||||||
plugin.name
|
plugin.id_
|
||||||
if (module_name := record["name"])
|
if (module_name := record["name"])
|
||||||
and (plugin := get_plugin_by_module_name(module_name))
|
and (plugin := get_plugin_by_module_name(module_name))
|
||||||
else (module_name and module_name.split(".")[0])
|
else (module_name and module_name.split(".", maxsplit=1)[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
def init(*, _env_file: Optional[DOTENV_TYPE] = None, **kwargs: Any) -> None:
|
||||||
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||||
|
|
||||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||||
@@ -296,9 +300,11 @@ def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
|||||||
_env_file = _env_file or f".env.{env.environment}"
|
_env_file = _env_file or f".env.{env.environment}"
|
||||||
config = Config(
|
config = Config(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
_env_file=(".env", _env_file)
|
_env_file=(
|
||||||
if isinstance(_env_file, (str, os.PathLike))
|
(".env", _env_file)
|
||||||
else _env_file,
|
if isinstance(_env_file, (str, os.PathLike))
|
||||||
|
else _env_file
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.configure(
|
logger.configure(
|
||||||
@@ -308,7 +314,7 @@ def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
|||||||
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
||||||
)
|
)
|
||||||
logger.opt(colors=True).debug(
|
logger.opt(colors=True).debug(
|
||||||
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
|
f"Loaded <y><b>Config</b></y>: {escape_tag(str(model_dump(config)))}"
|
||||||
)
|
)
|
||||||
|
|
||||||
DriverClass = _resolve_combine_expr(config.driver)
|
DriverClass = _resolve_combine_expr(config.driver)
|
||||||
@@ -331,32 +337,31 @@ def run(*args: Any, **kwargs: Any) -> None:
|
|||||||
get_driver().run(*args, **kwargs)
|
get_driver().run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
from nonebot.plugin import on as on
|
|
||||||
from nonebot.plugin import on_type as on_type
|
|
||||||
from nonebot.plugin import require as require
|
|
||||||
from nonebot.plugin import on_regex as on_regex
|
|
||||||
from nonebot.plugin import on_notice as on_notice
|
|
||||||
from nonebot.plugin import get_plugin as get_plugin
|
|
||||||
from nonebot.plugin import on_command as on_command
|
|
||||||
from nonebot.plugin import on_keyword as on_keyword
|
|
||||||
from nonebot.plugin import on_message as on_message
|
|
||||||
from nonebot.plugin import on_request as on_request
|
|
||||||
from nonebot.plugin import load_plugin as load_plugin
|
|
||||||
from nonebot.plugin import on_endswith as on_endswith
|
|
||||||
from nonebot.plugin import CommandGroup as CommandGroup
|
from nonebot.plugin import CommandGroup as CommandGroup
|
||||||
from nonebot.plugin import MatcherGroup as MatcherGroup
|
from nonebot.plugin import MatcherGroup as MatcherGroup
|
||||||
from nonebot.plugin import load_plugins as load_plugins
|
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
||||||
from nonebot.plugin import on_fullmatch as on_fullmatch
|
|
||||||
from nonebot.plugin import on_metaevent as on_metaevent
|
|
||||||
from nonebot.plugin import on_startswith as on_startswith
|
|
||||||
from nonebot.plugin import load_from_json as load_from_json
|
|
||||||
from nonebot.plugin import load_from_toml as load_from_toml
|
|
||||||
from nonebot.plugin import load_all_plugins as load_all_plugins
|
|
||||||
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 get_plugin as get_plugin
|
||||||
|
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
|
||||||
|
from nonebot.plugin import get_plugin_config as get_plugin_config
|
||||||
|
from nonebot.plugin import load_all_plugins as load_all_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 load_from_json as load_from_json
|
||||||
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
from nonebot.plugin import load_from_toml as load_from_toml
|
||||||
|
from nonebot.plugin import load_plugin as load_plugin
|
||||||
__autodoc__ = {"internal": False}
|
from nonebot.plugin import load_plugins as load_plugins
|
||||||
|
from nonebot.plugin import on as on
|
||||||
|
from nonebot.plugin import on_command as on_command
|
||||||
|
from nonebot.plugin import on_endswith as on_endswith
|
||||||
|
from nonebot.plugin import on_fullmatch as on_fullmatch
|
||||||
|
from nonebot.plugin import on_keyword as on_keyword
|
||||||
|
from nonebot.plugin import on_message as on_message
|
||||||
|
from nonebot.plugin import on_metaevent as on_metaevent
|
||||||
|
from nonebot.plugin import on_notice as on_notice
|
||||||
|
from nonebot.plugin import on_regex as on_regex
|
||||||
|
from nonebot.plugin import on_request as on_request
|
||||||
|
from nonebot.plugin import on_shell_command as on_shell_command
|
||||||
|
from nonebot.plugin import on_startswith as on_startswith
|
||||||
|
from nonebot.plugin import on_type as on_type
|
||||||
|
from nonebot.plugin import require as require
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
|
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
description: nonebot.adapters 模块
|
description: nonebot.adapters 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from nonebot.internal.adapter import Adapter as Adapter
|
||||||
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 Message as Message
|
from nonebot.internal.adapter import Message as Message
|
||||||
from nonebot.internal.adapter import MessageSegment as MessageSegment
|
from nonebot.internal.adapter import MessageSegment as MessageSegment
|
||||||
from nonebot.internal.adapter import MessageTemplate as MessageTemplate
|
from nonebot.internal.adapter import MessageTemplate as MessageTemplate
|
||||||
|
|||||||
417
nonebot/compat.py
Normal file
417
nonebot/compat.py
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
"""本模块为 Pydantic 版本兼容层模块
|
||||||
|
|
||||||
|
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
|
sidebar_position: 16
|
||||||
|
description: nonebot.compat 模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from dataclasses import dataclass, is_dataclass
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Annotated,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Generic,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
from typing_extensions import Self, get_args, get_origin, is_typeddict
|
||||||
|
|
||||||
|
from pydantic import VERSION, BaseModel
|
||||||
|
|
||||||
|
from nonebot.typing import origin_is_annotated
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
PYDANTIC_V2 = int(VERSION.split(".", 1)[0]) == 2
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class _CustomValidationClass(Protocol):
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]: ...
|
||||||
|
|
||||||
|
CVC = TypeVar("CVC", bound=_CustomValidationClass)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"DEFAULT_CONFIG",
|
||||||
|
"ConfigDict",
|
||||||
|
"FieldInfo",
|
||||||
|
"ModelField",
|
||||||
|
"PydanticUndefined",
|
||||||
|
"PydanticUndefinedType",
|
||||||
|
"Required",
|
||||||
|
"TypeAdapter",
|
||||||
|
"custom_validation",
|
||||||
|
"extract_field_info",
|
||||||
|
"model_config",
|
||||||
|
"model_dump",
|
||||||
|
"model_fields",
|
||||||
|
"type_validate_json",
|
||||||
|
"type_validate_python",
|
||||||
|
)
|
||||||
|
|
||||||
|
__autodoc__ = {
|
||||||
|
"PydanticUndefined": "Pydantic Undefined object",
|
||||||
|
"PydanticUndefinedType": "Pydantic Undefined type",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
|
from pydantic import GetCoreSchemaHandler
|
||||||
|
from pydantic import TypeAdapter as TypeAdapter
|
||||||
|
from pydantic._internal._repr import display_as_type
|
||||||
|
from pydantic.fields import FieldInfo as BaseFieldInfo
|
||||||
|
from pydantic_core import CoreSchema, core_schema
|
||||||
|
|
||||||
|
Required = Ellipsis
|
||||||
|
"""Alias of Ellipsis for compatibility with pydantic v1"""
|
||||||
|
|
||||||
|
# Export undefined type
|
||||||
|
from pydantic_core import PydanticUndefined as PydanticUndefined
|
||||||
|
from pydantic_core import PydanticUndefinedType as PydanticUndefinedType
|
||||||
|
|
||||||
|
# isort: split
|
||||||
|
|
||||||
|
# Export model config dict
|
||||||
|
from pydantic import ConfigDict as ConfigDict
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
||||||
|
"""Default config for validations"""
|
||||||
|
|
||||||
|
class FieldInfo(BaseFieldInfo):
|
||||||
|
"""FieldInfo class with extra property for compatibility with pydantic v1"""
|
||||||
|
|
||||||
|
# make default can be positional argument
|
||||||
|
def __init__(self, default: Any = PydanticUndefined, **kwargs: Any) -> None:
|
||||||
|
super().__init__(default=default, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra(self) -> dict[str, Any]:
|
||||||
|
"""Extra data that is not part of the standard pydantic fields.
|
||||||
|
|
||||||
|
For compatibility with pydantic v1.
|
||||||
|
"""
|
||||||
|
# extract extra data from attributes set except used slots
|
||||||
|
# we need to call super in advance due to
|
||||||
|
# comprehension not inlined in cpython < 3.12
|
||||||
|
# https://peps.python.org/pep-0709/
|
||||||
|
slots = super().__slots__
|
||||||
|
return {k: v for k, v in self._attributes_set.items() if k not in slots}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModelField:
|
||||||
|
"""ModelField class for compatibility with pydantic v1"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""The name of the field."""
|
||||||
|
annotation: Any
|
||||||
|
"""The annotation of the field."""
|
||||||
|
field_info: FieldInfo
|
||||||
|
"""The FieldInfo of the field."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:
|
||||||
|
return cls(name, annotation, field_info)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def construct(
|
||||||
|
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
|
||||||
|
) -> Self:
|
||||||
|
"""Construct a ModelField from given infos."""
|
||||||
|
return cls._construct(name, annotation, field_info or FieldInfo())
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
# Each ModelField is unique for our purposes,
|
||||||
|
# to allow store them in a set.
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def type_adapter(self) -> TypeAdapter:
|
||||||
|
"""TypeAdapter of the field.
|
||||||
|
|
||||||
|
Cache the TypeAdapter to avoid creating it multiple times.
|
||||||
|
Pydantic v2 uses too much cpu time to create TypeAdapter.
|
||||||
|
|
||||||
|
See: https://github.com/pydantic/pydantic/issues/9834
|
||||||
|
"""
|
||||||
|
return TypeAdapter(
|
||||||
|
Annotated[self.annotation, self.field_info],
|
||||||
|
config=None if self._annotation_has_config() else DEFAULT_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _annotation_has_config(self) -> bool:
|
||||||
|
"""Check if the annotation has config.
|
||||||
|
|
||||||
|
TypeAdapter raise error when annotation has config
|
||||||
|
and given config is not None.
|
||||||
|
"""
|
||||||
|
type_is_annotated = origin_is_annotated(get_origin(self.annotation))
|
||||||
|
inner_type = (
|
||||||
|
get_args(self.annotation)[0] if type_is_annotated else self.annotation
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
issubclass(inner_type, BaseModel)
|
||||||
|
or is_dataclass(inner_type)
|
||||||
|
or is_typeddict(inner_type)
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_default(self) -> Any:
|
||||||
|
"""Get the default value of the field."""
|
||||||
|
return self.field_info.get_default(call_default_factory=True)
|
||||||
|
|
||||||
|
def _type_display(self):
|
||||||
|
"""Get the display of the type of the field."""
|
||||||
|
return display_as_type(self.annotation)
|
||||||
|
|
||||||
|
def validate_value(self, value: Any) -> Any:
|
||||||
|
"""Validate the value pass to the field."""
|
||||||
|
return self.type_adapter.validate_python(value)
|
||||||
|
|
||||||
|
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
|
||||||
|
"""Get FieldInfo init kwargs from a FieldInfo instance."""
|
||||||
|
|
||||||
|
kwargs = field_info._attributes_set.copy()
|
||||||
|
kwargs["annotation"] = field_info.rebuild_annotation()
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def model_fields(model: type[BaseModel]) -> list[ModelField]:
|
||||||
|
"""Get field list of a model."""
|
||||||
|
|
||||||
|
return [
|
||||||
|
ModelField._construct(
|
||||||
|
name=name,
|
||||||
|
annotation=field_info.rebuild_annotation(),
|
||||||
|
field_info=FieldInfo(**extract_field_info(field_info)),
|
||||||
|
)
|
||||||
|
for name, field_info in model.model_fields.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
def model_config(model: type[BaseModel]) -> Any:
|
||||||
|
"""Get config of a model."""
|
||||||
|
return model.model_config
|
||||||
|
|
||||||
|
def model_dump(
|
||||||
|
model: BaseModel,
|
||||||
|
include: Optional[set[str]] = None,
|
||||||
|
exclude: Optional[set[str]] = None,
|
||||||
|
by_alias: bool = False,
|
||||||
|
exclude_unset: bool = False,
|
||||||
|
exclude_defaults: bool = False,
|
||||||
|
exclude_none: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return model.model_dump(
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
by_alias=by_alias,
|
||||||
|
exclude_unset=exclude_unset,
|
||||||
|
exclude_defaults=exclude_defaults,
|
||||||
|
exclude_none=exclude_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
def type_validate_python(type_: type[T], data: Any) -> T:
|
||||||
|
"""Validate data with given type."""
|
||||||
|
return TypeAdapter(type_).validate_python(data)
|
||||||
|
|
||||||
|
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
|
||||||
|
"""Validate JSON with given type."""
|
||||||
|
return TypeAdapter(type_).validate_json(data)
|
||||||
|
|
||||||
|
def __get_pydantic_core_schema__(
|
||||||
|
cls: type["_CustomValidationClass"],
|
||||||
|
source_type: Any,
|
||||||
|
handler: GetCoreSchemaHandler,
|
||||||
|
) -> CoreSchema:
|
||||||
|
validators = list(cls.__get_validators__())
|
||||||
|
if len(validators) == 1:
|
||||||
|
return core_schema.no_info_plain_validator_function(validators[0])
|
||||||
|
return core_schema.chain_schema(
|
||||||
|
[core_schema.no_info_plain_validator_function(func) for func in validators]
|
||||||
|
)
|
||||||
|
|
||||||
|
def custom_validation(class_: type["CVC"]) -> type["CVC"]:
|
||||||
|
"""Use pydantic v1 like validator generator in pydantic v2"""
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
class_,
|
||||||
|
"__get_pydantic_core_schema__",
|
||||||
|
classmethod(__get_pydantic_core_schema__),
|
||||||
|
)
|
||||||
|
return class_
|
||||||
|
|
||||||
|
else: # pragma: pydantic-v1
|
||||||
|
from pydantic import BaseConfig as PydanticConfig
|
||||||
|
from pydantic import Extra, parse_obj_as, parse_raw_as
|
||||||
|
from pydantic.fields import FieldInfo as BaseFieldInfo
|
||||||
|
from pydantic.fields import ModelField as BaseModelField
|
||||||
|
from pydantic.schema import get_annotation_from_field_info
|
||||||
|
|
||||||
|
# isort: split
|
||||||
|
|
||||||
|
from pydantic.fields import Required as Required
|
||||||
|
|
||||||
|
# isort: split
|
||||||
|
|
||||||
|
from pydantic.fields import Undefined as PydanticUndefined
|
||||||
|
from pydantic.fields import UndefinedType as PydanticUndefinedType
|
||||||
|
|
||||||
|
class ConfigDict(PydanticConfig):
|
||||||
|
"""Config class that allow get value with default value."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, field: str, default: Any = None) -> Any:
|
||||||
|
"""Get a config value."""
|
||||||
|
return getattr(cls, field, default)
|
||||||
|
|
||||||
|
class DEFAULT_CONFIG(ConfigDict):
|
||||||
|
extra = Extra.allow
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
class FieldInfo(BaseFieldInfo):
|
||||||
|
def __init__(self, default: Any = PydanticUndefined, **kwargs: Any):
|
||||||
|
# preprocess default value to make it compatible with pydantic v2
|
||||||
|
# when default is Required, set it to PydanticUndefined
|
||||||
|
if default is Required:
|
||||||
|
default = PydanticUndefined
|
||||||
|
super().__init__(default, **kwargs)
|
||||||
|
|
||||||
|
class ModelField(BaseModelField):
|
||||||
|
@classmethod
|
||||||
|
def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
type_=annotation,
|
||||||
|
class_validators=None,
|
||||||
|
model_config=DEFAULT_CONFIG,
|
||||||
|
default=field_info.default,
|
||||||
|
default_factory=field_info.default_factory,
|
||||||
|
required=(
|
||||||
|
field_info.default is PydanticUndefined
|
||||||
|
and field_info.default_factory is None
|
||||||
|
),
|
||||||
|
field_info=field_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def construct(
|
||||||
|
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
|
||||||
|
) -> Self:
|
||||||
|
"""Construct a ModelField from given infos.
|
||||||
|
|
||||||
|
Field annotation is preprocessed with field_info.
|
||||||
|
"""
|
||||||
|
if field_info is not None:
|
||||||
|
annotation = get_annotation_from_field_info(
|
||||||
|
annotation, field_info, name
|
||||||
|
)
|
||||||
|
return cls._construct(name, annotation, field_info or FieldInfo())
|
||||||
|
|
||||||
|
def validate_value(self, value: Any) -> Any:
|
||||||
|
"""Validate the value pass to the field."""
|
||||||
|
v, errs_ = self.validate(value, {}, loc=())
|
||||||
|
if errs_:
|
||||||
|
raise ValueError(value, self)
|
||||||
|
return v
|
||||||
|
|
||||||
|
class TypeAdapter(Generic[T]):
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
type: type[T],
|
||||||
|
*,
|
||||||
|
config: Optional[ConfigDict] = ...,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
type: Any,
|
||||||
|
*,
|
||||||
|
config: Optional[ConfigDict] = ...,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
type: Any,
|
||||||
|
*,
|
||||||
|
config: Optional[ConfigDict] = None,
|
||||||
|
) -> None:
|
||||||
|
self.type = type
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def validate_python(self, value: Any) -> T:
|
||||||
|
return type_validate_python(self.type, value)
|
||||||
|
|
||||||
|
def validate_json(self, value: Union[str, bytes]) -> T:
|
||||||
|
return type_validate_json(self.type, value)
|
||||||
|
|
||||||
|
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
|
||||||
|
"""Get FieldInfo init kwargs from a FieldInfo instance."""
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
s: getattr(field_info, s) for s in field_info.__slots__ if s != "extra"
|
||||||
|
}
|
||||||
|
kwargs.update(field_info.extra)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def model_fields(model: type[BaseModel]) -> list[ModelField]:
|
||||||
|
"""Get field list of a model."""
|
||||||
|
|
||||||
|
# construct the model field without preprocess to avoid error
|
||||||
|
return [
|
||||||
|
ModelField._construct(
|
||||||
|
name=model_field.name,
|
||||||
|
annotation=model_field.annotation,
|
||||||
|
field_info=FieldInfo(
|
||||||
|
**extract_field_info(model_field.field_info),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for model_field in model.__fields__.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
def model_config(model: type[BaseModel]) -> Any:
|
||||||
|
"""Get config of a model."""
|
||||||
|
return model.__config__
|
||||||
|
|
||||||
|
def model_dump(
|
||||||
|
model: BaseModel,
|
||||||
|
include: Optional[set[str]] = None,
|
||||||
|
exclude: Optional[set[str]] = None,
|
||||||
|
by_alias: bool = False,
|
||||||
|
exclude_unset: bool = False,
|
||||||
|
exclude_defaults: bool = False,
|
||||||
|
exclude_none: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return model.dict(
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
by_alias=by_alias,
|
||||||
|
exclude_unset=exclude_unset,
|
||||||
|
exclude_defaults=exclude_defaults,
|
||||||
|
exclude_none=exclude_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
def type_validate_python(type_: type[T], data: Any) -> T:
|
||||||
|
"""Validate data with given type."""
|
||||||
|
return parse_obj_as(type_, data)
|
||||||
|
|
||||||
|
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
|
||||||
|
"""Validate JSON with given type."""
|
||||||
|
return parse_raw_as(type_, data)
|
||||||
|
|
||||||
|
def custom_validation(class_: type["CVC"]) -> type["CVC"]:
|
||||||
|
"""Do nothing in pydantic v1"""
|
||||||
|
return class_
|
||||||
@@ -7,81 +7,253 @@ NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
|
|||||||
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
description: nonebot.config 模块
|
description: nonebot.config 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import abc
|
||||||
|
from collections.abc import Mapping
|
||||||
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
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||||
|
from typing_extensions import TypeAlias, get_args, get_origin
|
||||||
|
|
||||||
from pydantic.utils import deep_update
|
from dotenv import dotenv_values
|
||||||
from pydantic.fields import Undefined, UndefinedType
|
from pydantic import BaseModel, Field
|
||||||
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
from pydantic.networks import IPvAnyAddress
|
||||||
from pydantic.env_settings import (
|
|
||||||
DotenvType,
|
from nonebot.compat import (
|
||||||
SettingsError,
|
PYDANTIC_V2,
|
||||||
EnvSettingsSource,
|
ConfigDict,
|
||||||
InitSettingsSource,
|
ModelField,
|
||||||
SettingsSourceCallable,
|
PydanticUndefined,
|
||||||
|
PydanticUndefinedType,
|
||||||
|
model_config,
|
||||||
|
model_fields,
|
||||||
)
|
)
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
|
from nonebot.typing import origin_is_union
|
||||||
|
from nonebot.utils import deep_update, lenient_issubclass, type_is_complex
|
||||||
|
|
||||||
|
DOTENV_TYPE: TypeAlias = Union[
|
||||||
|
Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...]
|
||||||
|
]
|
||||||
|
|
||||||
|
ENV_FILE_SENTINEL = Path("")
|
||||||
|
|
||||||
|
|
||||||
class CustomEnvSettings(EnvSettingsSource):
|
class SettingsError(ValueError): ...
|
||||||
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
|
||||||
|
|
||||||
|
class BaseSettingsSource(abc.ABC):
|
||||||
|
def __init__(self, settings_cls: type["BaseSettings"]) -> None:
|
||||||
|
self.settings_cls = settings_cls
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> "SettingsConfig":
|
||||||
|
return model_config(self.settings_cls)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __call__(self) -> dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class InitSettingsSource(BaseSettingsSource):
|
||||||
|
__slots__ = ("init_kwargs",)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, settings_cls: type["BaseSettings"], init_kwargs: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
self.init_kwargs = init_kwargs
|
||||||
|
super().__init__(settings_cls)
|
||||||
|
|
||||||
|
def __call__(self) -> dict[str, Any]:
|
||||||
|
return self.init_kwargs
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"InitSettingsSource(init_kwargs={self.init_kwargs!r})"
|
||||||
|
|
||||||
|
|
||||||
|
class DotEnvSettingsSource(BaseSettingsSource):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
settings_cls: type["BaseSettings"],
|
||||||
|
env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL,
|
||||||
|
env_file_encoding: Optional[str] = None,
|
||||||
|
case_sensitive: Optional[bool] = None,
|
||||||
|
env_nested_delimiter: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(settings_cls)
|
||||||
|
self.env_file = (
|
||||||
|
env_file
|
||||||
|
if env_file is not ENV_FILE_SENTINEL
|
||||||
|
else self.config.get("env_file", (".env",))
|
||||||
|
)
|
||||||
|
self.env_file_encoding = (
|
||||||
|
env_file_encoding
|
||||||
|
if env_file_encoding is not None
|
||||||
|
else self.config.get("env_file_encoding", "utf-8")
|
||||||
|
)
|
||||||
|
self.case_sensitive = (
|
||||||
|
case_sensitive
|
||||||
|
if case_sensitive is not None
|
||||||
|
else self.config.get("case_sensitive", False)
|
||||||
|
)
|
||||||
|
self.env_nested_delimiter = (
|
||||||
|
env_nested_delimiter
|
||||||
|
if env_nested_delimiter is not None
|
||||||
|
else self.config.get("env_nested_delimiter", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_case_sensitive(self, var_name: str) -> str:
|
||||||
|
return var_name if self.case_sensitive else var_name.lower()
|
||||||
|
|
||||||
|
def _field_is_complex(self, field: ModelField) -> tuple[bool, bool]:
|
||||||
|
if type_is_complex(field.annotation):
|
||||||
|
return True, False
|
||||||
|
elif origin_is_union(get_origin(field.annotation)) and any(
|
||||||
|
type_is_complex(arg) for arg in get_args(field.annotation)
|
||||||
|
):
|
||||||
|
return True, True
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
def _parse_env_vars(
|
||||||
|
self, env_vars: Mapping[str, Optional[str]]
|
||||||
|
) -> dict[str, Optional[str]]:
|
||||||
|
return {
|
||||||
|
self._apply_case_sensitive(key): value for key, value in env_vars.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _read_env_file(self, file_path: Path) -> dict[str, Optional[str]]:
|
||||||
|
file_vars = dotenv_values(file_path, encoding=self.env_file_encoding)
|
||||||
|
return self._parse_env_vars(file_vars)
|
||||||
|
|
||||||
|
def _read_env_files(self) -> dict[str, Optional[str]]:
|
||||||
|
env_files = self.env_file
|
||||||
|
if env_files is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if isinstance(env_files, (str, os.PathLike)):
|
||||||
|
env_files = [env_files]
|
||||||
|
|
||||||
|
dotenv_vars: dict[str, Optional[str]] = {}
|
||||||
|
for env_file in env_files:
|
||||||
|
env_path = Path(env_file).expanduser()
|
||||||
|
if env_path.is_file():
|
||||||
|
dotenv_vars.update(self._read_env_file(env_path))
|
||||||
|
return dotenv_vars
|
||||||
|
|
||||||
|
def _next_field(
|
||||||
|
self, field: Optional[ModelField], key: str
|
||||||
|
) -> Optional[ModelField]:
|
||||||
|
if not field or origin_is_union(get_origin(field.annotation)):
|
||||||
|
return None
|
||||||
|
elif field.annotation and lenient_issubclass(field.annotation, BaseModel):
|
||||||
|
for field in model_fields(field.annotation):
|
||||||
|
if field.name == key:
|
||||||
|
return field
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _explode_env_vars(
|
||||||
|
self,
|
||||||
|
field: ModelField,
|
||||||
|
env_vars: dict[str, Optional[str]],
|
||||||
|
env_file_vars: dict[str, Optional[str]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if self.env_nested_delimiter is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
prefix = f"{field.name}{self.env_nested_delimiter}"
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
for env_name, env_val in env_vars.items():
|
||||||
|
if not env_name.startswith(prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# delete from file vars when used
|
||||||
|
if env_name in env_file_vars:
|
||||||
|
del env_file_vars[env_name]
|
||||||
|
|
||||||
|
_, *keys, last_key = env_name.split(self.env_nested_delimiter)
|
||||||
|
env_var = result
|
||||||
|
target_field: Optional[ModelField] = field
|
||||||
|
for key in keys:
|
||||||
|
target_field = self._next_field(target_field, key)
|
||||||
|
env_var = env_var.setdefault(key, {})
|
||||||
|
|
||||||
|
target_field = self._next_field(target_field, last_key)
|
||||||
|
if target_field and env_val:
|
||||||
|
is_complex, allow_parse_failure = self._field_is_complex(target_field)
|
||||||
|
if is_complex:
|
||||||
|
try:
|
||||||
|
env_val = json.loads(env_val)
|
||||||
|
except ValueError as e:
|
||||||
|
if not allow_parse_failure:
|
||||||
|
raise SettingsError(
|
||||||
|
f'error parsing env var "{env_name}"'
|
||||||
|
) from e
|
||||||
|
|
||||||
|
env_var[last_key] = env_val
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __call__(self) -> dict[str, Any]:
|
||||||
"""从环境变量和 dotenv 配置文件中读取配置项。"""
|
"""从环境变量和 dotenv 配置文件中读取配置项。"""
|
||||||
|
|
||||||
d: Dict[str, Any] = {}
|
d: dict[str, Any] = {}
|
||||||
|
|
||||||
if settings.__config__.case_sensitive:
|
env_vars = self._parse_env_vars(os.environ)
|
||||||
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
|
env_file_vars = self._read_env_files()
|
||||||
else:
|
|
||||||
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
|
||||||
|
|
||||||
env_file_vars = self._read_env_files(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 model_fields(self.settings_cls):
|
||||||
env_val: Union[str, None, UndefinedType] = Undefined
|
field_name = field.name
|
||||||
for env_name in field.field_info.extra["env_names"]:
|
env_name = self._apply_case_sensitive(field_name)
|
||||||
env_val = env_vars.get(env_name, Undefined)
|
|
||||||
if env_name in env_file_vars:
|
|
||||||
del env_file_vars[env_name]
|
|
||||||
if env_val is not Undefined:
|
|
||||||
break
|
|
||||||
|
|
||||||
is_complex, allow_parse_failure = self.field_is_complex(field)
|
# try get values from env vars
|
||||||
|
env_val = env_vars.get(env_name, PydanticUndefined)
|
||||||
|
# delete from file vars when used
|
||||||
|
if env_name in env_file_vars:
|
||||||
|
del env_file_vars[env_name]
|
||||||
|
|
||||||
|
is_complex, allow_parse_failure = self._field_is_complex(field)
|
||||||
if is_complex:
|
if is_complex:
|
||||||
if isinstance(env_val, UndefinedType):
|
if isinstance(env_val, PydanticUndefinedType):
|
||||||
# field is complex but no value found so far, try explode_env_vars
|
# field is complex but no value found so far, try explode_env_vars
|
||||||
if env_val_built := self.explode_env_vars(field, env_vars):
|
if env_val_built := self._explode_env_vars(
|
||||||
d[field.alias] = env_val_built
|
field, env_vars, env_file_vars
|
||||||
|
):
|
||||||
|
d[field_name] = env_val_built
|
||||||
elif env_val is None:
|
elif env_val is None:
|
||||||
d[field.alias] = env_val
|
d[field_name] = env_val
|
||||||
else:
|
else:
|
||||||
# field is complex and there's a value
|
# field is complex and there's a value
|
||||||
# decode that as JSON, then add explode_env_vars
|
# decode that as JSON, then add explode_env_vars
|
||||||
try:
|
try:
|
||||||
env_val = settings.__config__.parse_env_var(field.name, env_val)
|
env_val = json.loads(env_val)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if not allow_parse_failure:
|
if not allow_parse_failure:
|
||||||
raise SettingsError(
|
raise SettingsError(
|
||||||
f'error parsing env var "{env_name}"' # type: ignore
|
f'error parsing env var "{env_name}"'
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
if isinstance(env_val, dict):
|
if isinstance(env_val, dict):
|
||||||
d[field.alias] = deep_update(
|
# field value is a dict
|
||||||
env_val, self.explode_env_vars(field, env_vars)
|
# try explode_env_vars to find more sub-values
|
||||||
|
d[field_name] = deep_update(
|
||||||
|
env_val,
|
||||||
|
self._explode_env_vars(field, env_vars, env_file_vars),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
d[field.alias] = env_val
|
d[field_name] = env_val
|
||||||
elif not isinstance(env_val, UndefinedType):
|
elif env_val is not PydanticUndefined:
|
||||||
# simplest case, field is not complex
|
# simplest case, field is not complex
|
||||||
# we only need to add the value if it was found
|
# we only need to add the value if it was found
|
||||||
d[field.alias] = env_val
|
d[field_name] = env_val
|
||||||
|
|
||||||
# remain user custom config
|
# remain user custom config
|
||||||
for env_name in env_file_vars:
|
for env_name in env_file_vars:
|
||||||
@@ -89,7 +261,7 @@ class CustomEnvSettings(EnvSettingsSource):
|
|||||||
if env_val and (val_striped := env_val.strip()):
|
if env_val and (val_striped := env_val.strip()):
|
||||||
# there's a value, decode that as JSON
|
# there's a value, decode that as JSON
|
||||||
try:
|
try:
|
||||||
env_val = settings.__config__.parse_env_var(env_name, val_striped)
|
env_val = json.loads(val_striped)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.trace(
|
logger.trace(
|
||||||
"Error while parsing JSON for "
|
"Error while parsing JSON for "
|
||||||
@@ -113,38 +285,80 @@ class CustomEnvSettings(EnvSettingsSource):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class BaseConfig(BaseSettings):
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
|
|
||||||
|
class SettingsConfig(ConfigDict, total=False):
|
||||||
|
env_file: Optional[DOTENV_TYPE]
|
||||||
|
env_file_encoding: str
|
||||||
|
case_sensitive: bool
|
||||||
|
env_nested_delimiter: Optional[str]
|
||||||
|
|
||||||
|
else: # pragma: pydantic-v1
|
||||||
|
|
||||||
|
class SettingsConfig(ConfigDict):
|
||||||
|
env_file: Optional[DOTENV_TYPE]
|
||||||
|
env_file_encoding: str
|
||||||
|
case_sensitive: bool
|
||||||
|
env_nested_delimiter: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSettings(BaseModel):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# dummy getattr for pylance checking, actually not used
|
# dummy getattr for pylance checking, actually not used
|
||||||
def __getattr__(self, name: str) -> Any: # pragma: no cover
|
def __getattr__(self, name: str) -> Any: # pragma: no cover
|
||||||
return self.__dict__.get(name)
|
return self.__dict__.get(name)
|
||||||
|
|
||||||
class Config:
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
extra = Extra.allow
|
model_config = SettingsConfig(
|
||||||
env_nested_delimiter = "__"
|
extra="allow",
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=False,
|
||||||
|
env_nested_delimiter="__",
|
||||||
|
)
|
||||||
|
else: # pragma: pydantic-v1
|
||||||
|
|
||||||
@classmethod
|
class Config(SettingsConfig):
|
||||||
def customise_sources(
|
extra = "allow" # type: ignore
|
||||||
cls,
|
env_file = ".env"
|
||||||
init_settings: InitSettingsSource,
|
env_file_encoding = "utf-8"
|
||||||
env_settings: EnvSettingsSource,
|
case_sensitive = False
|
||||||
file_secret_settings: SettingsSourceCallable,
|
env_nested_delimiter = "__"
|
||||||
) -> Tuple[SettingsSourceCallable, ...]:
|
|
||||||
common_config = init_settings.init_kwargs.pop("_common_config", {})
|
def __init__(
|
||||||
return (
|
__settings_self__, # pyright: ignore[reportSelfClsParameterName]
|
||||||
init_settings,
|
_env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL,
|
||||||
CustomEnvSettings(
|
_env_file_encoding: Optional[str] = None,
|
||||||
env_settings.env_file,
|
_env_nested_delimiter: Optional[str] = None,
|
||||||
env_settings.env_file_encoding,
|
**values: Any,
|
||||||
env_settings.env_nested_delimiter,
|
) -> None:
|
||||||
env_settings.env_prefix_len,
|
super().__init__(
|
||||||
),
|
**__settings_self__._settings_build_values(
|
||||||
InitSettingsSource(common_config),
|
values,
|
||||||
file_secret_settings,
|
env_file=_env_file,
|
||||||
|
env_file_encoding=_env_file_encoding,
|
||||||
|
env_nested_delimiter=_env_nested_delimiter,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _settings_build_values(
|
||||||
|
self,
|
||||||
|
init_kwargs: dict[str, Any],
|
||||||
|
env_file: Optional[DOTENV_TYPE] = None,
|
||||||
|
env_file_encoding: Optional[str] = None,
|
||||||
|
env_nested_delimiter: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs)
|
||||||
|
env_settings = DotEnvSettingsSource(
|
||||||
|
self.__class__,
|
||||||
|
env_file=env_file,
|
||||||
|
env_file_encoding=env_file_encoding,
|
||||||
|
env_nested_delimiter=env_nested_delimiter,
|
||||||
|
)
|
||||||
|
return deep_update(env_settings(), init_settings())
|
||||||
|
|
||||||
|
|
||||||
class Env(BaseConfig):
|
class Env(BaseSettings):
|
||||||
"""运行环境配置。大小写不敏感。
|
"""运行环境配置。大小写不敏感。
|
||||||
|
|
||||||
将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
|
将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
|
||||||
@@ -156,11 +370,8 @@ class Env(BaseConfig):
|
|||||||
NoneBot 将从 `.env.{environment}` 文件中加载配置。
|
NoneBot 将从 `.env.{environment}` 文件中加载配置。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
|
|
||||||
|
class Config(BaseSettings):
|
||||||
class Config(BaseConfig):
|
|
||||||
"""NoneBot 主要配置。大小写不敏感。
|
"""NoneBot 主要配置。大小写不敏感。
|
||||||
|
|
||||||
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
||||||
@@ -169,7 +380,8 @@ class Config(BaseConfig):
|
|||||||
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
|
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_env_file: DotenvType = ".env", ".env.prod"
|
if TYPE_CHECKING:
|
||||||
|
_env_file: Optional[DOTENV_TYPE] = ".env", ".env.prod"
|
||||||
|
|
||||||
# nonebot configs
|
# nonebot configs
|
||||||
driver: str = "~fastapi"
|
driver: str = "~fastapi"
|
||||||
@@ -206,7 +418,7 @@ class Config(BaseConfig):
|
|||||||
"""API 请求超时时间,单位: 秒。"""
|
"""API 请求超时时间,单位: 秒。"""
|
||||||
|
|
||||||
# bot runtime configs
|
# bot runtime configs
|
||||||
superusers: Set[str] = set()
|
superusers: set[str] = set()
|
||||||
"""机器人超级用户。
|
"""机器人超级用户。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
@@ -214,9 +426,9 @@ class Config(BaseConfig):
|
|||||||
SUPERUSERS=["12345789"]
|
SUPERUSERS=["12345789"]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
nickname: Set[str] = set()
|
nickname: set[str] = set()
|
||||||
"""机器人昵称。"""
|
"""机器人昵称。"""
|
||||||
command_start: Set[str] = {"/"}
|
command_start: set[str] = {"/"}
|
||||||
"""命令的起始标记,用于判断一条消息是不是命令。
|
"""命令的起始标记,用于判断一条消息是不是命令。
|
||||||
|
|
||||||
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||||
@@ -226,7 +438,7 @@ class Config(BaseConfig):
|
|||||||
COMMAND_START=["/", ""]
|
COMMAND_START=["/", ""]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
command_sep: Set[str] = {"."}
|
command_sep: set[str] = {"."}
|
||||||
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
|
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
|
||||||
|
|
||||||
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||||
@@ -241,9 +453,8 @@ class Config(BaseConfig):
|
|||||||
|
|
||||||
用法:
|
用法:
|
||||||
```conf
|
```conf
|
||||||
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
|
SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]
|
||||||
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
|
SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
|
||||||
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -254,11 +465,21 @@ class Config(BaseConfig):
|
|||||||
# custom configs can be assigned during nonebot.init
|
# custom configs can be assigned during nonebot.init
|
||||||
# or from env file using json loads
|
# or from env file using json loads
|
||||||
|
|
||||||
class Config:
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
env_file = ".env", ".env.prod"
|
model_config = SettingsConfig(env_file=(".env", ".env.prod"))
|
||||||
|
else: # pragma: pydantic-v1
|
||||||
|
|
||||||
|
class Config( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
SettingsConfig
|
||||||
|
):
|
||||||
|
env_file = ".env", ".env.prod"
|
||||||
|
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
"CustomEnvSettings": False,
|
"SettingsError": False,
|
||||||
"BaseConfig": False,
|
"BaseSettingsSource": False,
|
||||||
|
"InitSettingsSource": False,
|
||||||
|
"DotEnvSettingsSource": False,
|
||||||
|
"SettingsConfig": False,
|
||||||
|
"BaseSettings": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
|
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 9
|
sidebar_position: 9
|
||||||
description: nonebot.consts 模块
|
description: nonebot.consts 模块
|
||||||
"""
|
"""
|
||||||
@@ -20,6 +22,10 @@ REJECT_TARGET: Literal["_current_target"] = "_current_target"
|
|||||||
"""当前 `reject` 目标存储 key"""
|
"""当前 `reject` 目标存储 key"""
|
||||||
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
|
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
|
||||||
"""下一个 `reject` 目标存储 key"""
|
"""下一个 `reject` 目标存储 key"""
|
||||||
|
PAUSE_PROMPT_RESULT_KEY: Literal["_pause_result"] = "_pause_result"
|
||||||
|
"""`pause` prompt 发送结果存储 key"""
|
||||||
|
REJECT_PROMPT_RESULT_KEY: Literal["_reject_{key}_result"] = "_reject_{key}_result"
|
||||||
|
"""`reject` prompt 发送结果存储 key"""
|
||||||
|
|
||||||
# used by Rule
|
# used by Rule
|
||||||
PREFIX_KEY: Literal["_prefix"] = "_prefix"
|
PREFIX_KEY: Literal["_prefix"] = "_prefix"
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
"""本模块模块实现了依赖注入的定义与处理。
|
"""本模块模块实现了依赖注入的定义与处理。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
description: nonebot.dependencies 模块
|
description: nonebot.dependencies 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import asyncio
|
from collections.abc import Awaitable, Iterable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from functools import partial
|
||||||
import inspect
|
import inspect
|
||||||
from dataclasses import field, dataclass
|
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Generic,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
|
||||||
Iterable,
|
|
||||||
Optional,
|
|
||||||
Awaitable,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import BaseConfig
|
import anyio
|
||||||
from pydantic.schema import get_annotation_from_field_info
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
|
||||||
|
|
||||||
|
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
|
||||||
|
from nonebot.exception import SkippedException
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.typing import _DependentCallable
|
from nonebot.typing import _DependentCallable
|
||||||
from nonebot.exception import SkippedException
|
from nonebot.utils import (
|
||||||
from nonebot.utils import run_sync, is_coroutine_callable
|
flatten_exception_group,
|
||||||
|
is_coroutine_callable,
|
||||||
|
run_coro_with_shield,
|
||||||
|
run_sync,
|
||||||
|
)
|
||||||
|
|
||||||
from .utils import check_field_type, get_typed_signature
|
from .utils import check_field_type, get_typed_signature
|
||||||
|
|
||||||
@@ -51,13 +46,13 @@ class Param(abc.ABC, FieldInfo):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> Optional["Param"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
|
cls, value: Any, allow_types: tuple[type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> Optional["Param"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -69,10 +64,6 @@ class Param(abc.ABC, FieldInfo):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(BaseConfig):
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Dependent(Generic[R]):
|
class Dependent(Generic[R]):
|
||||||
"""依赖注入容器
|
"""依赖注入容器
|
||||||
@@ -86,8 +77,8 @@ class Dependent(Generic[R]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
call: _DependentCallable[R]
|
call: _DependentCallable[R]
|
||||||
params: Tuple[ModelField, ...] = field(default_factory=tuple)
|
params: tuple[ModelField, ...] = field(default_factory=tuple)
|
||||||
parameterless: Tuple[Param, ...] = field(default_factory=tuple)
|
parameterless: tuple[Param, ...] = field(default_factory=tuple)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if inspect.isfunction(self.call) or inspect.isclass(self.call):
|
if inspect.isfunction(self.call) or inspect.isclass(self.call):
|
||||||
@@ -101,7 +92,16 @@ class Dependent(Generic[R]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __call__(self, **kwargs: Any) -> R:
|
async def __call__(self, **kwargs: Any) -> R:
|
||||||
try:
|
exception: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||||
|
|
||||||
|
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||||
|
nonlocal exception
|
||||||
|
exception = exc_group
|
||||||
|
# raise one of the exceptions instead
|
||||||
|
excs = list(flatten_exception_group(exc_group))
|
||||||
|
logger.trace(f"{self} skipped due to {excs}")
|
||||||
|
|
||||||
|
with catch({SkippedException: _handle_skipped}):
|
||||||
# do pre-check
|
# do pre-check
|
||||||
await self.check(**kwargs)
|
await self.check(**kwargs)
|
||||||
|
|
||||||
@@ -113,24 +113,19 @@ class Dependent(Generic[R]):
|
|||||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||||
else:
|
else:
|
||||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||||
except SkippedException as e:
|
|
||||||
logger.trace(f"{self} skipped due to {e}")
|
raise exception
|
||||||
raise
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_params(
|
def parse_params(
|
||||||
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
call: _DependentCallable[R], allow_types: tuple[type[Param], ...]
|
||||||
) -> Tuple[ModelField, ...]:
|
) -> tuple[ModelField, ...]:
|
||||||
fields: List[ModelField] = []
|
fields: list[ModelField] = []
|
||||||
params = get_typed_signature(call).parameters.values()
|
params = get_typed_signature(call).parameters.values()
|
||||||
|
|
||||||
for param in params:
|
for param in params:
|
||||||
default_value = Required
|
if isinstance(param.default, Param):
|
||||||
if param.default != param.empty:
|
field_info = param.default
|
||||||
default_value = param.default
|
|
||||||
|
|
||||||
if isinstance(default_value, Param):
|
|
||||||
field_info = default_value
|
|
||||||
else:
|
else:
|
||||||
for allow_type in allow_types:
|
for allow_type in allow_types:
|
||||||
if field_info := allow_type._check_param(param, allow_types):
|
if field_info := allow_type._check_param(param, allow_types):
|
||||||
@@ -141,25 +136,13 @@ class Dependent(Generic[R]):
|
|||||||
f"for function {call} with type {param.annotation}"
|
f"for function {call} with type {param.annotation}"
|
||||||
)
|
)
|
||||||
|
|
||||||
default_value = field_info.default
|
|
||||||
|
|
||||||
annotation: Any = Any
|
annotation: Any = Any
|
||||||
required = default_value == Required
|
if param.annotation is not param.empty:
|
||||||
if param.annotation != param.empty:
|
|
||||||
annotation = param.annotation
|
annotation = param.annotation
|
||||||
annotation = get_annotation_from_field_info(
|
|
||||||
annotation, field_info, param.name
|
|
||||||
)
|
|
||||||
|
|
||||||
fields.append(
|
fields.append(
|
||||||
ModelField(
|
ModelField.construct(
|
||||||
name=param.name,
|
name=param.name, annotation=annotation, field_info=field_info
|
||||||
type_=annotation,
|
|
||||||
class_validators=None,
|
|
||||||
model_config=CustomConfig,
|
|
||||||
default=None if required else default_value,
|
|
||||||
required=required,
|
|
||||||
field_info=field_info,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,9 +150,9 @@ class Dependent(Generic[R]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_parameterless(
|
def parse_parameterless(
|
||||||
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
|
parameterless: tuple[Any, ...], allow_types: tuple[type[Param], ...]
|
||||||
) -> Tuple[Param, ...]:
|
) -> tuple[Param, ...]:
|
||||||
parameterless_params: List[Param] = []
|
parameterless_params: list[Param] = []
|
||||||
for value in parameterless:
|
for value in parameterless:
|
||||||
for allow_type in allow_types:
|
for allow_type in allow_types:
|
||||||
if param := allow_type._check_parameterless(value, allow_types):
|
if param := allow_type._check_parameterless(value, allow_types):
|
||||||
@@ -185,7 +168,7 @@ class Dependent(Generic[R]):
|
|||||||
*,
|
*,
|
||||||
call: _DependentCallable[R],
|
call: _DependentCallable[R],
|
||||||
parameterless: Optional[Iterable[Any]] = None,
|
parameterless: Optional[Iterable[Any]] = None,
|
||||||
allow_types: Iterable[Type[Param]],
|
allow_types: Iterable[type[Param]],
|
||||||
) -> "Dependent[R]":
|
) -> "Dependent[R]":
|
||||||
allow_types = tuple(allow_types)
|
allow_types = tuple(allow_types)
|
||||||
|
|
||||||
@@ -199,29 +182,48 @@ class Dependent(Generic[R]):
|
|||||||
return cls(call, params, parameterless_params)
|
return cls(call, params, parameterless_params)
|
||||||
|
|
||||||
async def check(self, **params: Any) -> None:
|
async def check(self, **params: Any) -> None:
|
||||||
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
if self.parameterless:
|
||||||
await asyncio.gather(
|
async with anyio.create_task_group() as tg:
|
||||||
*(cast(Param, param.field_info)._check(**params) for param in self.params)
|
for param in self.parameterless:
|
||||||
)
|
tg.start_soon(partial(param._check, **params))
|
||||||
|
|
||||||
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
if self.params:
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for param in self.params:
|
||||||
|
tg.start_soon(
|
||||||
|
partial(cast(Param, param.field_info)._check, **params)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:
|
||||||
param = cast(Param, field.field_info)
|
param = cast(Param, field.field_info)
|
||||||
value = await param._solve(**params)
|
value = await param._solve(**params)
|
||||||
if value is Undefined:
|
if value is PydanticUndefined:
|
||||||
value = field.get_default()
|
value = field.get_default()
|
||||||
v = check_field_type(field, value)
|
v = check_field_type(field, value)
|
||||||
return v if param.validate else value
|
return v if param.validate else value
|
||||||
|
|
||||||
async def solve(self, **params: Any) -> Dict[str, Any]:
|
async def solve(self, **params: Any) -> dict[str, Any]:
|
||||||
# solve parameterless
|
# solve parameterless
|
||||||
for param in self.parameterless:
|
for param in self.parameterless:
|
||||||
await param._solve(**params)
|
await param._solve(**params)
|
||||||
|
|
||||||
# solve param values
|
# solve param values
|
||||||
values = await asyncio.gather(
|
result: dict[str, Any] = {}
|
||||||
*(self._solve_field(field, params) for field in self.params)
|
if not self.params:
|
||||||
)
|
return result
|
||||||
return {field.name: value for field, value in zip(self.params, values)}
|
|
||||||
|
async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:
|
||||||
|
value = await self._solve_field(field, params)
|
||||||
|
result[field.name] = value
|
||||||
|
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for field in self.params:
|
||||||
|
# shield the task to prevent cancellation
|
||||||
|
# when one of the tasks raises an exception
|
||||||
|
# this will improve the dependency cache reusability
|
||||||
|
tg.start_soon(run_coro_with_shield, _solve_field(field, params))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
__autodoc__ = {"CustomConfig": False}
|
__autodoc__ = {"CustomConfig": False}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
description: nonebot.dependencies.utils 模块
|
description: nonebot.dependencies.utils 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Dict, Callable, ForwardRef
|
from typing import Any, Callable, ForwardRef
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.fields import ModelField
|
|
||||||
from pydantic.typing import evaluate_forwardref
|
|
||||||
|
|
||||||
|
from nonebot.compat import ModelField
|
||||||
from nonebot.exception import TypeMisMatch
|
from nonebot.exception import TypeMisMatch
|
||||||
|
from nonebot.typing import evaluate_forwardref
|
||||||
|
|
||||||
|
|
||||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||||
@@ -31,7 +33,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|||||||
return inspect.Signature(typed_params)
|
return inspect.Signature(typed_params)
|
||||||
|
|
||||||
|
|
||||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
def get_typed_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any:
|
||||||
"""获取参数的类型注解"""
|
"""获取参数的类型注解"""
|
||||||
|
|
||||||
annotation = param.annotation
|
annotation = param.annotation
|
||||||
@@ -50,7 +52,7 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
|
|||||||
def check_field_type(field: ModelField, value: Any) -> Any:
|
def check_field_type(field: ModelField, value: Any) -> Any:
|
||||||
"""检查字段类型是否匹配"""
|
"""检查字段类型是否匹配"""
|
||||||
|
|
||||||
v, errs_ = field.validate(value, {}, loc=())
|
try:
|
||||||
if errs_:
|
return field.validate_value(value)
|
||||||
|
except ValueError:
|
||||||
raise TypeMisMatch(field, value)
|
raise TypeMisMatch(field, value)
|
||||||
return v
|
|
||||||
|
|||||||
@@ -3,28 +3,31 @@
|
|||||||
各驱动请继承以下基类。
|
各驱动请继承以下基类。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
description: nonebot.drivers 模块
|
description: nonebot.drivers 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from nonebot.internal.driver import URL as URL
|
from nonebot.internal.driver import URL as URL
|
||||||
from nonebot.internal.driver import Mixin as Mixin
|
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||||
from nonebot.internal.driver import Driver as Driver
|
|
||||||
from nonebot.internal.driver import Cookies as Cookies
|
from nonebot.internal.driver import Cookies as Cookies
|
||||||
|
from nonebot.internal.driver import Driver as Driver
|
||||||
|
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
||||||
|
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
||||||
|
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||||
|
from nonebot.internal.driver import HTTPClientSession as HTTPClientSession
|
||||||
|
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
||||||
|
from nonebot.internal.driver import Mixin as Mixin
|
||||||
from nonebot.internal.driver import Request as Request
|
from nonebot.internal.driver import Request as Request
|
||||||
from nonebot.internal.driver import Response as Response
|
from nonebot.internal.driver import Response as Response
|
||||||
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
|
||||||
from nonebot.internal.driver import WebSocket as WebSocket
|
|
||||||
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
|
||||||
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
|
||||||
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
|
||||||
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
|
||||||
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
||||||
from nonebot.internal.driver import combine_driver as combine_driver
|
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
||||||
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
from nonebot.internal.driver import WebSocket as WebSocket
|
||||||
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
|
||||||
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
||||||
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
from nonebot.internal.driver import combine_driver as combine_driver
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
"URL": True,
|
"URL": True,
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
from typing_extensions import TypeAlias
|
|
||||||
from typing import Any, List, Union, Callable, Awaitable, cast
|
|
||||||
|
|
||||||
from nonebot.utils import run_sync, is_coroutine_callable
|
|
||||||
|
|
||||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
|
||||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
|
||||||
LIFESPAN_FUNC: TypeAlias = 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()
|
|
||||||
@@ -11,24 +11,33 @@ pip install nonebot2[aiohttp]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 2
|
sidebar_position: 2
|
||||||
description: nonebot.drivers.aiohttp 模块
|
description: nonebot.drivers.aiohttp 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing_extensions import override
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import TYPE_CHECKING, AsyncGenerator
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from multidict import CIMultiDict
|
||||||
|
|
||||||
from nonebot.drivers import Request, Response
|
|
||||||
from nonebot.exception import WebSocketClosed
|
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
|
||||||
from nonebot.drivers import (
|
from nonebot.drivers import (
|
||||||
HTTPVersion,
|
URL,
|
||||||
HTTPClientMixin,
|
HTTPClientMixin,
|
||||||
|
HTTPClientSession,
|
||||||
|
HTTPVersion,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
WebSocketClientMixin,
|
WebSocketClientMixin,
|
||||||
combine_driver,
|
combine_driver,
|
||||||
)
|
)
|
||||||
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -39,6 +48,105 @@ except ModuleNotFoundError as e: # pragma: no cover
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
class Session(HTTPClientSession):
|
||||||
|
@override
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
params: QueryTypes = None,
|
||||||
|
headers: HeaderTypes = None,
|
||||||
|
cookies: CookieTypes = None,
|
||||||
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
proxy: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self._client: Optional[aiohttp.ClientSession] = None
|
||||||
|
|
||||||
|
self._params = URL.build(query=params).query if params is not None else None
|
||||||
|
|
||||||
|
self._headers = CIMultiDict(headers) if headers is not None else None
|
||||||
|
self._cookies = tuple(
|
||||||
|
(cookie.name, cookie.value)
|
||||||
|
for cookie in Cookies(cookies)
|
||||||
|
if cookie.value is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
version = HTTPVersion(version)
|
||||||
|
if version == HTTPVersion.H10:
|
||||||
|
self._version = aiohttp.HttpVersion10
|
||||||
|
elif version == HTTPVersion.H11:
|
||||||
|
self._version = aiohttp.HttpVersion11
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported HTTP version: {version}")
|
||||||
|
|
||||||
|
self._timeout = timeout
|
||||||
|
self._proxy = proxy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> aiohttp.ClientSession:
|
||||||
|
if self._client is None:
|
||||||
|
raise RuntimeError("Session is not initialized")
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def request(self, setup: Request) -> Response:
|
||||||
|
if self._params:
|
||||||
|
url = setup.url.with_query({**self._params, **setup.url.query})
|
||||||
|
else:
|
||||||
|
url = setup.url
|
||||||
|
|
||||||
|
data = setup.data
|
||||||
|
if setup.files:
|
||||||
|
data = aiohttp.FormData(data or {}, quote_fields=False)
|
||||||
|
for name, file in setup.files:
|
||||||
|
data.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||||
|
|
||||||
|
cookies = (
|
||||||
|
(cookie.name, cookie.value)
|
||||||
|
for cookie in setup.cookies
|
||||||
|
if cookie.value is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(setup.timeout)
|
||||||
|
|
||||||
|
async with await self.client.request(
|
||||||
|
setup.method,
|
||||||
|
url,
|
||||||
|
data=setup.content or data,
|
||||||
|
json=setup.json,
|
||||||
|
cookies=cookies,
|
||||||
|
headers=setup.headers,
|
||||||
|
proxy=setup.proxy or self._proxy,
|
||||||
|
timeout=timeout,
|
||||||
|
) as response:
|
||||||
|
return Response(
|
||||||
|
response.status,
|
||||||
|
headers=response.headers.copy(),
|
||||||
|
content=await response.read(),
|
||||||
|
request=setup,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def setup(self) -> None:
|
||||||
|
if self._client is not None:
|
||||||
|
raise RuntimeError("Session has already been initialized")
|
||||||
|
self._client = aiohttp.ClientSession(
|
||||||
|
cookies=self._cookies,
|
||||||
|
headers=self._headers,
|
||||||
|
version=self._version,
|
||||||
|
timeout=self._timeout,
|
||||||
|
trust_env=True,
|
||||||
|
)
|
||||||
|
await self._client.__aenter__()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def close(self) -> None:
|
||||||
|
try:
|
||||||
|
if self._client is not None:
|
||||||
|
await self._client.close()
|
||||||
|
finally:
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
|
||||||
class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||||
"""AIOHTTP Mixin"""
|
"""AIOHTTP Mixin"""
|
||||||
|
|
||||||
@@ -49,42 +157,8 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
if setup.version == HTTPVersion.H10:
|
async with self.get_session() as session:
|
||||||
version = aiohttp.HttpVersion10
|
return await session.request(setup)
|
||||||
elif setup.version == HTTPVersion.H11:
|
|
||||||
version = aiohttp.HttpVersion11
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(setup.timeout)
|
|
||||||
|
|
||||||
data = setup.data
|
|
||||||
if setup.files:
|
|
||||||
data = aiohttp.FormData(data or {})
|
|
||||||
for name, file in setup.files:
|
|
||||||
data.add_field(name, file[1], content_type=file[2], filename=file[0])
|
|
||||||
|
|
||||||
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(
|
|
||||||
setup.method,
|
|
||||||
setup.url,
|
|
||||||
data=setup.content or data,
|
|
||||||
json=setup.json,
|
|
||||||
headers=setup.headers,
|
|
||||||
timeout=timeout,
|
|
||||||
proxy=setup.proxy,
|
|
||||||
) as response:
|
|
||||||
return Response(
|
|
||||||
response.status,
|
|
||||||
headers=response.headers.copy(),
|
|
||||||
content=await response.read(),
|
|
||||||
request=setup,
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -96,16 +170,37 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientWSTimeout(ws_close=setup.timeout or 10.0) # type: ignore
|
||||||
|
|
||||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||||
async with session.ws_connect(
|
async with session.ws_connect(
|
||||||
setup.url,
|
setup.url,
|
||||||
method=setup.method,
|
method=setup.method,
|
||||||
timeout=setup.timeout or 10,
|
timeout=timeout,
|
||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
proxy=setup.proxy,
|
proxy=setup.proxy,
|
||||||
) as ws:
|
) as ws:
|
||||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_session(
|
||||||
|
self,
|
||||||
|
params: QueryTypes = None,
|
||||||
|
headers: HeaderTypes = None,
|
||||||
|
cookies: CookieTypes = None,
|
||||||
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
proxy: Optional[str] = None,
|
||||||
|
) -> Session:
|
||||||
|
return Session(
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
|
version=version,
|
||||||
|
timeout=timeout,
|
||||||
|
proxy=proxy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(BaseWebSocket):
|
class WebSocket(BaseWebSocket):
|
||||||
"""AIOHTTP Websocket Wrapper"""
|
"""AIOHTTP Websocket Wrapper"""
|
||||||
@@ -131,8 +226,8 @@ class WebSocket(BaseWebSocket):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def close(self, code: int = 1000):
|
async def close(self, code: int = 1000, reason: str = ""):
|
||||||
await self.websocket.close(code=code)
|
await self.websocket.close(code=code, message=reason.encode("utf-8"))
|
||||||
await self.session.close()
|
await self.session.close()
|
||||||
|
|
||||||
async def _receive(self) -> aiohttp.WSMessage:
|
async def _receive(self) -> aiohttp.WSMessage:
|
||||||
@@ -179,8 +274,7 @@ class WebSocket(BaseWebSocket):
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
class Driver(Mixin, NoneDriver):
|
class Driver(Mixin, NoneDriver): ...
|
||||||
...
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
Driver = combine_driver(NoneDriver, Mixin)
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
|||||||
@@ -11,36 +11,35 @@ pip install nonebot2[fastapi]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
description: nonebot.drivers.fastapi 模块
|
description: nonebot.drivers.fastapi 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
import logging
|
||||||
|
from typing import Any, Optional, Union
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from nonebot.config import Env
|
from nonebot.compat import model_dump, type_validate_python
|
||||||
from nonebot.drivers import ASGIMixin
|
|
||||||
from nonebot.exception import WebSocketClosed
|
|
||||||
from nonebot.internal.driver import FileTypes
|
|
||||||
from nonebot.drivers import Driver as BaseDriver
|
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
|
from nonebot.config import Env
|
||||||
|
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.drivers import Request as BaseRequest
|
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 HTTPServerSetup, WebSocketServerSetup
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.internal.driver import FileTypes
|
||||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvicorn
|
|
||||||
from fastapi.responses import Response
|
|
||||||
from fastapi import FastAPI, Request, UploadFile, status
|
from fastapi import FastAPI, Request, UploadFile, status
|
||||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
from fastapi.responses import Response
|
||||||
|
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
|
||||||
|
import uvicorn
|
||||||
except ModuleNotFoundError as e: # pragma: no cover
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Please install FastAPI first to use this driver. "
|
"Please install FastAPI first to use this driver. "
|
||||||
@@ -61,7 +60,7 @@ def catch_closed(func):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseModel):
|
||||||
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
|
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
|
||||||
|
|
||||||
fastapi_openapi_url: Optional[str] = None
|
fastapi_openapi_url: Optional[str] = None
|
||||||
@@ -74,20 +73,17 @@ class Config(BaseSettings):
|
|||||||
"""是否包含适配器路由的 schema,默认为 `True`"""
|
"""是否包含适配器路由的 schema,默认为 `True`"""
|
||||||
fastapi_reload: bool = False
|
fastapi_reload: bool = False
|
||||||
"""开启/关闭冷重载"""
|
"""开启/关闭冷重载"""
|
||||||
fastapi_reload_dirs: Optional[List[str]] = None
|
fastapi_reload_dirs: Optional[list[str]] = None
|
||||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
fastapi_reload_delay: float = 0.25
|
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_extra: dict[str, Any] = {}
|
||||||
"""传递给 `FastAPI` 的其他参数。"""
|
"""传递给 `FastAPI` 的其他参数。"""
|
||||||
|
|
||||||
class Config:
|
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
|
|
||||||
class Driver(BaseDriver, ASGIMixin):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""FastAPI 驱动框架。"""
|
"""FastAPI 驱动框架。"""
|
||||||
@@ -95,9 +91,7 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
super().__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self.fastapi_config: Config = Config(**config.dict())
|
self.fastapi_config: Config = type_validate_python(Config, model_dump(config))
|
||||||
|
|
||||||
self._lifespan = Lifespan()
|
|
||||||
|
|
||||||
self._server_app = FastAPI(
|
self._server_app = FastAPI(
|
||||||
lifespan=self._lifespan_manager,
|
lifespan=self._lifespan_manager,
|
||||||
@@ -155,14 +149,6 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
name=setup.name,
|
name=setup.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
return self._lifespan.on_startup(func)
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
return self._lifespan.on_shutdown(func)
|
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def _lifespan_manager(self, app: FastAPI):
|
async def _lifespan_manager(self, app: FastAPI):
|
||||||
await self._lifespan.startup()
|
await self._lifespan.startup()
|
||||||
@@ -176,7 +162,7 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*args,
|
||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -221,7 +207,7 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
json = await request.json()
|
json = await request.json()
|
||||||
|
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
files: Optional[List[Tuple[str, FileTypes]]] = None
|
files: Optional[list[tuple[str, FileTypes]]] = None
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
data = {}
|
data = {}
|
||||||
|
|||||||
@@ -11,21 +11,28 @@ pip install nonebot2[httpx]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
description: nonebot.drivers.httpx 模块
|
description: nonebot.drivers.httpx 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from multidict import CIMultiDict
|
||||||
|
|
||||||
from nonebot.drivers import (
|
from nonebot.drivers import (
|
||||||
|
URL,
|
||||||
|
HTTPClientMixin,
|
||||||
|
HTTPClientSession,
|
||||||
|
HTTPVersion,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
HTTPVersion,
|
|
||||||
HTTPClientMixin,
|
|
||||||
combine_driver,
|
combine_driver,
|
||||||
)
|
)
|
||||||
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
|
from nonebot.internal.driver import Cookies, CookieTypes, HeaderTypes, QueryTypes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
@@ -36,6 +43,81 @@ except ModuleNotFoundError as e: # pragma: no cover
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
class Session(HTTPClientSession):
|
||||||
|
@override
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
params: QueryTypes = None,
|
||||||
|
headers: HeaderTypes = None,
|
||||||
|
cookies: CookieTypes = None,
|
||||||
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
proxy: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self._client: Optional[httpx.AsyncClient] = None
|
||||||
|
|
||||||
|
self._params = (
|
||||||
|
tuple(URL.build(query=params).query.items()) if params is not None else None
|
||||||
|
)
|
||||||
|
self._headers = (
|
||||||
|
tuple(CIMultiDict(headers).items()) if headers is not None else None
|
||||||
|
)
|
||||||
|
self._cookies = Cookies(cookies)
|
||||||
|
self._version = HTTPVersion(version)
|
||||||
|
self._timeout = timeout
|
||||||
|
self._proxy = proxy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> httpx.AsyncClient:
|
||||||
|
if self._client is None:
|
||||||
|
raise RuntimeError("Session is not initialized")
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def request(self, setup: Request) -> Response:
|
||||||
|
response = await self.client.request(
|
||||||
|
setup.method,
|
||||||
|
str(setup.url),
|
||||||
|
content=setup.content,
|
||||||
|
data=setup.data,
|
||||||
|
files=setup.files,
|
||||||
|
json=setup.json,
|
||||||
|
# ensure the params priority
|
||||||
|
params=setup.url.raw_query_string,
|
||||||
|
headers=tuple(setup.headers.items()),
|
||||||
|
cookies=setup.cookies.jar,
|
||||||
|
timeout=setup.timeout,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
response.status_code,
|
||||||
|
headers=response.headers.multi_items(),
|
||||||
|
content=response.content,
|
||||||
|
request=setup,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def setup(self) -> None:
|
||||||
|
if self._client is not None:
|
||||||
|
raise RuntimeError("Session has already been initialized")
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
params=self._params,
|
||||||
|
headers=self._headers,
|
||||||
|
cookies=self._cookies.jar,
|
||||||
|
http2=self._version == HTTPVersion.H2,
|
||||||
|
proxy=self._proxy,
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
await self._client.__aenter__()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def close(self) -> None:
|
||||||
|
try:
|
||||||
|
if self._client is not None:
|
||||||
|
await self._client.aclose()
|
||||||
|
finally:
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
|
||||||
class Mixin(HTTPClientMixin):
|
class Mixin(HTTPClientMixin):
|
||||||
"""HTTPX Mixin"""
|
"""HTTPX Mixin"""
|
||||||
|
|
||||||
@@ -46,34 +128,34 @@ class Mixin(HTTPClientMixin):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
async with httpx.AsyncClient(
|
async with self.get_session(
|
||||||
cookies=setup.cookies.jar,
|
version=setup.version, proxy=setup.proxy
|
||||||
http2=setup.version == HTTPVersion.H2,
|
) as session:
|
||||||
proxies=setup.proxy,
|
return await session.request(setup)
|
||||||
follow_redirects=True,
|
|
||||||
) as client:
|
@override
|
||||||
response = await client.request(
|
def get_session(
|
||||||
setup.method,
|
self,
|
||||||
str(setup.url),
|
params: QueryTypes = None,
|
||||||
content=setup.content,
|
headers: HeaderTypes = None,
|
||||||
data=setup.data,
|
cookies: CookieTypes = None,
|
||||||
json=setup.json,
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
files=setup.files,
|
timeout: Optional[float] = None,
|
||||||
headers=tuple(setup.headers.items()),
|
proxy: Optional[str] = None,
|
||||||
timeout=setup.timeout,
|
) -> Session:
|
||||||
)
|
return Session(
|
||||||
return Response(
|
params=params,
|
||||||
response.status_code,
|
headers=headers,
|
||||||
headers=response.headers.multi_items(),
|
cookies=cookies,
|
||||||
content=response.content,
|
version=version,
|
||||||
request=setup,
|
timeout=timeout,
|
||||||
)
|
proxy=proxy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
class Driver(Mixin, NoneDriver):
|
class Driver(Mixin, NoneDriver): ...
|
||||||
...
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
Driver = combine_driver(NoneDriver, Mixin)
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
|||||||
@@ -5,21 +5,25 @@
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 6
|
sidebar_position: 6
|
||||||
description: nonebot.drivers.none 模块
|
description: nonebot.drivers.none 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import signal
|
import signal
|
||||||
import asyncio
|
from typing import Optional
|
||||||
import threading
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from nonebot.log import logger
|
import anyio
|
||||||
from nonebot.consts import WINDOWS
|
from anyio.abc import TaskGroup
|
||||||
from nonebot.config import Env, Config
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
from nonebot.drivers import Driver as BaseDriver
|
|
||||||
|
|
||||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
from nonebot.config import Config, Env
|
||||||
|
from nonebot.consts import WINDOWS
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.utils import flatten_exception_group
|
||||||
|
|
||||||
HANDLED_SIGNALS = (
|
HANDLED_SIGNALS = (
|
||||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||||
@@ -35,10 +39,8 @@ class Driver(BaseDriver):
|
|||||||
def __init__(self, env: Env, config: Config):
|
def __init__(self, env: Env, config: Config):
|
||||||
super().__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self._lifespan = Lifespan()
|
self.should_exit: anyio.Event = anyio.Event()
|
||||||
|
self.force_exit: anyio.Event = anyio.Event()
|
||||||
self.should_exit: asyncio.Event = asyncio.Event()
|
|
||||||
self.force_exit: bool = False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
@@ -52,97 +54,102 @@ class Driver(BaseDriver):
|
|||||||
"""none driver 使用的 logger"""
|
"""none driver 使用的 logger"""
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
"""注册一个启动时执行的函数"""
|
|
||||||
return self._lifespan.on_startup(func)
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
|
||||||
"""注册一个停止时执行的函数"""
|
|
||||||
return self._lifespan.on_shutdown(func)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
"""启动 none driver"""
|
"""启动 none driver"""
|
||||||
super().run(*args, **kwargs)
|
super().run(*args, **kwargs)
|
||||||
loop = asyncio.get_event_loop()
|
anyio.run(self._serve)
|
||||||
loop.run_until_complete(self._serve())
|
|
||||||
|
|
||||||
async def _serve(self):
|
async def _serve(self):
|
||||||
self._install_signal_handlers()
|
async with anyio.create_task_group() as driver_tg:
|
||||||
await self._startup()
|
driver_tg.start_soon(self._handle_signals)
|
||||||
if self.should_exit.is_set():
|
driver_tg.start_soon(self._listen_force_exit, driver_tg)
|
||||||
return
|
driver_tg.start_soon(self._handle_lifespan, driver_tg)
|
||||||
await self._main_loop()
|
|
||||||
await self._shutdown()
|
|
||||||
|
|
||||||
async def _startup(self):
|
async def _handle_signals(self):
|
||||||
try:
|
try:
|
||||||
await self._lifespan.startup()
|
with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:
|
||||||
except Exception as e:
|
async for sig in signal_receiver:
|
||||||
logger.opt(colors=True, exception=e).error(
|
self.exit(force=self.should_exit.is_set())
|
||||||
"<r><bg #f8bbd0>Error when running startup function. "
|
|
||||||
"Ignored!</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Application startup completed.")
|
|
||||||
|
|
||||||
async def _main_loop(self):
|
|
||||||
await self.should_exit.wait()
|
|
||||||
|
|
||||||
async def _shutdown(self):
|
|
||||||
logger.info("Shutting down")
|
|
||||||
|
|
||||||
logger.info("Waiting for application shutdown.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self._lifespan.shutdown()
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
|
||||||
"Ignored!</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
|
|
||||||
for task in asyncio.all_tasks():
|
|
||||||
if task is not asyncio.current_task() and not task.done():
|
|
||||||
task.cancel()
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
||||||
if tasks and not self.force_exit:
|
|
||||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
|
||||||
while tasks and not self.force_exit:
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
||||||
|
|
||||||
for task in tasks:
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
|
||||||
|
|
||||||
logger.info("Application shutdown complete.")
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.stop()
|
|
||||||
|
|
||||||
def _install_signal_handlers(self) -> None:
|
|
||||||
if threading.current_thread() is not threading.main_thread():
|
|
||||||
# Signals can only be listened to from the main thread.
|
|
||||||
return
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
try:
|
|
||||||
for sig in HANDLED_SIGNALS:
|
|
||||||
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_legacy_signal)
|
||||||
|
|
||||||
def _handle_exit(self, sig, frame):
|
# backport for Windows signal handling
|
||||||
|
def _handle_legacy_signal(self, sig, frame):
|
||||||
self.exit(force=self.should_exit.is_set())
|
self.exit(force=self.should_exit.is_set())
|
||||||
|
|
||||||
|
async def _handle_lifespan(self, tg: TaskGroup):
|
||||||
|
try:
|
||||||
|
await self._startup()
|
||||||
|
|
||||||
|
if self.should_exit.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._listen_exit()
|
||||||
|
|
||||||
|
await self._shutdown()
|
||||||
|
finally:
|
||||||
|
tg.cancel_scope.cancel()
|
||||||
|
|
||||||
|
async def _startup(self):
|
||||||
|
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||||
|
self.should_exit.set()
|
||||||
|
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(
|
||||||
|
"<r><bg #f8bbd0>Error occurred while running startup hook."
|
||||||
|
"</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
logger.error(
|
||||||
|
"<r><bg #f8bbd0>Application startup failed. "
|
||||||
|
"Exiting.</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
|
with catch({Exception: handle_exception}):
|
||||||
|
await self._lifespan.startup()
|
||||||
|
|
||||||
|
if not self.should_exit.is_set():
|
||||||
|
logger.info("Application startup completed.")
|
||||||
|
|
||||||
|
async def _listen_exit(self, tg: Optional[TaskGroup] = None):
|
||||||
|
await self.should_exit.wait()
|
||||||
|
|
||||||
|
if tg is not None:
|
||||||
|
tg.cancel_scope.cancel()
|
||||||
|
|
||||||
|
async def _shutdown(self):
|
||||||
|
logger.info("Shutting down")
|
||||||
|
logger.info("Waiting for application shutdown. (CTRL+C to force quit)")
|
||||||
|
|
||||||
|
error_occurred: bool = False
|
||||||
|
|
||||||
|
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||||
|
nonlocal error_occurred
|
||||||
|
|
||||||
|
error_occurred = True
|
||||||
|
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(
|
||||||
|
"<r><bg #f8bbd0>Error occurred while running shutdown hook."
|
||||||
|
"</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
logger.error(
|
||||||
|
"<r><bg #f8bbd0>Application shutdown failed. "
|
||||||
|
"Exiting.</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
|
with catch({Exception: handle_exception}):
|
||||||
|
await self._lifespan.shutdown()
|
||||||
|
|
||||||
|
if not error_occurred:
|
||||||
|
logger.info("Application shutdown complete.")
|
||||||
|
|
||||||
|
async def _listen_force_exit(self, tg: TaskGroup):
|
||||||
|
await self.force_exit.wait()
|
||||||
|
tg.cancel_scope.cancel()
|
||||||
|
|
||||||
def exit(self, force: bool = False):
|
def exit(self, force: bool = False):
|
||||||
"""退出 none driver
|
"""退出 none driver
|
||||||
|
|
||||||
@@ -152,4 +159,4 @@ class Driver(BaseDriver):
|
|||||||
if not self.should_exit.is_set():
|
if not self.should_exit.is_set():
|
||||||
self.should_exit.set()
|
self.should_exit.set()
|
||||||
if force:
|
if force:
|
||||||
self.force_exit = True
|
self.force_exit.set()
|
||||||
|
|||||||
@@ -11,54 +11,43 @@ pip install nonebot2[quart]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 5
|
sidebar_position: 5
|
||||||
description: nonebot.drivers.quart 模块
|
description: nonebot.drivers.quart 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from typing import Any, Optional, Union, cast
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
|
||||||
Optional,
|
|
||||||
Coroutine,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from nonebot.config import Env
|
from nonebot.compat import model_dump, type_validate_python
|
||||||
from nonebot.drivers import ASGIMixin
|
|
||||||
from nonebot.exception import WebSocketClosed
|
|
||||||
from nonebot.internal.driver import FileTypes
|
|
||||||
from nonebot.drivers import Driver as BaseDriver
|
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
|
from nonebot.config import Env
|
||||||
|
from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.drivers import Request as BaseRequest
|
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 HTTPServerSetup, WebSocketServerSetup
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.internal.driver import FileTypes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvicorn
|
from quart import Quart, Request, Response
|
||||||
|
from quart import Websocket as QuartWebSocket
|
||||||
from quart import request as _request
|
from quart import request as _request
|
||||||
from quart.ctx import WebsocketContext
|
from quart.ctx import WebsocketContext
|
||||||
from quart.globals import websocket_ctx
|
|
||||||
from quart import Quart, Request, Response
|
|
||||||
from quart.datastructures import FileStorage
|
from quart.datastructures import FileStorage
|
||||||
from quart import Websocket as QuartWebSocket
|
from quart.globals import websocket_ctx
|
||||||
|
import uvicorn
|
||||||
except ModuleNotFoundError as e: # pragma: no cover
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Please install Quart first to use this driver. "
|
"Please install Quart first to use this driver. "
|
||||||
"Install with pip: `pip install nonebot2[quart]`"
|
"Install with pip: `pip install nonebot2[quart]`"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
|
||||||
|
|
||||||
|
|
||||||
def catch_closed(func):
|
def catch_closed(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -71,25 +60,22 @@ def catch_closed(func):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseModel):
|
||||||
"""Quart 驱动框架设置"""
|
"""Quart 驱动框架设置"""
|
||||||
|
|
||||||
quart_reload: bool = False
|
quart_reload: bool = False
|
||||||
"""开启/关闭冷重载"""
|
"""开启/关闭冷重载"""
|
||||||
quart_reload_dirs: Optional[List[str]] = None
|
quart_reload_dirs: Optional[list[str]] = None
|
||||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
quart_reload_delay: float = 0.25
|
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_extra: dict[str, Any] = {}
|
||||||
"""传递给 `Quart` 的其他参数。"""
|
"""传递给 `Quart` 的其他参数。"""
|
||||||
|
|
||||||
class Config:
|
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
|
|
||||||
class Driver(BaseDriver, ASGIMixin):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""Quart 驱动框架"""
|
"""Quart 驱动框架"""
|
||||||
@@ -97,11 +83,13 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
super().__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self.quart_config = Config(**config.dict())
|
self.quart_config = type_validate_python(Config, model_dump(config))
|
||||||
|
|
||||||
self._server_app = Quart(
|
self._server_app = Quart(
|
||||||
self.__class__.__qualname__, **self.quart_config.quart_extra
|
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||||
)
|
)
|
||||||
|
self._server_app.before_serving(self._lifespan.startup)
|
||||||
|
self._server_app.after_serving(self._lifespan.shutdown)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
@@ -150,22 +138,12 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
view_func=_handle,
|
view_func=_handle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
|
|
||||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
|
||||||
return self.server_app.before_serving(func) # type: ignore
|
|
||||||
|
|
||||||
@override
|
|
||||||
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
|
|
||||||
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
|
||||||
return self.server_app.after_serving(func) # type: ignore
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
*,
|
*args,
|
||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -207,7 +185,7 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
|
|
||||||
data = await request.form
|
data = await request.form
|
||||||
files_dict = await request.files
|
files_dict = await request.files
|
||||||
files: List[Tuple[str, FileTypes]] = []
|
files: list[tuple[str, FileTypes]] = []
|
||||||
key: str
|
key: str
|
||||||
value: FileStorage
|
value: FileStorage
|
||||||
for key, value in files_dict.items():
|
for key, value in files_dict.items():
|
||||||
|
|||||||
@@ -11,22 +11,24 @@ pip install nonebot2[websockets]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 4
|
sidebar_position: 4
|
||||||
description: nonebot.drivers.websockets 模块
|
description: nonebot.drivers.websockets 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
from collections.abc import AsyncGenerator, Coroutine
|
||||||
from functools import wraps
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from functools import wraps
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
||||||
from typing_extensions import ParamSpec, override
|
from typing_extensions import ParamSpec, override
|
||||||
from typing import TYPE_CHECKING, Union, TypeVar, Callable, Awaitable, AsyncGenerator
|
|
||||||
|
|
||||||
from nonebot.drivers import Request
|
from nonebot.drivers import Request, WebSocketClientMixin, combine_driver
|
||||||
from nonebot.log import LoguruHandler
|
|
||||||
from nonebot.exception import WebSocketClosed
|
|
||||||
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 WebSocketClientMixin, combine_driver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.log import LoguruHandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from websockets.exceptions import ConnectionClosed
|
from websockets.exceptions import ConnectionClosed
|
||||||
@@ -44,16 +46,15 @@ logger = logging.Logger("websockets.client", "INFO")
|
|||||||
logger.addHandler(LoguruHandler())
|
logger.addHandler(LoguruHandler())
|
||||||
|
|
||||||
|
|
||||||
def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
def catch_closed(
|
||||||
|
func: Callable[P, Coroutine[Any, Any, T]],
|
||||||
|
) -> Callable[P, Coroutine[Any, Any, T]]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except ConnectionClosed as e:
|
except ConnectionClosed as e:
|
||||||
if e.rcvd_then_sent:
|
raise WebSocketClosed(e.code, e.reason)
|
||||||
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason) # type: ignore
|
|
||||||
else:
|
|
||||||
raise WebSocketClosed(e.sent.code, e.sent.reason) # type: ignore
|
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@@ -69,6 +70,8 @@ class Mixin(WebSocketClientMixin):
|
|||||||
@override
|
@override
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||||
|
if setup.proxy is not None:
|
||||||
|
logger.warning("proxy is not supported by websockets driver")
|
||||||
connection = Connect(
|
connection = Connect(
|
||||||
str(setup.url),
|
str(setup.url),
|
||||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||||
@@ -131,8 +134,7 @@ class WebSocket(BaseWebSocket):
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
class Driver(Mixin, NoneDriver):
|
class Driver(Mixin, NoneDriver): ...
|
||||||
...
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
Driver = combine_driver(NoneDriver, Mixin)
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
|||||||
@@ -25,13 +25,15 @@ NoneBotException
|
|||||||
```
|
```
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 10
|
sidebar_position: 10
|
||||||
description: nonebot.exception 模块
|
description: nonebot.exception 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic.fields import ModelField
|
from nonebot.compat import ModelField
|
||||||
|
|
||||||
|
|
||||||
class NoneBotException(Exception):
|
class NoneBotException(Exception):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
from .adapter import Adapter as Adapter
|
||||||
from .bot import Bot as Bot
|
from .bot import Bot as Bot
|
||||||
from .event import Event as Event
|
from .event import Event as Event
|
||||||
from .adapter import Adapter as Adapter
|
|
||||||
from .message import Message as Message
|
from .message import Message as Message
|
||||||
from .message import MessageSegment as MessageSegment
|
from .message import MessageSegment as MessageSegment
|
||||||
from .template import MessageTemplate as MessageTemplate
|
from .template import MessageTemplate as MessageTemplate
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import abc
|
import abc
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Dict, AsyncGenerator
|
from typing import Any
|
||||||
|
|
||||||
from nonebot.config import Config
|
from nonebot.config import Config
|
||||||
from nonebot.internal.driver import (
|
from nonebot.internal.driver import (
|
||||||
Driver,
|
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
ASGIMixin,
|
ASGIMixin,
|
||||||
WebSocket,
|
Driver,
|
||||||
HTTPClientMixin,
|
HTTPClientMixin,
|
||||||
HTTPServerSetup,
|
HTTPServerSetup,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
WebSocket,
|
||||||
WebSocketClientMixin,
|
WebSocketClientMixin,
|
||||||
WebSocketServerSetup,
|
WebSocketServerSetup,
|
||||||
)
|
)
|
||||||
|
from nonebot.internal.driver._lifespan import LIFESPAN_FUNC
|
||||||
|
|
||||||
from .bot import Bot
|
from .bot import Bot
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ class Adapter(abc.ABC):
|
|||||||
def __init__(self, driver: Driver, **kwargs: Any):
|
def __init__(self, driver: Driver, **kwargs: Any):
|
||||||
self.driver: Driver = driver
|
self.driver: Driver = driver
|
||||||
"""{ref}`nonebot.drivers.Driver` 实例"""
|
"""{ref}`nonebot.drivers.Driver` 实例"""
|
||||||
self.bots: Dict[str, Bot] = {}
|
self.bots: dict[str, Bot] = {}
|
||||||
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
|
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -97,6 +99,9 @@ class Adapter(abc.ABC):
|
|||||||
async with self.driver.websocket(setup) as ws:
|
async with self.driver.websocket(setup) as ws:
|
||||||
yield ws
|
yield ws
|
||||||
|
|
||||||
|
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
return self.driver._lifespan.on_ready(func)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
|
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
|
||||||
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import abc
|
import abc
|
||||||
import asyncio
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
|
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Protocol, Union
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
|
||||||
from nonebot.log import logger
|
|
||||||
from nonebot.config import Config
|
from nonebot.config import Config
|
||||||
from nonebot.exception import MockApiException
|
from nonebot.exception import MockApiException
|
||||||
|
from nonebot.log import logger
|
||||||
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
||||||
|
from nonebot.utils import flatten_exception_group
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .event import Event
|
|
||||||
from .adapter import Adapter
|
from .adapter import Adapter
|
||||||
|
from .event import Event
|
||||||
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: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Bot(abc.ABC):
|
class Bot(abc.ABC):
|
||||||
@@ -28,9 +30,9 @@ class Bot(abc.ABC):
|
|||||||
self_id: 机器人 ID
|
self_id: 机器人 ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_calling_api_hook: Set[T_CallingAPIHook] = set()
|
_calling_api_hook: ClassVar[set[T_CallingAPIHook]] = set()
|
||||||
"""call_api 时执行的函数"""
|
"""call_api 时执行的函数"""
|
||||||
_called_api_hook: Set[T_CalledAPIHook] = set()
|
_called_api_hook: ClassVar[set[T_CalledAPIHook]] = set()
|
||||||
"""call_api 后执行的函数"""
|
"""call_api 后执行的函数"""
|
||||||
|
|
||||||
def __init__(self, adapter: "Adapter", self_id: str):
|
def __init__(self, adapter: "Adapter", self_id: str):
|
||||||
@@ -77,47 +79,98 @@ class Bot(abc.ABC):
|
|||||||
skip_calling_api: bool = False
|
skip_calling_api: bool = False
|
||||||
exception: Optional[Exception] = None
|
exception: Optional[Exception] = None
|
||||||
|
|
||||||
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
if self._calling_api_hook:
|
||||||
try:
|
logger.debug("Running CallingAPI hooks...")
|
||||||
logger.debug("Running CallingAPI hooks...")
|
|
||||||
await asyncio.gather(*coros)
|
def _handle_mock_api_exception(
|
||||||
except MockApiException as e:
|
exc_group: BaseExceptionGroup[MockApiException],
|
||||||
|
) -> None:
|
||||||
|
nonlocal skip_calling_api, result
|
||||||
|
|
||||||
|
excs = [
|
||||||
|
exc
|
||||||
|
for exc in flatten_exception_group(exc_group)
|
||||||
|
if isinstance(exc, MockApiException)
|
||||||
|
]
|
||||||
|
if not excs:
|
||||||
|
return
|
||||||
|
elif len(excs) > 1:
|
||||||
|
logger.warning(
|
||||||
|
"Multiple hooks want to mock API result. Use the first one."
|
||||||
|
)
|
||||||
|
|
||||||
skip_calling_api = True
|
skip_calling_api = True
|
||||||
result = e.result
|
result = excs[0].result
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calling API {api} is cancelled. Return {result} instead."
|
f"Calling API {api} is cancelled. Return {result!r} instead."
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
|
|
||||||
"Running cancelled!</bg #f8bbd0></r>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(
|
||||||
|
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
|
||||||
|
"Running cancelled!</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
|
with catch(
|
||||||
|
{
|
||||||
|
MockApiException: _handle_mock_api_exception,
|
||||||
|
Exception: _handle_exception,
|
||||||
|
}
|
||||||
|
):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for hook in self._calling_api_hook:
|
||||||
|
tg.start_soon(hook, self, api, data)
|
||||||
|
|
||||||
if not skip_calling_api:
|
if not skip_calling_api:
|
||||||
try:
|
try:
|
||||||
result = await self.adapter._call_api(self, api, **data)
|
result = await self.adapter._call_api(self, api, **data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception = e
|
exception = e
|
||||||
|
|
||||||
if coros := [
|
if self._called_api_hook:
|
||||||
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
logger.debug("Running CalledAPI hooks...")
|
||||||
]:
|
|
||||||
try:
|
def _handle_mock_api_exception(
|
||||||
logger.debug("Running CalledAPI hooks...")
|
exc_group: BaseExceptionGroup[MockApiException],
|
||||||
await asyncio.gather(*coros)
|
) -> None:
|
||||||
except MockApiException as e:
|
nonlocal result, exception
|
||||||
# mock api result
|
|
||||||
result = e.result
|
excs = [
|
||||||
# ignore exception
|
exc
|
||||||
|
for exc in flatten_exception_group(exc_group)
|
||||||
|
if isinstance(exc, MockApiException)
|
||||||
|
]
|
||||||
|
if not excs:
|
||||||
|
return
|
||||||
|
elif len(excs) > 1:
|
||||||
|
logger.warning(
|
||||||
|
"Multiple hooks want to mock API result. Use the first one."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = excs[0].result
|
||||||
exception = None
|
exception = None
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calling API {api} result is mocked. Return {result} instead."
|
f"Calling API {api} result is mocked. Return {result} instead."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||||
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
|
for exc in flatten_exception_group(exc_group):
|
||||||
"Running cancelled!</bg #f8bbd0></r>"
|
logger.opt(colors=True, exception=exc).error(
|
||||||
)
|
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
|
||||||
|
"Running cancelled!</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
|
with catch(
|
||||||
|
{
|
||||||
|
MockApiException: _handle_mock_api_exception,
|
||||||
|
Exception: _handle_exception,
|
||||||
|
}
|
||||||
|
):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for hook in self._called_api_hook:
|
||||||
|
tg.start_soon(hook, self, exception, api, data, result)
|
||||||
|
|
||||||
if exception:
|
if exception:
|
||||||
raise exception
|
raise exception
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import abc
|
import abc
|
||||||
from typing import Any, Type, TypeVar
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from nonebot.compat import PYDANTIC_V2, ConfigDict
|
||||||
from nonebot.utils import DataclassEncoder
|
from nonebot.utils import DataclassEncoder
|
||||||
|
|
||||||
from .message import Message
|
from .message import Message
|
||||||
@@ -13,15 +14,21 @@ E = TypeVar("E", bound="Event")
|
|||||||
class Event(abc.ABC, BaseModel):
|
class Event(abc.ABC, BaseModel):
|
||||||
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
|
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
|
||||||
|
|
||||||
class Config:
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
extra = "allow"
|
model_config = ConfigDict(extra="allow")
|
||||||
json_encoders = {Message: DataclassEncoder}
|
else: # pragma: pydantic-v1
|
||||||
|
|
||||||
@classmethod
|
class Config(ConfigDict):
|
||||||
def validate(cls: Type["E"], value: Any) -> "E":
|
extra = "allow" # type: ignore
|
||||||
if isinstance(value, Event) and not isinstance(value, cls):
|
json_encoders = {Message: DataclassEncoder} # noqa: RUF012
|
||||||
raise TypeError(f"{value} is incompatible with Event type {cls}")
|
|
||||||
return super().validate(value)
|
if not PYDANTIC_V2: # pragma: pydantic-v1
|
||||||
|
|
||||||
|
@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:
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import abc
|
import abc
|
||||||
|
from collections.abc import Iterable
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing_extensions import Self
|
from dataclasses import asdict, dataclass, field
|
||||||
from dataclasses import field, asdict, dataclass
|
from typing import ( # noqa: UP035
|
||||||
from typing import (
|
|
||||||
Any,
|
Any,
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
Generic,
|
Generic,
|
||||||
TypeVar,
|
|
||||||
Iterable,
|
|
||||||
Optional,
|
Optional,
|
||||||
SupportsIndex,
|
SupportsIndex,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from pydantic import parse_obj_as
|
from nonebot.compat import custom_validation, type_validate_python
|
||||||
|
|
||||||
from .template import MessageTemplate
|
from .template import MessageTemplate
|
||||||
|
|
||||||
@@ -25,18 +22,19 @@ TMS = TypeVar("TMS", bound="MessageSegment")
|
|||||||
TM = TypeVar("TM", bound="Message")
|
TM = TypeVar("TM", bound="Message")
|
||||||
|
|
||||||
|
|
||||||
|
@custom_validation
|
||||||
@dataclass
|
@dataclass
|
||||||
class MessageSegment(abc.ABC, Generic[TM]):
|
class MessageSegment(abc.ABC, Generic[TM]):
|
||||||
"""消息段基类"""
|
"""消息段基类"""
|
||||||
|
|
||||||
type: str
|
type: str
|
||||||
"""消息段类型"""
|
"""消息段类型"""
|
||||||
data: Dict[str, Any] = field(default_factory=dict)
|
data: dict[str, Any] = field(default_factory=dict)
|
||||||
"""消息段数据"""
|
"""消息段数据"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_message_class(cls) -> Type[TM]:
|
def get_message_class(cls) -> Type[TM]: # noqa: UP006
|
||||||
"""获取消息数组类型"""
|
"""获取消息数组类型"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -48,7 +46,9 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(str(self))
|
return len(str(self))
|
||||||
|
|
||||||
def __ne__(self, other: Self) -> bool:
|
def __ne__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, other: Self
|
||||||
|
) -> bool:
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||||
@@ -65,6 +65,8 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
|||||||
def _validate(cls, value) -> Self:
|
def _validate(cls, value) -> Self:
|
||||||
if isinstance(value, cls):
|
if isinstance(value, cls):
|
||||||
return value
|
return value
|
||||||
|
if isinstance(value, MessageSegment):
|
||||||
|
raise ValueError(f"Type {type(value)} can not be converted to {cls}")
|
||||||
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)}")
|
||||||
if "type" not in value:
|
if "type" not in value:
|
||||||
@@ -97,7 +99,8 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class Message(List[TMS], abc.ABC):
|
@custom_validation
|
||||||
|
class Message(list[TMS], abc.ABC):
|
||||||
"""消息序列
|
"""消息序列
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -138,7 +141,7 @@ class Message(List[TMS], abc.ABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_segment_class(cls) -> Type[TMS]:
|
def get_segment_class(cls) -> type[TMS]:
|
||||||
"""获取消息段类型"""
|
"""获取消息段类型"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -158,9 +161,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
pass
|
pass
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
value = parse_obj_as(cls.get_segment_class(), value)
|
value = type_validate_python(cls.get_segment_class(), value)
|
||||||
elif isinstance(value, Iterable):
|
elif isinstance(value, Iterable):
|
||||||
value = [parse_obj_as(cls.get_segment_class(), v) for v in value]
|
value = [type_validate_python(cls.get_segment_class(), v) for v in value]
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Expected str, dict or iterable for Message, got {type(value)}"
|
f"Expected str, dict or iterable for Message, got {type(value)}"
|
||||||
@@ -173,7 +176,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
"""构造消息数组"""
|
"""构造消息数组"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __add__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
def __add__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, other: Union[str, TMS, Iterable[TMS]]
|
||||||
|
) -> Self:
|
||||||
result = self.copy()
|
result = self.copy()
|
||||||
result += other
|
result += other
|
||||||
return result
|
return result
|
||||||
@@ -205,7 +210,7 @@ class Message(List[TMS], abc.ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, args: Tuple[str, int]) -> TMS:
|
def __getitem__(self, args: tuple[str, int]) -> TMS:
|
||||||
"""索引指定类型的消息段
|
"""索引指定类型的消息段
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -216,7 +221,7 @@ class Message(List[TMS], abc.ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, args: Tuple[str, slice]) -> Self:
|
def __getitem__(self, args: tuple[str, slice]) -> Self:
|
||||||
"""切片指定类型的消息段
|
"""切片指定类型的消息段
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -248,12 +253,12 @@ class Message(List[TMS], abc.ABC):
|
|||||||
消息切片 `args`
|
消息切片 `args`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __getitem__(
|
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
args: Union[
|
args: Union[
|
||||||
str,
|
str,
|
||||||
Tuple[str, int],
|
tuple[str, int],
|
||||||
Tuple[str, slice],
|
tuple[str, slice],
|
||||||
int,
|
int,
|
||||||
slice,
|
slice,
|
||||||
],
|
],
|
||||||
@@ -272,7 +277,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
||||||
|
|
||||||
def __contains__(self, value: Union[TMS, str]) -> bool:
|
def __contains__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, value: Union[TMS, str]
|
||||||
|
) -> bool:
|
||||||
"""检查消息段是否存在
|
"""检查消息段是否存在
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -281,7 +288,7 @@ class Message(List[TMS], abc.ABC):
|
|||||||
消息内是否存在给定消息段或给定类型的消息段
|
消息内是否存在给定消息段或给定类型的消息段
|
||||||
"""
|
"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return bool(next((seg for seg in self if seg.type == value), None))
|
return next((seg for seg in self if seg.type == value), None) is not None
|
||||||
return super().__contains__(value)
|
return super().__contains__(value)
|
||||||
|
|
||||||
def has(self, value: Union[TMS, str]) -> bool:
|
def has(self, value: Union[TMS, str]) -> bool:
|
||||||
@@ -322,8 +329,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
return self[type_]
|
return self[type_]
|
||||||
|
|
||||||
iterator, filtered = (
|
iterator, filtered = (
|
||||||
seg for seg in self if seg.type == type_
|
(seg for seg in self if seg.type == type_),
|
||||||
), self.__class__()
|
self.__class__(),
|
||||||
|
)
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
seg = next(iterator, None)
|
seg = next(iterator, None)
|
||||||
if seg is None:
|
if seg is None:
|
||||||
@@ -355,7 +363,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
return all(seg.type == value for seg in self)
|
return all(seg.type == value for seg in self)
|
||||||
return all(seg == value for seg in self)
|
return all(seg == value for seg in self)
|
||||||
|
|
||||||
def append(self, obj: Union[str, TMS]) -> Self:
|
def append( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, obj: Union[str, TMS]
|
||||||
|
) -> Self:
|
||||||
"""添加一个消息段到消息数组末尾。
|
"""添加一个消息段到消息数组末尾。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -369,7 +379,9 @@ class Message(List[TMS], abc.ABC):
|
|||||||
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
|
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def extend(self, obj: Union[Self, Iterable[TMS]]) -> Self:
|
def extend( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, obj: Union[Self, Iterable[TMS]]
|
||||||
|
) -> Self:
|
||||||
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
|
from _string import formatter_field_name_split # type: ignore
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
import functools
|
import functools
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Set,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
Generic,
|
|
||||||
Mapping,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
Callable,
|
||||||
|
Generic,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
TypeVar,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .message import Message, MessageSegment
|
from .message import Message, MessageSegment
|
||||||
|
|
||||||
|
def formatter_field_name_split(
|
||||||
|
field_name: str,
|
||||||
|
) -> tuple[str, list[tuple[bool, str]]]: ...
|
||||||
|
|
||||||
|
|
||||||
TM = TypeVar("TM", bound="Message")
|
TM = TypeVar("TM", bound="Message")
|
||||||
TF = TypeVar("TF", str, "Message")
|
TF = TypeVar("TF", str, "Message")
|
||||||
|
|
||||||
@@ -36,26 +36,35 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
参数:
|
参数:
|
||||||
template: 模板
|
template: 模板
|
||||||
factory: 消息类型工厂,默认为 `str`
|
factory: 消息类型工厂,默认为 `str`
|
||||||
|
private_getattr: 是否允许在模板中访问私有属性,默认为 `False`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(
|
def __init__(
|
||||||
self: "MessageTemplate[str]", template: str, factory: Type[str] = str
|
self: "MessageTemplate[str]",
|
||||||
) -> None:
|
template: str,
|
||||||
...
|
factory: type[str] = str,
|
||||||
|
private_getattr: bool = False,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(
|
def __init__(
|
||||||
self: "MessageTemplate[TM]", template: Union[str, TM], factory: Type[TM]
|
self: "MessageTemplate[TM]",
|
||||||
) -> None:
|
template: Union[str, TM],
|
||||||
...
|
factory: type[TM],
|
||||||
|
private_getattr: bool = False,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
|
self,
|
||||||
|
template: Union[str, TM],
|
||||||
|
factory: Union[type[str], type[TM]] = str,
|
||||||
|
private_getattr: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.template: TF = template # type: ignore
|
self.template: TF = template # type: ignore
|
||||||
self.factory: Type[TF] = factory # type: ignore
|
self.factory: type[TF] = factory # type: ignore
|
||||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
self.format_specs: dict[str, FormatSpecFunc] = {}
|
||||||
|
self.private_getattr = private_getattr
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
||||||
@@ -69,7 +78,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
self.format_specs[name] = spec
|
self.format_specs[name] = spec
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
def format(self, *args, **kwargs):
|
def format( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, *args, **kwargs
|
||||||
|
) -> TF:
|
||||||
"""根据传入参数和模板生成消息对象"""
|
"""根据传入参数和模板生成消息对象"""
|
||||||
return self._format(args, kwargs)
|
return self._format(args, kwargs)
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
self.check_unused_args(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( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
format_string: str,
|
format_string: str,
|
||||||
args: Sequence[Any],
|
args: Sequence[Any],
|
||||||
@@ -110,15 +121,15 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
) -> TF:
|
) -> TF:
|
||||||
raise NotImplementedError("`vformat` has merged into `_format`")
|
raise NotImplementedError("`vformat` has merged into `_format`")
|
||||||
|
|
||||||
def _vformat(
|
def _vformat( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
format_string: str,
|
format_string: str,
|
||||||
args: Sequence[Any],
|
args: Sequence[Any],
|
||||||
kwargs: Mapping[str, Any],
|
kwargs: Mapping[str, Any],
|
||||||
used_args: Set[Union[int, str]],
|
used_args: set[Union[int, str]],
|
||||||
auto_arg_index: int = 0,
|
auto_arg_index: int = 0,
|
||||||
) -> 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
|
||||||
@@ -167,10 +178,23 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
|
|
||||||
return functools.reduce(self._add, results), auto_arg_index
|
return functools.reduce(self._add, results), auto_arg_index
|
||||||
|
|
||||||
|
def get_field(
|
||||||
|
self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
||||||
|
) -> tuple[Any, Union[int, str]]:
|
||||||
|
first, rest = formatter_field_name_split(field_name)
|
||||||
|
obj = self.get_value(first, args, kwargs)
|
||||||
|
|
||||||
|
for is_attr, value in rest:
|
||||||
|
if not self.private_getattr and value.startswith("_"):
|
||||||
|
raise ValueError("Cannot access private attribute")
|
||||||
|
obj = getattr(obj, value) if is_attr else obj[value]
|
||||||
|
|
||||||
|
return obj, first
|
||||||
|
|
||||||
def format_field(self, value: Any, format_spec: str) -> Any:
|
def format_field(self, value: Any, format_spec: str) -> Any:
|
||||||
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
|
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
|
||||||
if formatter is None and not issubclass(self.factory, str):
|
if formatter is None and not issubclass(self.factory, str):
|
||||||
segment_class: Type["MessageSegment"] = self.factory.get_segment_class()
|
segment_class: type["MessageSegment"] = self.factory.get_segment_class()
|
||||||
method = getattr(segment_class, format_spec, None)
|
method = getattr(segment_class, format_spec, None)
|
||||||
if callable(method) and not cast(str, method.__name__).startswith("_"):
|
if callable(method) and not cast(str, method.__name__).startswith("_"):
|
||||||
formatter = getattr(segment_class, format_spec)
|
formatter = getattr(segment_class, format_spec)
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
from .model import URL as URL
|
|
||||||
from .model import RawURL as RawURL
|
|
||||||
from .abstract import Mixin as Mixin
|
|
||||||
from .model import Cookies as Cookies
|
|
||||||
from .model import Request as Request
|
|
||||||
from .abstract import Driver as Driver
|
|
||||||
from .model import FileType as FileType
|
|
||||||
from .model import Response as Response
|
|
||||||
from .model import DataTypes as DataTypes
|
|
||||||
from .model import FileTypes as FileTypes
|
|
||||||
from .model import WebSocket as WebSocket
|
|
||||||
from .model import FilesTypes as FilesTypes
|
|
||||||
from .model import QueryTypes as QueryTypes
|
|
||||||
from .abstract import ASGIMixin as ASGIMixin
|
from .abstract import ASGIMixin as ASGIMixin
|
||||||
from .model import CookieTypes as CookieTypes
|
from .abstract import Driver as Driver
|
||||||
from .model import FileContent as FileContent
|
|
||||||
from .model import HTTPVersion as HTTPVersion
|
|
||||||
from .model import HeaderTypes as HeaderTypes
|
|
||||||
from .model import SimpleQuery as SimpleQuery
|
|
||||||
from .model import ContentTypes as ContentTypes
|
|
||||||
from .model import QueryVariable as QueryVariable
|
|
||||||
from .abstract import ForwardMixin as ForwardMixin
|
|
||||||
from .abstract import ReverseMixin as ReverseMixin
|
|
||||||
from .abstract import ForwardDriver as ForwardDriver
|
from .abstract import ForwardDriver as ForwardDriver
|
||||||
from .abstract import ReverseDriver as ReverseDriver
|
from .abstract import ForwardMixin as ForwardMixin
|
||||||
from .combine import combine_driver as combine_driver
|
|
||||||
from .model import HTTPServerSetup as HTTPServerSetup
|
|
||||||
from .abstract import HTTPClientMixin as HTTPClientMixin
|
from .abstract import HTTPClientMixin as HTTPClientMixin
|
||||||
from .model import WebSocketServerSetup as WebSocketServerSetup
|
from .abstract import HTTPClientSession as HTTPClientSession
|
||||||
|
from .abstract import Mixin as Mixin
|
||||||
|
from .abstract import ReverseDriver as ReverseDriver
|
||||||
|
from .abstract import ReverseMixin as ReverseMixin
|
||||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||||
|
from .combine import combine_driver as combine_driver
|
||||||
|
from .model import URL as URL
|
||||||
|
from .model import ContentTypes as ContentTypes
|
||||||
|
from .model import Cookies as Cookies
|
||||||
|
from .model import CookieTypes as CookieTypes
|
||||||
|
from .model import DataTypes as DataTypes
|
||||||
|
from .model import FileContent as FileContent
|
||||||
|
from .model import FilesTypes as FilesTypes
|
||||||
|
from .model import FileType as FileType
|
||||||
|
from .model import FileTypes as FileTypes
|
||||||
|
from .model import HeaderTypes as HeaderTypes
|
||||||
|
from .model import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from .model import HTTPVersion as HTTPVersion
|
||||||
|
from .model import QueryTypes as QueryTypes
|
||||||
|
from .model import QueryVariable as QueryVariable
|
||||||
|
from .model import RawURL as RawURL
|
||||||
|
from .model import Request as Request
|
||||||
|
from .model import Response as Response
|
||||||
|
from .model import SimpleQuery as SimpleQuery
|
||||||
|
from .model import WebSocket as WebSocket
|
||||||
|
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
|||||||
100
nonebot/internal/driver/_lifespan.py
Normal file
100
nonebot/internal/driver/_lifespan.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from collections.abc import Awaitable, Iterable
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Any, Callable, Optional, Union, cast
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
from anyio.abc import TaskGroup
|
||||||
|
from exceptiongroup import suppress
|
||||||
|
|
||||||
|
from nonebot.utils import is_coroutine_callable, run_sync
|
||||||
|
|
||||||
|
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||||
|
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||||
|
LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||||
|
|
||||||
|
|
||||||
|
class Lifespan:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._task_group: Optional[TaskGroup] = None
|
||||||
|
|
||||||
|
self._startup_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
self._ready_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
self._shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_group(self) -> TaskGroup:
|
||||||
|
if self._task_group is None:
|
||||||
|
raise RuntimeError("Lifespan not started")
|
||||||
|
return self._task_group
|
||||||
|
|
||||||
|
@task_group.setter
|
||||||
|
def task_group(self, task_group: TaskGroup) -> None:
|
||||||
|
if self._task_group is not None:
|
||||||
|
raise RuntimeError("Lifespan already started")
|
||||||
|
self._task_group = task_group
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
self._ready_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _run_lifespan_func(
|
||||||
|
funcs: Iterable[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:
|
||||||
|
# create background task group
|
||||||
|
self.task_group = anyio.create_task_group()
|
||||||
|
await self.task_group.__aenter__()
|
||||||
|
|
||||||
|
# run startup funcs
|
||||||
|
if self._startup_funcs:
|
||||||
|
await self._run_lifespan_func(self._startup_funcs)
|
||||||
|
|
||||||
|
# run ready funcs
|
||||||
|
if self._ready_funcs:
|
||||||
|
await self._run_lifespan_func(self._ready_funcs)
|
||||||
|
|
||||||
|
async def shutdown(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
exc_type: Optional[type[BaseException]] = None,
|
||||||
|
exc_val: Optional[BaseException] = None,
|
||||||
|
exc_tb: Optional[TracebackType] = None,
|
||||||
|
) -> None:
|
||||||
|
if self._shutdown_funcs:
|
||||||
|
# reverse shutdown funcs to ensure stack order
|
||||||
|
await self._run_lifespan_func(reversed(self._shutdown_funcs))
|
||||||
|
|
||||||
|
# shutdown background task group
|
||||||
|
self.task_group.cancel_scope.cancel()
|
||||||
|
|
||||||
|
with suppress(Exception):
|
||||||
|
await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
|
||||||
|
|
||||||
|
self._task_group = None
|
||||||
|
|
||||||
|
async def __aenter__(self) -> None:
|
||||||
|
await self.startup()
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
exc_tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
|
||||||
@@ -1,25 +1,41 @@
|
|||||||
import abc
|
import abc
|
||||||
import asyncio
|
from collections.abc import AsyncGenerator
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
from types import TracebackType
|
||||||
|
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||||
|
from typing_extensions import Self, TypeAlias
|
||||||
|
|
||||||
from nonebot.log import logger
|
from anyio import CancelScope, create_task_group
|
||||||
from nonebot.config import Env, Config
|
from anyio.abc import TaskGroup
|
||||||
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
|
||||||
|
from nonebot.config import Config, Env
|
||||||
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.internal.params import BotParam, DefaultParam, DependParam
|
||||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
from nonebot.log import logger
|
||||||
from nonebot.typing import (
|
from nonebot.typing import (
|
||||||
T_DependencyCache,
|
|
||||||
T_BotConnectionHook,
|
T_BotConnectionHook,
|
||||||
T_BotDisconnectionHook,
|
T_BotDisconnectionHook,
|
||||||
|
T_DependencyCache,
|
||||||
|
)
|
||||||
|
from nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch
|
||||||
|
|
||||||
|
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||||
|
from .model import (
|
||||||
|
CookieTypes,
|
||||||
|
HeaderTypes,
|
||||||
|
HTTPServerSetup,
|
||||||
|
HTTPVersion,
|
||||||
|
QueryTypes,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketServerSetup,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.internal.adapter import Bot, Adapter
|
from nonebot.internal.adapter import Adapter, Bot
|
||||||
|
|
||||||
|
|
||||||
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
||||||
@@ -35,11 +51,11 @@ class Driver(abc.ABC):
|
|||||||
config: 包含配置信息的 Config 对象
|
config: 包含配置信息的 Config 对象
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_adapters: Dict[str, "Adapter"] = {}
|
_adapters: ClassVar[dict[str, "Adapter"]] = {}
|
||||||
"""已注册的适配器列表"""
|
"""已注册的适配器列表"""
|
||||||
_bot_connection_hook: Set[Dependent[Any]] = set()
|
_bot_connection_hook: ClassVar[set[Dependent[Any]]] = set()
|
||||||
"""Bot 连接建立时执行的函数"""
|
"""Bot 连接建立时执行的函数"""
|
||||||
_bot_disconnection_hook: Set[Dependent[Any]] = set()
|
_bot_disconnection_hook: ClassVar[set[Dependent[Any]]] = set()
|
||||||
"""Bot 连接断开时执行的函数"""
|
"""Bot 连接断开时执行的函数"""
|
||||||
|
|
||||||
def __init__(self, env: Env, config: Config):
|
def __init__(self, env: Env, config: Config):
|
||||||
@@ -47,8 +63,8 @@ class Driver(abc.ABC):
|
|||||||
"""环境名称"""
|
"""环境名称"""
|
||||||
self.config: Config = config
|
self.config: Config = config
|
||||||
"""全局配置对象"""
|
"""全局配置对象"""
|
||||||
self._bots: Dict[str, "Bot"] = {}
|
self._bots: dict[str, "Bot"] = {}
|
||||||
self._bot_tasks: Set[asyncio.Task] = set()
|
self._lifespan = Lifespan()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -57,11 +73,15 @@ class Driver(abc.ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bots(self) -> Dict[str, "Bot"]:
|
def bots(self) -> dict[str, "Bot"]:
|
||||||
"""获取当前所有已连接的 Bot"""
|
"""获取当前所有已连接的 Bot"""
|
||||||
return self._bots
|
return self._bots
|
||||||
|
|
||||||
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
|
@property
|
||||||
|
def task_group(self) -> TaskGroup:
|
||||||
|
return self._lifespan.task_group
|
||||||
|
|
||||||
|
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
|
||||||
"""注册一个协议适配器
|
"""注册一个协议适配器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -94,21 +114,17 @@ class Driver(abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
"""启动驱动框架"""
|
"""启动驱动框架"""
|
||||||
logger.opt(colors=True).debug(
|
logger.opt(colors=True).success(
|
||||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.on_shutdown(self._cleanup)
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""注册一个启动时执行的函数"""
|
||||||
|
return self._lifespan.on_startup(func)
|
||||||
|
|
||||||
@abc.abstractmethod
|
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
def on_startup(self, func: Callable) -> Callable:
|
"""注册一个停止时执行的函数"""
|
||||||
"""注册一个在驱动器启动时执行的函数"""
|
return self._lifespan.on_shutdown(func)
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def on_shutdown(self, func: Callable) -> Callable:
|
|
||||||
"""注册一个在驱动器停止时执行的函数"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
||||||
@@ -142,66 +158,63 @@ class Driver(abc.ABC):
|
|||||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||||
self._bots[bot.self_id] = bot
|
self._bots[bot.self_id] = bot
|
||||||
|
|
||||||
|
if not self._bot_connection_hook:
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(
|
||||||
|
"<r><bg #f8bbd0>"
|
||||||
|
"Error when running WebSocketConnection hook:"
|
||||||
|
"</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
async def _run_hook(bot: "Bot") -> None:
|
async def _run_hook(bot: "Bot") -> None:
|
||||||
dependency_cache: T_DependencyCache = {}
|
dependency_cache: T_DependencyCache = {}
|
||||||
async with AsyncExitStack() as stack:
|
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||||
if coros := [
|
async with AsyncExitStack() as stack, create_task_group() as tg:
|
||||||
run_coro_with_catch(
|
for hook in self._bot_connection_hook:
|
||||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
tg.start_soon(
|
||||||
(SkippedException,),
|
run_coro_with_catch,
|
||||||
)
|
hook(
|
||||||
for hook in self._bot_connection_hook
|
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||||
]:
|
),
|
||||||
try:
|
(SkippedException,),
|
||||||
await asyncio.gather(*coros)
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>"
|
|
||||||
"Error when running WebSocketConnection hook. "
|
|
||||||
"Running cancelled!"
|
|
||||||
"</bg #f8bbd0></r>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
task = asyncio.create_task(_run_hook(bot))
|
self.task_group.start_soon(_run_hook, bot)
|
||||||
task.add_done_callback(self._bot_tasks.discard)
|
|
||||||
self._bot_tasks.add(task)
|
|
||||||
|
|
||||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||||
if bot.self_id in self._bots:
|
if bot.self_id in self._bots:
|
||||||
del self._bots[bot.self_id]
|
del self._bots[bot.self_id]
|
||||||
|
|
||||||
|
if not self._bot_disconnection_hook:
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(
|
||||||
|
"<r><bg #f8bbd0>"
|
||||||
|
"Error when running WebSocketDisConnection hook:"
|
||||||
|
"</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
|
||||||
async def _run_hook(bot: "Bot") -> None:
|
async def _run_hook(bot: "Bot") -> None:
|
||||||
dependency_cache: T_DependencyCache = {}
|
dependency_cache: T_DependencyCache = {}
|
||||||
async with AsyncExitStack() as stack:
|
# shield cancellation to ensure bot disconnect hooks are always run
|
||||||
if coros := [
|
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||||
run_coro_with_catch(
|
async with create_task_group() as tg, AsyncExitStack() as stack:
|
||||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
for hook in self._bot_disconnection_hook:
|
||||||
(SkippedException,),
|
tg.start_soon(
|
||||||
)
|
run_coro_with_catch,
|
||||||
for hook in self._bot_disconnection_hook
|
hook(
|
||||||
]:
|
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||||
try:
|
),
|
||||||
await asyncio.gather(*coros)
|
(SkippedException,),
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>"
|
|
||||||
"Error when running WebSocketDisConnection hook. "
|
|
||||||
"Running cancelled!"
|
|
||||||
"</bg #f8bbd0></r>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
task = asyncio.create_task(_run_hook(bot))
|
self.task_group.start_soon(_run_hook, bot)
|
||||||
task.add_done_callback(self._bot_tasks.discard)
|
|
||||||
self._bot_tasks.add(task)
|
|
||||||
|
|
||||||
async def _cleanup(self) -> None:
|
|
||||||
"""清理驱动器资源"""
|
|
||||||
if self._bot_tasks:
|
|
||||||
logger.opt(colors=True).debug(
|
|
||||||
"<y>Waiting for running bot connection hooks...</y>"
|
|
||||||
)
|
|
||||||
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Mixin(abc.ABC):
|
class Mixin(abc.ABC):
|
||||||
@@ -222,6 +235,49 @@ class ReverseMixin(Mixin):
|
|||||||
"""服务端混入基类。"""
|
"""服务端混入基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClientSession(abc.ABC):
|
||||||
|
"""HTTP 客户端会话基类。"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
params: QueryTypes = None,
|
||||||
|
headers: HeaderTypes = None,
|
||||||
|
cookies: CookieTypes = None,
|
||||||
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
proxy: Optional[str] = None,
|
||||||
|
):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def request(self, setup: Request) -> Response:
|
||||||
|
"""发送一个 HTTP 请求"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def setup(self) -> None:
|
||||||
|
"""初始化会话"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""关闭会话"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def __aenter__(self) -> Self:
|
||||||
|
await self.setup()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[type[BaseException]],
|
||||||
|
exc: Optional[BaseException],
|
||||||
|
tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
|
||||||
class HTTPClientMixin(ForwardMixin):
|
class HTTPClientMixin(ForwardMixin):
|
||||||
"""HTTP 客户端混入基类。"""
|
"""HTTP 客户端混入基类。"""
|
||||||
|
|
||||||
@@ -230,6 +286,19 @@ class HTTPClientMixin(ForwardMixin):
|
|||||||
"""发送一个 HTTP 请求"""
|
"""发送一个 HTTP 请求"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_session(
|
||||||
|
self,
|
||||||
|
params: QueryTypes = None,
|
||||||
|
headers: HeaderTypes = None,
|
||||||
|
cookies: CookieTypes = None,
|
||||||
|
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
proxy: Optional[str] = None,
|
||||||
|
) -> HTTPClientSession:
|
||||||
|
"""获取一个 HTTP 会话"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClientMixin(ForwardMixin):
|
class WebSocketClientMixin(ForwardMixin):
|
||||||
"""WebSocket 客户端混入基类。"""
|
"""WebSocket 客户端混入基类。"""
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
from typing import TYPE_CHECKING, Type, Union, TypeVar, overload
|
from typing import TYPE_CHECKING, TypeVar, Union, overload
|
||||||
|
|
||||||
from .abstract import Mixin, Driver
|
from .abstract import Driver, Mixin
|
||||||
|
|
||||||
D = TypeVar("D", bound="Driver")
|
D = TypeVar("D", bound="Driver")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
class CombinedDriver(Driver, Mixin):
|
class CombinedDriver(Driver, Mixin): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def combine_driver(driver: Type[D]) -> Type[D]:
|
def combine_driver(driver: type[D]) -> type[D]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
|
def combine_driver(
|
||||||
...
|
driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin]
|
||||||
|
) -> type["CombinedDriver"]: ...
|
||||||
|
|
||||||
|
|
||||||
def combine_driver(
|
def combine_driver(
|
||||||
driver: Type[D], *mixins: Type[Mixin]
|
driver: type[D], *mixins: type[Mixin]
|
||||||
) -> Union[Type[D], Type["CombinedDriver"]]:
|
) -> Union[type[D], type["CombinedDriver"]]:
|
||||||
"""将一个驱动器和多个混入类合并。"""
|
"""将一个驱动器和多个混入类合并。"""
|
||||||
# check first
|
# check first
|
||||||
if not issubclass(driver, Driver):
|
if not issubclass(driver, Driver):
|
||||||
@@ -35,11 +34,9 @@ def combine_driver(
|
|||||||
|
|
||||||
def type_(self: "CombinedDriver") -> str:
|
def type_(self: "CombinedDriver") -> str:
|
||||||
return (
|
return (
|
||||||
driver.type.__get__(self)
|
driver.type.__get__(self) # type: ignore
|
||||||
+ "+"
|
+ "+"
|
||||||
+ "+".join(x.type.__get__(self) for x in mixins)
|
+ "+".join(x.type.__get__(self) for x in mixins) # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
return type(
|
return type("CombinedDriver", (*mixins, driver), {"type": property(type_)}) # type: ignore
|
||||||
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
|
||||||
) # type: ignore
|
|
||||||
|
|||||||
@@ -1,59 +1,47 @@
|
|||||||
import abc
|
import abc
|
||||||
import urllib.request
|
from collections.abc import Awaitable, Iterator, Mapping, MutableMapping
|
||||||
from enum import Enum
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing_extensions import TypeAlias
|
from enum import Enum
|
||||||
from http.cookiejar import Cookie, CookieJar
|
from http.cookiejar import Cookie, CookieJar
|
||||||
from typing import (
|
from typing import IO, Any, Callable, Optional, Union
|
||||||
IO,
|
from typing_extensions import TypeAlias
|
||||||
Any,
|
import urllib.request
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
Mapping,
|
|
||||||
Callable,
|
|
||||||
Iterator,
|
|
||||||
Optional,
|
|
||||||
Awaitable,
|
|
||||||
MutableMapping,
|
|
||||||
)
|
|
||||||
|
|
||||||
from yarl import URL as URL
|
|
||||||
from multidict import CIMultiDict
|
from multidict import CIMultiDict
|
||||||
|
from yarl import URL as URL
|
||||||
|
|
||||||
RawURL: TypeAlias = Tuple[bytes, bytes, Optional[int], bytes]
|
RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes]
|
||||||
|
|
||||||
SimpleQuery: TypeAlias = Union[str, int, float]
|
SimpleQuery: TypeAlias = Union[str, int, float]
|
||||||
QueryVariable: TypeAlias = Union[SimpleQuery, List[SimpleQuery]]
|
QueryVariable: TypeAlias = Union[SimpleQuery, list[SimpleQuery]]
|
||||||
QueryTypes: TypeAlias = Union[
|
QueryTypes: TypeAlias = Union[
|
||||||
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
|
None, str, Mapping[str, QueryVariable], list[tuple[str, SimpleQuery]]
|
||||||
]
|
]
|
||||||
|
|
||||||
HeaderTypes: TypeAlias = Union[
|
HeaderTypes: TypeAlias = Union[
|
||||||
None,
|
None,
|
||||||
CIMultiDict[str],
|
CIMultiDict[str],
|
||||||
Dict[str, str],
|
dict[str, str],
|
||||||
List[Tuple[str, str]],
|
list[tuple[str, str]],
|
||||||
]
|
]
|
||||||
|
|
||||||
CookieTypes: TypeAlias = Union[
|
CookieTypes: TypeAlias = Union[
|
||||||
None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]
|
None, "Cookies", CookieJar, dict[str, str], list[tuple[str, str]]
|
||||||
]
|
]
|
||||||
|
|
||||||
ContentTypes: TypeAlias = Union[str, bytes, None]
|
ContentTypes: TypeAlias = Union[str, bytes, None]
|
||||||
DataTypes: TypeAlias = Union[dict, None]
|
DataTypes: TypeAlias = Union[dict, None]
|
||||||
FileContent: TypeAlias = Union[IO[bytes], bytes]
|
FileContent: TypeAlias = Union[IO[bytes], bytes]
|
||||||
FileType: TypeAlias = Tuple[Optional[str], FileContent, Optional[str]]
|
FileType: TypeAlias = tuple[Optional[str], FileContent, Optional[str]]
|
||||||
FileTypes: TypeAlias = Union[
|
FileTypes: TypeAlias = Union[
|
||||||
# file (or bytes)
|
# file (or bytes)
|
||||||
FileContent,
|
FileContent,
|
||||||
# (filename, file (or bytes))
|
# (filename, file (or bytes))
|
||||||
Tuple[Optional[str], FileContent],
|
tuple[Optional[str], FileContent],
|
||||||
# (filename, file (or bytes), content_type)
|
# (filename, file (or bytes), content_type)
|
||||||
FileType,
|
FileType,
|
||||||
]
|
]
|
||||||
FilesTypes: TypeAlias = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
|
FilesTypes: TypeAlias = Union[dict[str, FileTypes], list[tuple[str, FileTypes]], None]
|
||||||
|
|
||||||
|
|
||||||
class HTTPVersion(Enum):
|
class HTTPVersion(Enum):
|
||||||
@@ -119,7 +107,7 @@ class Request:
|
|||||||
self.content: ContentTypes = content
|
self.content: ContentTypes = content
|
||||||
self.data: DataTypes = data
|
self.data: DataTypes = data
|
||||||
self.json: Any = json
|
self.json: Any = json
|
||||||
self.files: Optional[List[Tuple[str, FileType]]] = None
|
self.files: Optional[list[tuple[str, FileType]]] = None
|
||||||
if files:
|
if files:
|
||||||
self.files = []
|
self.files = []
|
||||||
files_ = files.items() if isinstance(files, dict) else files
|
files_ = files.items() if isinstance(files, dict) else files
|
||||||
@@ -257,7 +245,7 @@ class Cookies(MutableMapping):
|
|||||||
)
|
)
|
||||||
self.jar.set_cookie(cookie)
|
self.jar.set_cookie(cookie)
|
||||||
|
|
||||||
def get(
|
def get( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
default: Optional[str] = None,
|
default: Optional[str] = None,
|
||||||
@@ -298,12 +286,14 @@ class Cookies(MutableMapping):
|
|||||||
def clear(self, domain: Optional[str] = None, path: Optional[str] = None) -> None:
|
def clear(self, domain: Optional[str] = None, path: Optional[str] = None) -> None:
|
||||||
self.jar.clear(domain, path)
|
self.jar.clear(domain, path)
|
||||||
|
|
||||||
def update(self, cookies: CookieTypes = None) -> None:
|
def update( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, cookies: CookieTypes = None
|
||||||
|
) -> None:
|
||||||
cookies = Cookies(cookies)
|
cookies = Cookies(cookies)
|
||||||
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]:
|
def as_header(self, request: Request) -> dict[str, str]:
|
||||||
urllib_request = self._CookieCompatRequest(request)
|
urllib_request = self._CookieCompatRequest(request)
|
||||||
self.jar.add_cookie_header(urllib_request)
|
self.jar.add_cookie_header(urllib_request)
|
||||||
return urllib_request.added_headers
|
return urllib_request.added_headers
|
||||||
@@ -341,9 +331,11 @@ class Cookies(MutableMapping):
|
|||||||
method=request.method,
|
method=request.method,
|
||||||
)
|
)
|
||||||
self.request = request
|
self.request = request
|
||||||
self.added_headers: Dict[str, str] = {}
|
self.added_headers: dict[str, str] = {}
|
||||||
|
|
||||||
def add_unredirected_header(self, key: str, value: str) -> None:
|
def add_unredirected_header( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, key: str, value: str
|
||||||
|
) -> None:
|
||||||
super().add_unredirected_header(key, value)
|
super().add_unredirected_header(key, value)
|
||||||
self.added_headers[key] = value
|
self.added_headers[key] = value
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from .manager import MatcherManager as MatcherManager
|
from .manager import MatcherManager as MatcherManager
|
||||||
from .provider import MatcherProvider as MatcherProvider
|
|
||||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||||
|
from .provider import MatcherProvider as MatcherProvider
|
||||||
|
|
||||||
matchers = MatcherManager()
|
matchers = MatcherManager()
|
||||||
|
|
||||||
from .matcher import Matcher as Matcher
|
from .matcher import Matcher as Matcher
|
||||||
from .matcher import current_bot as current_bot
|
|
||||||
from .matcher import MatcherSource as MatcherSource
|
from .matcher import MatcherSource as MatcherSource
|
||||||
|
from .matcher import current_bot as current_bot
|
||||||
from .matcher import current_event as current_event
|
from .matcher import current_event as current_event
|
||||||
from .matcher import current_handler as current_handler
|
from .matcher import current_handler as current_handler
|
||||||
from .matcher import current_matcher as current_matcher
|
from .matcher import current_matcher as current_matcher
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
from typing import (
|
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
|
||||||
TYPE_CHECKING,
|
from typing import TYPE_CHECKING, Optional, TypeVar, Union, overload
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Iterator,
|
|
||||||
KeysView,
|
|
||||||
Optional,
|
|
||||||
ItemsView,
|
|
||||||
ValuesView,
|
|
||||||
MutableMapping,
|
|
||||||
overload,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||||
|
|
||||||
@@ -22,7 +9,7 @@ if TYPE_CHECKING:
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
|
||||||
"""事件响应器管理器
|
"""事件响应器管理器
|
||||||
|
|
||||||
实现了常用字典操作,用于管理事件响应器。
|
实现了常用字典操作,用于管理事件响应器。
|
||||||
@@ -43,10 +30,10 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self.provider)
|
return len(self.provider)
|
||||||
|
|
||||||
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
|
def __getitem__(self, key: int) -> list[type["Matcher"]]:
|
||||||
return self.provider[key]
|
return self.provider[key]
|
||||||
|
|
||||||
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
|
def __setitem__(self, key: int, value: list[type["Matcher"]]) -> None:
|
||||||
self.provider[key] = value
|
self.provider[key] = value
|
||||||
|
|
||||||
def __delitem__(self, key: int) -> None:
|
def __delitem__(self, key: int) -> None:
|
||||||
@@ -58,43 +45,45 @@ class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
|||||||
def keys(self) -> KeysView[int]:
|
def keys(self) -> KeysView[int]:
|
||||||
return self.provider.keys()
|
return self.provider.keys()
|
||||||
|
|
||||||
def values(self) -> ValuesView[List[Type["Matcher"]]]:
|
def values(self) -> ValuesView[list[type["Matcher"]]]:
|
||||||
return self.provider.values()
|
return self.provider.values()
|
||||||
|
|
||||||
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
|
def items(self) -> ItemsView[int, list[type["Matcher"]]]:
|
||||||
return self.provider.items()
|
return self.provider.items()
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get(self, key: int) -> Optional[List[Type["Matcher"]]]:
|
def get(self, key: int) -> Optional[list[type["Matcher"]]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get(self, key: int, default: T) -> Union[List[Type["Matcher"]], T]:
|
def get(self, key: int, default: T) -> Union[list[type["Matcher"]], T]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, key: int, default: Optional[T] = None
|
self, key: int, default: Optional[T] = None
|
||||||
) -> Optional[Union[List[Type["Matcher"]], T]]:
|
) -> Optional[Union[list[type["Matcher"]], T]]:
|
||||||
return self.provider.get(key, default)
|
return self.provider.get(key, default)
|
||||||
|
|
||||||
def pop(self, key: int) -> List[Type["Matcher"]]:
|
def pop( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, key: int
|
||||||
|
) -> list[type["Matcher"]]:
|
||||||
return self.provider.pop(key)
|
return self.provider.pop(key)
|
||||||
|
|
||||||
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
|
def popitem(self) -> tuple[int, list[type["Matcher"]]]:
|
||||||
return self.provider.popitem()
|
return self.provider.popitem()
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self.provider.clear()
|
self.provider.clear()
|
||||||
|
|
||||||
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
|
def update( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self.provider.update(__m)
|
self, m: MutableMapping[int, list[type["Matcher"]]], /
|
||||||
|
) -> None:
|
||||||
|
self.provider.update(m)
|
||||||
|
|
||||||
def setdefault(
|
def setdefault(
|
||||||
self, key: int, default: List[Type["Matcher"]]
|
self, key: int, default: list[type["Matcher"]]
|
||||||
) -> List[Type["Matcher"]]:
|
) -> list[type["Matcher"]]:
|
||||||
return self.provider.setdefault(key, default)
|
return self.provider.setdefault(key, default)
|
||||||
|
|
||||||
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
|
def set_provider(self, provider_class: type[MatcherProvider]) -> None:
|
||||||
"""设置事件响应器存储器
|
"""设置事件响应器存储器
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
import sys
|
from collections.abc import Iterable
|
||||||
import inspect
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from contextvars import ContextVar
|
|
||||||
from typing_extensions import Self
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from contextlib import AsyncExitStack, contextmanager
|
from contextlib import AsyncExitStack, contextmanager
|
||||||
from typing import (
|
from contextvars import ContextVar
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import ( # noqa: UP035
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Iterable,
|
|
||||||
NoReturn,
|
NoReturn,
|
||||||
Optional,
|
Optional,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import Self
|
||||||
|
import warnings
|
||||||
|
|
||||||
from nonebot.log import logger
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
from nonebot.internal.rule import Rule
|
|
||||||
from nonebot.utils import classproperty
|
from nonebot.consts import (
|
||||||
from nonebot.dependencies import Param, Dependent
|
ARG_KEY,
|
||||||
from nonebot.internal.permission import User, Permission
|
LAST_RECEIVE_KEY,
|
||||||
|
PAUSE_PROMPT_RESULT_KEY,
|
||||||
|
RECEIVE_KEY,
|
||||||
|
REJECT_CACHE_TARGET,
|
||||||
|
REJECT_PROMPT_RESULT_KEY,
|
||||||
|
REJECT_TARGET,
|
||||||
|
)
|
||||||
|
from nonebot.dependencies import Dependent, Param
|
||||||
|
from nonebot.exception import (
|
||||||
|
FinishedException,
|
||||||
|
PausedException,
|
||||||
|
RejectedException,
|
||||||
|
SkippedException,
|
||||||
|
StopPropagation,
|
||||||
|
)
|
||||||
from nonebot.internal.adapter import (
|
from nonebot.internal.adapter import (
|
||||||
Bot,
|
Bot,
|
||||||
Event,
|
Event,
|
||||||
@@ -36,37 +48,27 @@ from nonebot.internal.adapter import (
|
|||||||
MessageSegment,
|
MessageSegment,
|
||||||
MessageTemplate,
|
MessageTemplate,
|
||||||
)
|
)
|
||||||
from nonebot.typing import (
|
|
||||||
T_State,
|
|
||||||
T_Handler,
|
|
||||||
T_TypeUpdater,
|
|
||||||
T_DependencyCache,
|
|
||||||
T_PermissionUpdater,
|
|
||||||
)
|
|
||||||
from nonebot.consts import (
|
|
||||||
ARG_KEY,
|
|
||||||
RECEIVE_KEY,
|
|
||||||
REJECT_TARGET,
|
|
||||||
LAST_RECEIVE_KEY,
|
|
||||||
REJECT_CACHE_TARGET,
|
|
||||||
)
|
|
||||||
from nonebot.exception import (
|
|
||||||
PausedException,
|
|
||||||
StopPropagation,
|
|
||||||
SkippedException,
|
|
||||||
FinishedException,
|
|
||||||
RejectedException,
|
|
||||||
)
|
|
||||||
from nonebot.internal.params import (
|
from nonebot.internal.params import (
|
||||||
Depends,
|
|
||||||
ArgParam,
|
ArgParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
|
||||||
StateParam,
|
|
||||||
DependParam,
|
|
||||||
DefaultParam,
|
DefaultParam,
|
||||||
|
DependParam,
|
||||||
|
Depends,
|
||||||
|
EventParam,
|
||||||
MatcherParam,
|
MatcherParam,
|
||||||
|
StateParam,
|
||||||
)
|
)
|
||||||
|
from nonebot.internal.permission import Permission, User
|
||||||
|
from nonebot.internal.rule import Rule
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.typing import (
|
||||||
|
T_DependencyCache,
|
||||||
|
T_Handler,
|
||||||
|
T_PermissionUpdater,
|
||||||
|
T_State,
|
||||||
|
T_TypeUpdater,
|
||||||
|
)
|
||||||
|
from nonebot.utils import classproperty, flatten_exception_group
|
||||||
|
|
||||||
from . import matchers
|
from . import matchers
|
||||||
|
|
||||||
@@ -78,15 +80,15 @@ T = TypeVar("T")
|
|||||||
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")
|
||||||
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
current_handler: ContextVar[Dependent[Any]] = ContextVar("current_handler")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MatcherSource:
|
class MatcherSource:
|
||||||
"""Matcher 源代码上下文信息"""
|
"""Matcher 源代码上下文信息"""
|
||||||
|
|
||||||
plugin_name: Optional[str] = None
|
plugin_id: Optional[str] = None
|
||||||
"""事件响应器所在插件名称"""
|
"""事件响应器所在插件标识符"""
|
||||||
module_name: Optional[str] = None
|
module_name: Optional[str] = None
|
||||||
"""事件响应器所在插件模块的路径名"""
|
"""事件响应器所在插件模块的路径名"""
|
||||||
lineno: Optional[int] = None
|
lineno: Optional[int] = None
|
||||||
@@ -97,8 +99,13 @@ class MatcherSource:
|
|||||||
"""事件响应器所在插件"""
|
"""事件响应器所在插件"""
|
||||||
from nonebot.plugin import get_plugin
|
from nonebot.plugin import get_plugin
|
||||||
|
|
||||||
if self.plugin_name is not None:
|
if self.plugin_id is not None:
|
||||||
return get_plugin(self.plugin_name)
|
return get_plugin(self.plugin_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_name(self) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件名"""
|
||||||
|
return self.plugin and self.plugin.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self) -> Optional[ModuleType]:
|
def module(self) -> Optional[ModuleType]:
|
||||||
@@ -141,7 +148,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
"""事件响应器匹配规则"""
|
"""事件响应器匹配规则"""
|
||||||
permission: ClassVar[Permission] = Permission()
|
permission: ClassVar[Permission] = Permission()
|
||||||
"""事件响应器触发权限"""
|
"""事件响应器触发权限"""
|
||||||
handlers: List[Dependent[Any]] = []
|
handlers: ClassVar[list[Dependent[Any]]] = []
|
||||||
"""事件响应器拥有的事件处理函数列表"""
|
"""事件响应器拥有的事件处理函数列表"""
|
||||||
priority: ClassVar[int] = 1
|
priority: ClassVar[int] = 1
|
||||||
"""事件响应器优先级"""
|
"""事件响应器优先级"""
|
||||||
@@ -160,7 +167,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||||
"""事件响应器权限更新函数"""
|
"""事件响应器权限更新函数"""
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES: ClassVar[Tuple[Type[Param], ...]] = (
|
HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = ( # noqa: UP006
|
||||||
DependParam,
|
DependParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
EventParam,
|
||||||
@@ -171,7 +178,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handlers = self.handlers.copy()
|
self.remain_handlers: list[Dependent[Any]] = self.handlers.copy()
|
||||||
self.state = self._default_state.copy()
|
self.state = self._default_state.copy()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -192,7 +199,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
type_: str = "",
|
type_: str = "",
|
||||||
rule: Optional[Rule] = None,
|
rule: Optional[Rule] = None,
|
||||||
permission: Optional[Permission] = None,
|
permission: Optional[Permission] = None,
|
||||||
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
|
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
|
||||||
temp: bool = False,
|
temp: bool = False,
|
||||||
priority: int = 1,
|
priority: int = 1,
|
||||||
block: bool = False,
|
block: bool = False,
|
||||||
@@ -206,7 +213,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
default_permission_updater: Optional[
|
default_permission_updater: Optional[
|
||||||
Union[T_PermissionUpdater, Dependent[Permission]]
|
Union[T_PermissionUpdater, Dependent[Permission]]
|
||||||
] = None,
|
] = None,
|
||||||
) -> Type[Self]:
|
) -> Type[Self]: # noqa: UP006
|
||||||
"""
|
"""
|
||||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||||
|
|
||||||
@@ -247,7 +254,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
)
|
)
|
||||||
source = source or (
|
source = source or (
|
||||||
MatcherSource(
|
MatcherSource(
|
||||||
plugin_name=plugin and plugin.name,
|
plugin_id=plugin and plugin.id_,
|
||||||
module_name=module and module.__name__,
|
module_name=module and module.__name__,
|
||||||
)
|
)
|
||||||
if plugin is not None or module is not None
|
if plugin is not None or module is not None
|
||||||
@@ -262,16 +269,20 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
"type": type_,
|
"type": type_,
|
||||||
"rule": rule or Rule(),
|
"rule": rule or Rule(),
|
||||||
"permission": permission or Permission(),
|
"permission": permission or Permission(),
|
||||||
"handlers": [
|
"handlers": (
|
||||||
handler
|
[
|
||||||
if isinstance(handler, Dependent)
|
(
|
||||||
else Dependent[Any].parse(
|
handler
|
||||||
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
|
if isinstance(handler, Dependent)
|
||||||
)
|
else Dependent[Any].parse(
|
||||||
for handler in handlers
|
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
|
||||||
]
|
)
|
||||||
if handlers
|
)
|
||||||
else [],
|
for handler in handlers
|
||||||
|
]
|
||||||
|
if handlers
|
||||||
|
else []
|
||||||
|
),
|
||||||
"temp": temp,
|
"temp": temp,
|
||||||
"expire_time": (
|
"expire_time": (
|
||||||
expire_time
|
expire_time
|
||||||
@@ -313,7 +324,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
matchers[priority].append(NewMatcher)
|
matchers[priority].append(NewMatcher)
|
||||||
|
|
||||||
return NewMatcher
|
return NewMatcher # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def destroy(cls) -> None:
|
def destroy(cls) -> None:
|
||||||
@@ -326,15 +337,20 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
return cls._source and cls._source.plugin
|
return cls._source and cls._source.plugin
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module(cls) -> Optional[ModuleType]:
|
def plugin_id(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件模块"""
|
"""事件响应器所在插件标识符"""
|
||||||
return cls._source and cls._source.module
|
return cls._source and cls._source.plugin_id
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def plugin_name(cls) -> Optional[str]:
|
def plugin_name(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件名"""
|
"""事件响应器所在插件名"""
|
||||||
return cls._source and cls._source.plugin_name
|
return cls._source and cls._source.plugin_name
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module(cls) -> Optional[ModuleType]:
|
||||||
|
"""事件响应器所在插件模块"""
|
||||||
|
return cls._source and cls._source.module
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module_name(cls) -> Optional[str]:
|
def module_name(cls) -> Optional[str]:
|
||||||
"""事件响应器所在插件模块路径"""
|
"""事件响应器所在插件模块路径"""
|
||||||
@@ -453,7 +469,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
parameterless: 非参数类型依赖列表
|
parameterless: 非参数类型依赖列表
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
|
async def _receive(event: Event, matcher: "Matcher") -> None:
|
||||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
matcher.set_target(RECEIVE_KEY.format(id=id))
|
||||||
if matcher.get_target() == RECEIVE_KEY.format(id=id):
|
if matcher.get_target() == RECEIVE_KEY.format(id=id):
|
||||||
matcher.set_receive(id, event)
|
matcher.set_receive(id, event)
|
||||||
@@ -546,8 +562,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
"""
|
"""
|
||||||
bot = current_bot.get()
|
bot = current_bot.get()
|
||||||
event = current_event.get()
|
event = current_event.get()
|
||||||
state = current_matcher.get().state
|
|
||||||
if isinstance(message, MessageTemplate):
|
if isinstance(message, MessageTemplate):
|
||||||
|
state = current_matcher.get().state
|
||||||
_message = message.format(**state)
|
_message = message.format(**state)
|
||||||
else:
|
else:
|
||||||
_message = message
|
_message = message
|
||||||
@@ -583,8 +599,15 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
请参考对应 adapter 的 bot 对象 api
|
请参考对应 adapter 的 bot 对象 api
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
matcher = current_matcher.get()
|
||||||
|
except Exception:
|
||||||
|
matcher = None
|
||||||
|
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
await cls.send(prompt, **kwargs)
|
result = await cls.send(prompt, **kwargs)
|
||||||
|
if matcher is not None:
|
||||||
|
matcher.state[PAUSE_PROMPT_RESULT_KEY] = result
|
||||||
raise PausedException
|
raise PausedException
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -601,8 +624,19 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
请参考对应 adapter 的 bot 对象 api
|
请参考对应 adapter 的 bot 对象 api
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
matcher = current_matcher.get()
|
||||||
|
key = matcher.get_target()
|
||||||
|
except Exception:
|
||||||
|
matcher = None
|
||||||
|
key = None
|
||||||
|
|
||||||
|
key = REJECT_PROMPT_RESULT_KEY.format(key=key) if key is not None else None
|
||||||
|
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
await cls.send(prompt, **kwargs)
|
result = await cls.send(prompt, **kwargs)
|
||||||
|
if key is not None and matcher:
|
||||||
|
matcher.state[key] = result
|
||||||
raise RejectedException
|
raise RejectedException
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -622,9 +656,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
请参考对应 adapter 的 bot 对象 api
|
请参考对应 adapter 的 bot 对象 api
|
||||||
"""
|
"""
|
||||||
matcher = current_matcher.get()
|
matcher = current_matcher.get()
|
||||||
matcher.set_target(ARG_KEY.format(key=key))
|
arg_key = ARG_KEY.format(key=key)
|
||||||
|
matcher.set_target(arg_key)
|
||||||
|
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
await cls.send(prompt, **kwargs)
|
result = await cls.send(prompt, **kwargs)
|
||||||
|
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=arg_key)] = result
|
||||||
raise RejectedException
|
raise RejectedException
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -644,9 +681,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
请参考对应 adapter 的 bot 对象 api
|
请参考对应 adapter 的 bot 对象 api
|
||||||
"""
|
"""
|
||||||
matcher = current_matcher.get()
|
matcher = current_matcher.get()
|
||||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
receive_key = RECEIVE_KEY.format(id=id)
|
||||||
|
matcher.set_target(receive_key)
|
||||||
|
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
await cls.send(prompt, **kwargs)
|
result = await cls.send(prompt, **kwargs)
|
||||||
|
matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=receive_key)] = result
|
||||||
raise RejectedException
|
raise RejectedException
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -658,12 +698,10 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
raise SkippedException
|
raise SkippedException
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_receive(self, id: str) -> Union[Event, None]:
|
def get_receive(self, id: str) -> Union[Event, None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_receive(self, id: str, default: T) -> Union[Event, T]:
|
def get_receive(self, id: str, default: T) -> Union[Event, T]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def get_receive(
|
def get_receive(
|
||||||
self, id: str, default: Optional[T] = None
|
self, id: str, default: Optional[T] = None
|
||||||
@@ -680,12 +718,10 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self.state[LAST_RECEIVE_KEY] = event
|
self.state[LAST_RECEIVE_KEY] = event
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_last_receive(self) -> Union[Event, None]:
|
def get_last_receive(self) -> Union[Event, None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_last_receive(self, default: T) -> Union[Event, T]:
|
def get_last_receive(self, default: T) -> Union[Event, T]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def get_last_receive(
|
def get_last_receive(
|
||||||
self, default: Optional[T] = None
|
self, default: Optional[T] = None
|
||||||
@@ -697,12 +733,10 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
return self.state.get(LAST_RECEIVE_KEY, default)
|
return self.state.get(LAST_RECEIVE_KEY, default)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_arg(self, key: str) -> Union[Message, None]:
|
def get_arg(self, key: str) -> Union[Message, None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_arg(self, key: str, default: T) -> Union[Message, T]:
|
def get_arg(self, key: str, default: T) -> Union[Message, T]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def get_arg(
|
def get_arg(
|
||||||
self, key: str, default: Optional[T] = None
|
self, key: str, default: Optional[T] = None
|
||||||
@@ -724,12 +758,10 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self.state[REJECT_TARGET] = target
|
self.state[REJECT_TARGET] = target
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_target(self) -> Union[str, None]:
|
def get_target(self) -> Union[str, None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_target(self, default: T) -> Union[str, T]:
|
def get_target(self, default: T) -> Union[str, T]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def get_target(self, default: Optional[T] = None) -> Optional[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)
|
||||||
@@ -779,7 +811,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
async def resolve_reject(self):
|
async def resolve_reject(self):
|
||||||
handler = current_handler.get()
|
handler = current_handler.get()
|
||||||
self.handlers.insert(0, handler)
|
self.remain_handlers.insert(0, handler)
|
||||||
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]
|
||||||
|
|
||||||
@@ -808,28 +840,34 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
f"bot={bot}, event={event!r}, state={state!r}"
|
f"bot={bot}, event={event!r}, state={state!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):
|
||||||
|
self.block = True
|
||||||
|
|
||||||
with self.ensure_context(bot, event):
|
with self.ensure_context(bot, event):
|
||||||
try:
|
try:
|
||||||
# Refresh preprocess state
|
with catch({StopPropagation: _handle_stop_propagation}):
|
||||||
self.state.update(state)
|
# Refresh preprocess state
|
||||||
|
self.state.update(state)
|
||||||
|
|
||||||
while self.handlers:
|
while self.remain_handlers:
|
||||||
handler = self.handlers.pop(0)
|
handler = self.remain_handlers.pop(0)
|
||||||
current_handler.set(handler)
|
current_handler.set(handler)
|
||||||
logger.debug(f"Running handler {handler}")
|
logger.debug(f"Running handler {handler}")
|
||||||
try:
|
|
||||||
await handler(
|
def _handle_skipped(
|
||||||
matcher=self,
|
exc_group: BaseExceptionGroup[SkippedException],
|
||||||
bot=bot,
|
):
|
||||||
event=event,
|
logger.debug(f"Handler {handler} skipped")
|
||||||
state=self.state,
|
|
||||||
stack=stack,
|
with catch({SkippedException: _handle_skipped}):
|
||||||
dependency_cache=dependency_cache,
|
await handler(
|
||||||
)
|
matcher=self,
|
||||||
except SkippedException:
|
bot=bot,
|
||||||
logger.debug(f"Handler {handler} skipped")
|
event=event,
|
||||||
except StopPropagation:
|
state=self.state,
|
||||||
self.block = True
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
logger.info(f"{self} running complete")
|
logger.info(f"{self} running complete")
|
||||||
|
|
||||||
@@ -842,10 +880,54 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
stack: Optional[AsyncExitStack] = None,
|
stack: Optional[AsyncExitStack] = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
):
|
):
|
||||||
try:
|
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = (
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_special_exception(
|
||||||
|
exc_group: BaseExceptionGroup[
|
||||||
|
Union[FinishedException, RejectedException, PausedException]
|
||||||
|
],
|
||||||
|
):
|
||||||
|
nonlocal exc
|
||||||
|
excs = list(flatten_exception_group(exc_group))
|
||||||
|
if len(excs) > 1:
|
||||||
|
logger.warning(
|
||||||
|
"Multiple session control exceptions occurred. "
|
||||||
|
"NoneBot will choose the proper one."
|
||||||
|
)
|
||||||
|
finished_exc = next(
|
||||||
|
(e for e in excs if isinstance(e, FinishedException)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
rejected_exc = next(
|
||||||
|
(e for e in excs if isinstance(e, RejectedException)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
paused_exc = next(
|
||||||
|
(e for e in excs if isinstance(e, PausedException)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
exc = finished_exc or rejected_exc or paused_exc
|
||||||
|
elif isinstance(
|
||||||
|
excs[0], (FinishedException, RejectedException, PausedException)
|
||||||
|
):
|
||||||
|
exc = excs[0]
|
||||||
|
|
||||||
|
with catch(
|
||||||
|
{
|
||||||
|
(
|
||||||
|
FinishedException,
|
||||||
|
RejectedException,
|
||||||
|
PausedException,
|
||||||
|
): _handle_special_exception
|
||||||
|
}
|
||||||
|
):
|
||||||
await self.simple_run(bot, event, state, stack, dependency_cache)
|
await self.simple_run(bot, event, state, stack, dependency_cache)
|
||||||
|
|
||||||
except RejectedException:
|
if isinstance(exc, FinishedException):
|
||||||
|
pass
|
||||||
|
elif isinstance(exc, RejectedException):
|
||||||
await self.resolve_reject()
|
await self.resolve_reject()
|
||||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||||
permission = await self.update_permission(
|
permission = await self.update_permission(
|
||||||
@@ -856,7 +938,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
type_,
|
type_,
|
||||||
Rule(),
|
Rule(),
|
||||||
permission,
|
permission,
|
||||||
self.handlers,
|
self.remain_handlers,
|
||||||
temp=True,
|
temp=True,
|
||||||
priority=0,
|
priority=0,
|
||||||
block=True,
|
block=True,
|
||||||
@@ -866,7 +948,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
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:
|
elif isinstance(exc, PausedException):
|
||||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||||
permission = await self.update_permission(
|
permission = await self.update_permission(
|
||||||
bot, event, stack, dependency_cache
|
bot, event, stack, dependency_cache
|
||||||
@@ -876,7 +958,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
type_,
|
type_,
|
||||||
Rule(),
|
Rule(),
|
||||||
permission,
|
permission,
|
||||||
self.handlers,
|
self.remain_handlers,
|
||||||
temp=True,
|
temp=True,
|
||||||
priority=0,
|
priority=0,
|
||||||
block=True,
|
block=True,
|
||||||
@@ -886,5 +968,3 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
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:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import abc
|
import abc
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
|
from collections.abc import Mapping, MutableMapping
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .matcher import Matcher
|
from .matcher import Matcher
|
||||||
|
|
||||||
|
|
||||||
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
|
class MatcherProvider(abc.ABC, MutableMapping[int, list[type["Matcher"]]]):
|
||||||
"""事件响应器存储器基类
|
"""事件响应器存储器基类
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -14,12 +15,12 @@ class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class _DictProvider(defaultdict, MatcherProvider):
|
class _DictProvider(defaultdict, MatcherProvider):
|
||||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]):
|
||||||
super().__init__(list, matchers)
|
super().__init__(list, matchers)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,47 @@
|
|||||||
import asyncio
|
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
||||||
|
from enum import Enum
|
||||||
import inspect
|
import inspect
|
||||||
from typing_extensions import Self, Annotated, override
|
|
||||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
Literal,
|
|
||||||
Callable,
|
Callable,
|
||||||
|
Literal,
|
||||||
Optional,
|
Optional,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import Self, get_args, get_origin, override
|
||||||
|
|
||||||
from pydantic.typing import get_args, get_origin
|
import anyio
|
||||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
||||||
|
|
||||||
|
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
|
||||||
|
from nonebot.consts import ARG_KEY, REJECT_PROMPT_RESULT_KEY
|
||||||
|
from nonebot.dependencies import Dependent, Param
|
||||||
from nonebot.dependencies.utils import check_field_type
|
from nonebot.dependencies.utils import check_field_type
|
||||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
from nonebot.exception import SkippedException
|
||||||
from nonebot.typing import T_State, T_Handler, T_DependencyCache
|
from nonebot.typing import (
|
||||||
|
_STATE_FLAG,
|
||||||
|
T_DependencyCache,
|
||||||
|
T_Handler,
|
||||||
|
T_State,
|
||||||
|
origin_is_annotated,
|
||||||
|
)
|
||||||
from nonebot.utils import (
|
from nonebot.utils import (
|
||||||
|
generic_check_issubclass,
|
||||||
get_name,
|
get_name,
|
||||||
run_sync,
|
|
||||||
is_gen_callable,
|
|
||||||
run_sync_ctx_manager,
|
|
||||||
is_async_gen_callable,
|
is_async_gen_callable,
|
||||||
is_coroutine_callable,
|
is_coroutine_callable,
|
||||||
generic_check_issubclass,
|
is_gen_callable,
|
||||||
|
run_sync,
|
||||||
|
run_sync_ctx_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from nonebot.adapters import Bot, Event, Message
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
from nonebot.adapters import Bot, Event
|
|
||||||
|
|
||||||
EXTRA_FIELD_INFO = (
|
|
||||||
"gt",
|
|
||||||
"lt",
|
|
||||||
"ge",
|
|
||||||
"le",
|
|
||||||
"multiple_of",
|
|
||||||
"allow_inf_nan",
|
|
||||||
"max_digits",
|
|
||||||
"decimal_places",
|
|
||||||
"min_items",
|
|
||||||
"max_items",
|
|
||||||
"unique_items",
|
|
||||||
"min_length",
|
|
||||||
"max_length",
|
|
||||||
"regex",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DependsInner:
|
class DependsInner:
|
||||||
@@ -58,7 +50,7 @@ class DependsInner:
|
|||||||
dependency: Optional[T_Handler] = None,
|
dependency: Optional[T_Handler] = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
validate: Union[bool, FieldInfo] = False,
|
validate: Union[bool, PydanticFieldInfo] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.dependency = dependency
|
self.dependency = dependency
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
@@ -75,7 +67,7 @@ def Depends(
|
|||||||
dependency: Optional[T_Handler] = None,
|
dependency: Optional[T_Handler] = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
validate: Union[bool, FieldInfo] = False,
|
validate: Union[bool, PydanticFieldInfo] = False,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""子依赖装饰器
|
"""子依赖装饰器
|
||||||
|
|
||||||
@@ -105,6 +97,78 @@ def Depends(
|
|||||||
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
||||||
|
|
||||||
|
|
||||||
|
class CacheState(str, Enum):
|
||||||
|
"""子依赖缓存状态"""
|
||||||
|
|
||||||
|
PENDING = "PENDING"
|
||||||
|
FINISHED = "FINISHED"
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyCache:
|
||||||
|
"""子依赖结果缓存。
|
||||||
|
|
||||||
|
用于缓存子依赖的结果,以避免重复计算。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._state = CacheState.PENDING
|
||||||
|
self._result: Any = None
|
||||||
|
self._exception: Optional[BaseException] = None
|
||||||
|
self._waiter = anyio.Event()
|
||||||
|
|
||||||
|
def done(self) -> bool:
|
||||||
|
return self._state == CacheState.FINISHED
|
||||||
|
|
||||||
|
def result(self) -> Any:
|
||||||
|
"""获取子依赖结果"""
|
||||||
|
|
||||||
|
if self._state != CacheState.FINISHED:
|
||||||
|
raise RuntimeError("Result is not ready")
|
||||||
|
|
||||||
|
if self._exception is not None:
|
||||||
|
raise self._exception
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def exception(self) -> Optional[BaseException]:
|
||||||
|
"""获取子依赖异常"""
|
||||||
|
|
||||||
|
if self._state != CacheState.FINISHED:
|
||||||
|
raise RuntimeError("Result is not ready")
|
||||||
|
|
||||||
|
return self._exception
|
||||||
|
|
||||||
|
def set_result(self, result: Any) -> None:
|
||||||
|
"""设置子依赖结果"""
|
||||||
|
|
||||||
|
if self._state != CacheState.PENDING:
|
||||||
|
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||||
|
|
||||||
|
self._result = result
|
||||||
|
self._state = CacheState.FINISHED
|
||||||
|
self._waiter.set()
|
||||||
|
|
||||||
|
def set_exception(self, exception: BaseException) -> None:
|
||||||
|
"""设置子依赖异常"""
|
||||||
|
|
||||||
|
if self._state != CacheState.PENDING:
|
||||||
|
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||||
|
|
||||||
|
self._exception = exception
|
||||||
|
self._state = CacheState.FINISHED
|
||||||
|
self._waiter.set()
|
||||||
|
|
||||||
|
async def wait(self):
|
||||||
|
"""等待子依赖结果"""
|
||||||
|
await self._waiter.wait()
|
||||||
|
if self._state != CacheState.FINISHED:
|
||||||
|
raise RuntimeError("Invalid cache state")
|
||||||
|
|
||||||
|
if self._exception is not None:
|
||||||
|
raise self._exception
|
||||||
|
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
|
||||||
class DependParam(Param):
|
class DependParam(Param):
|
||||||
"""子依赖注入参数。
|
"""子依赖注入参数。
|
||||||
|
|
||||||
@@ -113,29 +177,37 @@ class DependParam(Param):
|
|||||||
本注入应该具有最高优先级,因此应该在其他参数之前检查。
|
本注入应该具有最高优先级,因此应该在其他参数之前检查。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.dependent = dependent
|
||||||
|
self.use_cache = use_cache
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Depends({self.extra['dependent']})"
|
return f"Depends({self.dependent}, use_cache={self.use_cache})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_field(
|
def _from_field(
|
||||||
cls, sub_dependent: Dependent, use_cache: bool, validate: Union[bool, FieldInfo]
|
cls,
|
||||||
|
sub_dependent: Dependent[Any],
|
||||||
|
use_cache: bool,
|
||||||
|
validate: Union[bool, PydanticFieldInfo],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if isinstance(validate, FieldInfo):
|
if isinstance(validate, PydanticFieldInfo):
|
||||||
kwargs.update((k, getattr(validate, k)) for k in EXTRA_FIELD_INFO)
|
kwargs.update(extract_field_info(validate))
|
||||||
|
|
||||||
return cls(
|
kwargs["validate"] = bool(validate)
|
||||||
Required,
|
kwargs["dependent"] = sub_dependent
|
||||||
validate=bool(validate),
|
kwargs["use_cache"] = use_cache
|
||||||
**kwargs,
|
|
||||||
dependent=sub_dependent,
|
return cls(**kwargs)
|
||||||
use_cache=use_cache,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
type_annotation, depends_inner = param.annotation, None
|
type_annotation, depends_inner = param.annotation, None
|
||||||
# extract type annotation and dependency from Annotated
|
# extract type annotation and dependency from Annotated
|
||||||
@@ -175,7 +247,7 @@ class DependParam(Param):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
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"
|
||||||
@@ -191,24 +263,34 @@ class DependParam(Param):
|
|||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
use_cache: bool = self.extra["use_cache"]
|
use_cache: bool = self.use_cache
|
||||||
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 = self.dependent
|
||||||
call = cast(Callable[..., Any], sub_dependent.call)
|
call = cast(Callable[..., Any], sub_dependent.call)
|
||||||
|
|
||||||
# solve sub dependency with current cache
|
# solve sub dependency with current cache
|
||||||
sub_values = await sub_dependent.solve(
|
exc: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||||
stack=stack,
|
|
||||||
dependency_cache=dependency_cache,
|
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||||
**kwargs,
|
nonlocal exc
|
||||||
)
|
exc = exc_group
|
||||||
|
|
||||||
|
with catch({SkippedException: _handle_skipped}):
|
||||||
|
sub_values = await sub_dependent.solve(
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if exc is not None:
|
||||||
|
raise exc
|
||||||
|
|
||||||
# run dependency function
|
# run dependency function
|
||||||
task: asyncio.Task[Any]
|
|
||||||
if use_cache and call in dependency_cache:
|
if use_cache and call in dependency_cache:
|
||||||
return await dependency_cache[call]
|
return await dependency_cache[call].wait()
|
||||||
elif is_gen_callable(call) or is_async_gen_callable(call):
|
|
||||||
|
if is_gen_callable(call) or is_async_gen_callable(call):
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
stack, AsyncExitStack
|
stack, AsyncExitStack
|
||||||
), "Generator dependency should be called in context"
|
), "Generator dependency should be called in context"
|
||||||
@@ -216,23 +298,33 @@ class DependParam(Param):
|
|||||||
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
|
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
|
||||||
else:
|
else:
|
||||||
cm = asynccontextmanager(call)(**sub_values)
|
cm = asynccontextmanager(call)(**sub_values)
|
||||||
task = asyncio.create_task(stack.enter_async_context(cm))
|
|
||||||
dependency_cache[call] = task
|
target = stack.enter_async_context(cm)
|
||||||
return await task
|
|
||||||
elif is_coroutine_callable(call):
|
elif is_coroutine_callable(call):
|
||||||
task = asyncio.create_task(call(**sub_values))
|
target = call(**sub_values)
|
||||||
dependency_cache[call] = task
|
|
||||||
return await task
|
|
||||||
else:
|
else:
|
||||||
task = asyncio.create_task(run_sync(call)(**sub_values))
|
target = run_sync(call)(**sub_values)
|
||||||
dependency_cache[call] = task
|
|
||||||
return await task
|
dependency_cache[call] = cache = DependencyCache()
|
||||||
|
try:
|
||||||
|
result = await target
|
||||||
|
except Exception as e:
|
||||||
|
cache.set_exception(e)
|
||||||
|
raise
|
||||||
|
except BaseException as e:
|
||||||
|
cache.set_exception(e)
|
||||||
|
# remove cache when base exception occurs
|
||||||
|
# e.g. CancelledError
|
||||||
|
dependency_cache.pop(call, None)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
cache.set_result(result)
|
||||||
|
return result
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _check(self, **kwargs: Any) -> None:
|
async def _check(self, **kwargs: Any) -> None:
|
||||||
# run sub dependent pre-checkers
|
# run sub dependent pre-checkers
|
||||||
sub_dependent: Dependent = self.extra["dependent"]
|
await self.dependent.check(**kwargs)
|
||||||
await sub_dependent.check(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BotParam(Param):
|
class BotParam(Param):
|
||||||
@@ -243,21 +335,23 @@ class BotParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.checker = checker
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
"BotParam("
|
"BotParam("
|
||||||
+ (
|
+ (repr(self.checker.annotation) if self.checker is not None else "")
|
||||||
repr(cast(ModelField, checker).type_)
|
|
||||||
if (checker := self.extra.get("checker"))
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
|
|
||||||
@@ -265,27 +359,26 @@ class BotParam(Param):
|
|||||||
if generic_check_issubclass(param.annotation, Bot):
|
if generic_check_issubclass(param.annotation, Bot):
|
||||||
checker: Optional[ModelField] = None
|
checker: Optional[ModelField] = None
|
||||||
if param.annotation is not Bot:
|
if param.annotation is not Bot:
|
||||||
checker = ModelField(
|
checker = ModelField.construct(
|
||||||
name=param.name,
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
type_=param.annotation,
|
|
||||||
class_validators=None,
|
|
||||||
model_config=CustomConfig,
|
|
||||||
default=None,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
return cls(Required, checker=checker)
|
return cls(checker=checker)
|
||||||
# legacy: param is named "bot" and has no type annotation
|
# legacy: param is named "bot" and has no type annotation
|
||||||
elif param.annotation == param.empty and param.name == "bot":
|
elif param.annotation == param.empty and param.name == "bot":
|
||||||
return cls(Required)
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, bot: "Bot", **kwargs: Any
|
||||||
|
) -> Any:
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
if checker := self.extra.get("checker"):
|
self, bot: "Bot", **kwargs: Any
|
||||||
check_field_type(checker, bot)
|
) -> None:
|
||||||
|
if self.checker is not None:
|
||||||
|
check_field_type(self.checker, bot)
|
||||||
|
|
||||||
|
|
||||||
class EventParam(Param):
|
class EventParam(Param):
|
||||||
@@ -296,21 +389,23 @@ class EventParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.checker = checker
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
"EventParam("
|
"EventParam("
|
||||||
+ (
|
+ (repr(self.checker.annotation) if self.checker is not None else "")
|
||||||
repr(cast(ModelField, checker).type_)
|
|
||||||
if (checker := self.extra.get("checker"))
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
@@ -318,27 +413,26 @@ class EventParam(Param):
|
|||||||
if generic_check_issubclass(param.annotation, Event):
|
if generic_check_issubclass(param.annotation, Event):
|
||||||
checker: Optional[ModelField] = None
|
checker: Optional[ModelField] = None
|
||||||
if param.annotation is not Event:
|
if param.annotation is not Event:
|
||||||
checker = ModelField(
|
checker = ModelField.construct(
|
||||||
name=param.name,
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
type_=param.annotation,
|
|
||||||
class_validators=None,
|
|
||||||
model_config=CustomConfig,
|
|
||||||
default=None,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
return cls(Required, checker=checker)
|
return cls(checker=checker)
|
||||||
# legacy: param is named "event" and has no type annotation
|
# legacy: param is named "event" and has no type annotation
|
||||||
elif param.annotation == param.empty and param.name == "event":
|
elif param.annotation == param.empty and param.name == "event":
|
||||||
return cls(Required)
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, event: "Event", **kwargs: Any
|
||||||
|
) -> Any:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
if checker := self.extra.get("checker", None):
|
self, event: "Event", **kwargs: Any
|
||||||
check_field_type(checker, event)
|
) -> Any:
|
||||||
|
if self.checker is not None:
|
||||||
|
check_field_type(self.checker, event)
|
||||||
|
|
||||||
|
|
||||||
class StateParam(Param):
|
class StateParam(Param):
|
||||||
@@ -355,17 +449,21 @@ class StateParam(Param):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
# param type is T_State
|
# param type is T_State
|
||||||
if param.annotation is T_State:
|
if origin_is_annotated(
|
||||||
return cls(Required)
|
get_origin(param.annotation)
|
||||||
|
) and _STATE_FLAG in get_args(param.annotation):
|
||||||
|
return cls()
|
||||||
# legacy: param is named "state" and has no type annotation
|
# legacy: param is named "state" and has no type annotation
|
||||||
elif param.annotation == param.empty and param.name == "state":
|
elif param.annotation == param.empty and param.name == "state":
|
||||||
return cls(Required)
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, state: T_State, **kwargs: Any
|
||||||
|
) -> Any:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
@@ -377,13 +475,23 @@ class MatcherParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.checker = checker
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "MatcherParam()"
|
return (
|
||||||
|
"MatcherParam("
|
||||||
|
+ (repr(self.checker.annotation) if self.checker is not None else "")
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
|
|
||||||
@@ -391,35 +499,34 @@ class MatcherParam(Param):
|
|||||||
if generic_check_issubclass(param.annotation, Matcher):
|
if generic_check_issubclass(param.annotation, Matcher):
|
||||||
checker: Optional[ModelField] = None
|
checker: Optional[ModelField] = None
|
||||||
if param.annotation is not Matcher:
|
if param.annotation is not Matcher:
|
||||||
checker = ModelField(
|
checker = ModelField.construct(
|
||||||
name=param.name,
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
type_=param.annotation,
|
|
||||||
class_validators=None,
|
|
||||||
model_config=CustomConfig,
|
|
||||||
default=None,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
return cls(Required, checker=checker)
|
return cls(checker=checker)
|
||||||
# legacy: param is named "matcher" and has no type annotation
|
# legacy: param is named "matcher" and has no type annotation
|
||||||
elif param.annotation == param.empty and param.name == "matcher":
|
elif param.annotation == param.empty and param.name == "matcher":
|
||||||
return cls(Required)
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, matcher: "Matcher", **kwargs: Any
|
||||||
|
) -> Any:
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
async def _check( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
if checker := self.extra.get("checker", None):
|
self, matcher: "Matcher", **kwargs: Any
|
||||||
check_field_type(checker, matcher)
|
) -> Any:
|
||||||
|
if self.checker is not None:
|
||||||
|
check_field_type(self.checker, matcher)
|
||||||
|
|
||||||
|
|
||||||
class ArgInner:
|
class ArgInner:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
|
self, key: Optional[str], type: Literal["message", "str", "plaintext", "prompt"]
|
||||||
) -> None:
|
) -> None:
|
||||||
self.key = key
|
self.key: Optional[str] = key
|
||||||
self.type = type
|
self.type: Literal["message", "str", "plaintext", "prompt"] = type
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
||||||
@@ -440,6 +547,11 @@ def ArgPlainText(key: Optional[str] = None) -> str:
|
|||||||
return ArgInner(key, "plaintext") # type: ignore
|
return ArgInner(key, "plaintext") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def ArgPromptResult(key: Optional[str] = None) -> Any:
|
||||||
|
"""`arg` prompt 发送结果"""
|
||||||
|
return ArgInner(key, "prompt")
|
||||||
|
|
||||||
|
|
||||||
class ArgParam(Param):
|
class ArgParam(Param):
|
||||||
"""Arg 注入参数
|
"""Arg 注入参数
|
||||||
|
|
||||||
@@ -449,34 +561,61 @@ class ArgParam(Param):
|
|||||||
留空则会根据参数名称获取。
|
留空则会根据参数名称获取。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
key: str,
|
||||||
|
type: Literal["message", "str", "plaintext", "prompt"],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.key = key
|
||||||
|
self.type = type
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
return f"ArgParam(key={self.key!r}, type={self.type!r})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if isinstance(param.default, ArgInner):
|
if isinstance(param.default, ArgInner):
|
||||||
return cls(
|
return cls(key=param.default.key or param.name, type=param.default.type)
|
||||||
Required, key=param.default.key or param.name, type=param.default.type
|
|
||||||
)
|
|
||||||
elif get_origin(param.annotation) is Annotated:
|
elif get_origin(param.annotation) is Annotated:
|
||||||
for arg in get_args(param.annotation)[:0:-1]:
|
for arg in get_args(param.annotation)[:0:-1]:
|
||||||
if isinstance(arg, ArgInner):
|
if isinstance(arg, ArgInner):
|
||||||
return cls(Required, key=arg.key or param.name, type=arg.type)
|
return cls(key=arg.key or param.name, type=arg.type)
|
||||||
|
|
||||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
async def _solve( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
key: str = self.extra["key"]
|
self, matcher: "Matcher", **kwargs: Any
|
||||||
message = matcher.get_arg(key)
|
) -> Any:
|
||||||
if message is None:
|
if self.type == "message":
|
||||||
return message
|
return self._solve_message(matcher)
|
||||||
if self.extra["type"] == "message":
|
elif self.type == "str":
|
||||||
return message
|
return self._solve_str(matcher)
|
||||||
elif self.extra["type"] == "str":
|
elif self.type == "plaintext":
|
||||||
return str(message)
|
return self._solve_plaintext(matcher)
|
||||||
|
elif self.type == "prompt":
|
||||||
|
return self._solve_prompt(matcher)
|
||||||
else:
|
else:
|
||||||
return message.extract_plain_text()
|
raise ValueError(f"Unknown Arg type: {self.type}")
|
||||||
|
|
||||||
|
def _solve_message(self, matcher: "Matcher") -> Optional["Message"]:
|
||||||
|
return matcher.get_arg(self.key)
|
||||||
|
|
||||||
|
def _solve_str(self, matcher: "Matcher") -> Optional[str]:
|
||||||
|
message = matcher.get_arg(self.key)
|
||||||
|
return str(message) if message is not None else None
|
||||||
|
|
||||||
|
def _solve_plaintext(self, matcher: "Matcher") -> Optional[str]:
|
||||||
|
message = matcher.get_arg(self.key)
|
||||||
|
return message.extract_plain_text() if message is not None else None
|
||||||
|
|
||||||
|
def _solve_prompt(self, matcher: "Matcher") -> Optional[Any]:
|
||||||
|
return matcher.state.get(
|
||||||
|
REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExceptionParam(Param):
|
class ExceptionParam(Param):
|
||||||
@@ -493,14 +632,14 @@ class ExceptionParam(Param):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
# param type is Exception(s) or subclass(es) of Exception or None
|
# param type is Exception(s) or subclass(es) of Exception or None
|
||||||
if generic_check_issubclass(param.annotation, Exception):
|
if generic_check_issubclass(param.annotation, Exception):
|
||||||
return cls(Required)
|
return cls()
|
||||||
# legacy: param is named "exception" and has no type annotation
|
# legacy: param is named "exception" and has no type annotation
|
||||||
elif param.annotation == param.empty and param.name == "exception":
|
elif param.annotation == param.empty and param.name == "exception":
|
||||||
return cls(Required)
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
||||||
@@ -521,14 +660,14 @@ class DefaultParam(Param):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if param.default != param.empty:
|
if param.default != param.empty:
|
||||||
return cls(param.default)
|
return cls(default=param.default)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, **kwargs: Any) -> Any:
|
async def _solve(self, **kwargs: Any) -> Any:
|
||||||
return Undefined
|
return PydanticUndefined
|
||||||
|
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import asyncio
|
|
||||||
from typing_extensions import Self
|
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Set, Tuple, Union, NoReturn, Optional
|
from typing import ClassVar, NoReturn, Optional, Union
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.utils import run_coro_with_catch
|
|
||||||
from nonebot.exception import SkippedException
|
from nonebot.exception import SkippedException
|
||||||
from nonebot.typing import T_DependencyCache, T_PermissionChecker
|
from nonebot.typing import T_DependencyCache, T_PermissionChecker
|
||||||
|
from nonebot.utils import run_coro_with_catch
|
||||||
|
|
||||||
from .adapter import Bot, Event
|
from .adapter import Bot, Event
|
||||||
from .params import BotParam, EventParam, DependParam, DefaultParam
|
from .params import BotParam, DefaultParam, DependParam, EventParam, Param
|
||||||
|
|
||||||
|
|
||||||
class Permission:
|
class Permission:
|
||||||
@@ -30,7 +31,7 @@ class Permission:
|
|||||||
|
|
||||||
__slots__ = ("checkers",)
|
__slots__ = ("checkers",)
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES = [
|
HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [
|
||||||
DependParam,
|
DependParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
EventParam,
|
||||||
@@ -38,11 +39,13 @@ 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]] = {
|
self.checkers: set[Dependent[bool]] = {
|
||||||
checker
|
(
|
||||||
if isinstance(checker, Dependent)
|
checker
|
||||||
else Dependent[bool].parse(
|
if isinstance(checker, Dependent)
|
||||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
else Dependent[bool].parse(
|
||||||
|
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for checker in checkers
|
for checker in checkers
|
||||||
}
|
}
|
||||||
@@ -68,22 +71,26 @@ class Permission:
|
|||||||
"""
|
"""
|
||||||
if not self.checkers:
|
if not self.checkers:
|
||||||
return True
|
return True
|
||||||
results = await asyncio.gather(
|
|
||||||
*(
|
result = False
|
||||||
run_coro_with_catch(
|
|
||||||
checker(
|
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||||
bot=bot,
|
nonlocal result
|
||||||
event=event,
|
# calculate the result first to avoid data racing
|
||||||
stack=stack,
|
is_passed = await run_coro_with_catch(
|
||||||
dependency_cache=dependency_cache,
|
checker(
|
||||||
),
|
bot=bot, event=event, stack=stack, dependency_cache=dependency_cache
|
||||||
(SkippedException,),
|
),
|
||||||
False,
|
(SkippedException,),
|
||||||
)
|
False,
|
||||||
for checker in self.checkers
|
)
|
||||||
),
|
result |= is_passed
|
||||||
)
|
|
||||||
return any(results)
|
async with anyio.create_task_group() as tg:
|
||||||
|
for checker in self.checkers:
|
||||||
|
tg.start_soon(_run_checker, checker)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def __and__(self, other: object) -> NoReturn:
|
def __and__(self, other: object) -> NoReturn:
|
||||||
raise RuntimeError("And operation between Permissions is not allowed.")
|
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||||
@@ -117,10 +124,10 @@ class User:
|
|||||||
perm: 需同时满足的权限
|
perm: 需同时满足的权限
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("users", "perm")
|
__slots__ = ("perm", "users")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, users: Tuple[str, ...], perm: Optional[Permission] = None
|
self, users: tuple[str, ...], perm: Optional[Permission] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.users = users
|
self.users = users
|
||||||
self.perm = perm
|
self.perm = perm
|
||||||
@@ -144,7 +151,7 @@ class User:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
||||||
if len(perm.checkers) == 1 and isinstance(
|
if len(perm.checkers) == 1 and isinstance(
|
||||||
user_perm := tuple(perm.checkers)[0].call, cls
|
user_perm := next(iter(perm.checkers)).call, cls
|
||||||
):
|
):
|
||||||
return user_perm.perm
|
return user_perm.perm
|
||||||
return perm
|
return perm
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import asyncio
|
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Set, Union, NoReturn, Optional
|
from typing import ClassVar, NoReturn, Optional, Union
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.exception import SkippedException
|
from nonebot.exception import SkippedException
|
||||||
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
|
from nonebot.typing import T_DependencyCache, T_RuleChecker, T_State
|
||||||
|
|
||||||
from .adapter import Bot, Event
|
from .adapter import Bot, Event
|
||||||
from .params import BotParam, EventParam, StateParam, DependParam, DefaultParam
|
from .params import BotParam, DefaultParam, DependParam, EventParam, Param, StateParam
|
||||||
|
|
||||||
|
|
||||||
class Rule:
|
class Rule:
|
||||||
@@ -28,7 +30,7 @@ class Rule:
|
|||||||
|
|
||||||
__slots__ = ("checkers",)
|
__slots__ = ("checkers",)
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES = [
|
HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [
|
||||||
DependParam,
|
DependParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
EventParam,
|
||||||
@@ -37,11 +39,13 @@ 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]] = {
|
self.checkers: set[Dependent[bool]] = {
|
||||||
checker
|
(
|
||||||
if isinstance(checker, Dependent)
|
checker
|
||||||
else Dependent[bool].parse(
|
if isinstance(checker, Dependent)
|
||||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
else Dependent[bool].parse(
|
||||||
|
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for checker in checkers
|
for checker in checkers
|
||||||
}
|
}
|
||||||
@@ -69,22 +73,33 @@ class Rule:
|
|||||||
"""
|
"""
|
||||||
if not self.checkers:
|
if not self.checkers:
|
||||||
return True
|
return True
|
||||||
try:
|
|
||||||
results = await asyncio.gather(
|
result = True
|
||||||
*(
|
|
||||||
checker(
|
def _handle_skipped_exception(
|
||||||
bot=bot,
|
exc_group: BaseExceptionGroup[SkippedException],
|
||||||
event=event,
|
) -> None:
|
||||||
state=state,
|
nonlocal result
|
||||||
stack=stack,
|
result = False
|
||||||
dependency_cache=dependency_cache,
|
|
||||||
)
|
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||||
for checker in self.checkers
|
nonlocal result
|
||||||
)
|
# calculate the result first to avoid data racing
|
||||||
|
is_passed = await checker(
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
state=state,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
)
|
)
|
||||||
except SkippedException:
|
result &= is_passed
|
||||||
return False
|
|
||||||
return all(results)
|
with catch({SkippedException: _handle_skipped_exception}):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for checker in self.checkers:
|
||||||
|
tg.start_soon(_run_checker, checker)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||||
if other is None:
|
if other is None:
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
|||||||
[loguru]: https://github.com/Delgan/loguru
|
[loguru]: https://github.com/Delgan/loguru
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 7
|
sidebar_position: 7
|
||||||
description: nonebot.log 模块
|
description: nonebot.log 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
@@ -45,6 +48,7 @@ logger: "Logger" = loguru.logger
|
|||||||
# logger.addHandler(default_handler)
|
# logger.addHandler(default_handler)
|
||||||
|
|
||||||
|
|
||||||
|
# https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
|
||||||
class LoguruHandler(logging.Handler): # pragma: no cover
|
class LoguruHandler(logging.Handler): # pragma: no cover
|
||||||
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
|
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
|
||||||
|
|
||||||
@@ -54,8 +58,8 @@ class LoguruHandler(logging.Handler): # pragma: no cover
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
level = record.levelno
|
level = record.levelno
|
||||||
|
|
||||||
frame, depth = sys._getframe(6), 6
|
frame, depth = inspect.currentframe(), 0
|
||||||
while frame and frame.f_code.co_filename == logging.__file__:
|
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
|
||||||
frame = frame.f_back
|
frame = frame.f_back
|
||||||
depth += 1
|
depth += 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
|
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
description: nonebot.matcher 模块
|
description: nonebot.matcher 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||||
from nonebot.internal.matcher import Matcher as Matcher
|
from nonebot.internal.matcher import Matcher as Matcher
|
||||||
from nonebot.internal.matcher import matchers as matchers
|
|
||||||
from nonebot.internal.matcher import current_bot as current_bot
|
|
||||||
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
|
||||||
from nonebot.internal.matcher import current_event as current_event
|
|
||||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||||
|
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||||
|
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_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
|
from nonebot.internal.matcher import matchers as matchers
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
"Matcher": True,
|
"Matcher": True,
|
||||||
|
|||||||
@@ -3,53 +3,62 @@
|
|||||||
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 2
|
sidebar_position: 2
|
||||||
description: nonebot.message 模块
|
description: nonebot.message 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from datetime import datetime
|
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||||
|
|
||||||
|
import anyio
|
||||||
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
|
||||||
from nonebot.log import logger
|
|
||||||
from nonebot.rule import TrieRule
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.matcher import Matcher, matchers
|
|
||||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
|
||||||
from nonebot.exception import (
|
from nonebot.exception import (
|
||||||
NoLogException,
|
|
||||||
StopPropagation,
|
|
||||||
IgnoredException,
|
IgnoredException,
|
||||||
|
NoLogException,
|
||||||
SkippedException,
|
SkippedException,
|
||||||
)
|
StopPropagation,
|
||||||
from nonebot.typing import (
|
|
||||||
T_State,
|
|
||||||
T_DependencyCache,
|
|
||||||
T_RunPreProcessor,
|
|
||||||
T_RunPostProcessor,
|
|
||||||
T_EventPreProcessor,
|
|
||||||
T_EventPostProcessor,
|
|
||||||
)
|
)
|
||||||
from nonebot.internal.params import (
|
from nonebot.internal.params import (
|
||||||
ArgParam,
|
ArgParam,
|
||||||
BotParam,
|
BotParam,
|
||||||
EventParam,
|
|
||||||
StateParam,
|
|
||||||
DependParam,
|
|
||||||
DefaultParam,
|
DefaultParam,
|
||||||
MatcherParam,
|
DependParam,
|
||||||
|
EventParam,
|
||||||
ExceptionParam,
|
ExceptionParam,
|
||||||
|
MatcherParam,
|
||||||
|
StateParam,
|
||||||
|
)
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.matcher import Matcher, matchers
|
||||||
|
from nonebot.rule import TrieRule
|
||||||
|
from nonebot.typing import (
|
||||||
|
T_DependencyCache,
|
||||||
|
T_EventPostProcessor,
|
||||||
|
T_EventPreProcessor,
|
||||||
|
T_RunPostProcessor,
|
||||||
|
T_RunPreProcessor,
|
||||||
|
T_State,
|
||||||
|
)
|
||||||
|
from nonebot.utils import (
|
||||||
|
escape_tag,
|
||||||
|
flatten_exception_group,
|
||||||
|
run_coro_with_catch,
|
||||||
|
run_coro_with_shield,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.adapters import Bot, Event
|
from nonebot.adapters import Bot, Event
|
||||||
|
|
||||||
_event_preprocessors: Set[Dependent[Any]] = set()
|
_event_preprocessors: set[Dependent[Any]] = set()
|
||||||
_event_postprocessors: Set[Dependent[Any]] = set()
|
_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,
|
||||||
@@ -123,6 +132,21 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
|||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_ignored_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||||
|
def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:
|
||||||
|
logger.opt(colors=True).info(msg)
|
||||||
|
|
||||||
|
return _handle
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||||
|
def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||||
|
for exc in flatten_exception_group(exc_group):
|
||||||
|
logger.opt(colors=True, exception=exc).error(msg)
|
||||||
|
|
||||||
|
return _handle
|
||||||
|
|
||||||
|
|
||||||
async def _apply_event_preprocessors(
|
async def _apply_event_preprocessors(
|
||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
@@ -150,10 +174,21 @@ async def _apply_event_preprocessors(
|
|||||||
if show_log:
|
if show_log:
|
||||||
logger.debug("Running PreProcessors...")
|
logger.debug("Running PreProcessors...")
|
||||||
|
|
||||||
try:
|
with catch(
|
||||||
await asyncio.gather(
|
{
|
||||||
*(
|
IgnoredException: _handle_ignored_exception(
|
||||||
run_coro_with_catch(
|
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||||
|
),
|
||||||
|
Exception: _handle_exception(
|
||||||
|
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||||
|
"Event ignored!</bg #f8bbd0></r>"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for proc in _event_preprocessors:
|
||||||
|
tg.start_soon(
|
||||||
|
run_coro_with_catch,
|
||||||
proc(
|
proc(
|
||||||
bot=bot,
|
bot=bot,
|
||||||
event=event,
|
event=event,
|
||||||
@@ -163,22 +198,10 @@ async def _apply_event_preprocessors(
|
|||||||
),
|
),
|
||||||
(SkippedException,),
|
(SkippedException,),
|
||||||
)
|
)
|
||||||
for proc in _event_preprocessors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except IgnoredException:
|
|
||||||
logger.opt(colors=True).info(
|
|
||||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
|
||||||
"Event ignored!</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _apply_event_postprocessors(
|
async def _apply_event_postprocessors(
|
||||||
@@ -205,10 +228,17 @@ async def _apply_event_postprocessors(
|
|||||||
if show_log:
|
if show_log:
|
||||||
logger.debug("Running PostProcessors...")
|
logger.debug("Running PostProcessors...")
|
||||||
|
|
||||||
try:
|
with catch(
|
||||||
await asyncio.gather(
|
{
|
||||||
*(
|
Exception: _handle_exception(
|
||||||
run_coro_with_catch(
|
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for proc in _event_postprocessors:
|
||||||
|
tg.start_soon(
|
||||||
|
run_coro_with_catch,
|
||||||
proc(
|
proc(
|
||||||
bot=bot,
|
bot=bot,
|
||||||
event=event,
|
event=event,
|
||||||
@@ -218,13 +248,6 @@ async def _apply_event_postprocessors(
|
|||||||
),
|
),
|
||||||
(SkippedException,),
|
(SkippedException,),
|
||||||
)
|
)
|
||||||
for proc in _event_postprocessors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _apply_run_preprocessors(
|
async def _apply_run_preprocessors(
|
||||||
@@ -252,35 +275,38 @@ async def _apply_run_preprocessors(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# ensure matcher function can be correctly called
|
# ensure matcher function can be correctly called
|
||||||
with matcher.ensure_context(bot, event):
|
with (
|
||||||
try:
|
matcher.ensure_context(bot, event),
|
||||||
await asyncio.gather(
|
catch(
|
||||||
*(
|
{
|
||||||
run_coro_with_catch(
|
IgnoredException: _handle_ignored_exception(
|
||||||
proc(
|
f"{matcher} running is <b>cancelled</b>"
|
||||||
matcher=matcher,
|
),
|
||||||
bot=bot,
|
Exception: _handle_exception(
|
||||||
event=event,
|
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||||
state=state,
|
"Running cancelled!</bg #f8bbd0></r>"
|
||||||
stack=stack,
|
),
|
||||||
dependency_cache=dependency_cache,
|
}
|
||||||
),
|
),
|
||||||
(SkippedException,),
|
):
|
||||||
)
|
async with anyio.create_task_group() as tg:
|
||||||
for proc in _run_preprocessors
|
for proc in _run_preprocessors:
|
||||||
|
tg.start_soon(
|
||||||
|
run_coro_with_catch,
|
||||||
|
proc(
|
||||||
|
matcher=matcher,
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
state=state,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
),
|
||||||
|
(SkippedException,),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
except IgnoredException:
|
|
||||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
|
||||||
"Running cancelled!</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _apply_run_postprocessors(
|
async def _apply_run_postprocessors(
|
||||||
@@ -304,33 +330,36 @@ async def _apply_run_postprocessors(
|
|||||||
if not _run_postprocessors:
|
if not _run_postprocessors:
|
||||||
return
|
return
|
||||||
|
|
||||||
with matcher.ensure_context(bot, event):
|
with (
|
||||||
try:
|
matcher.ensure_context(bot, event),
|
||||||
await asyncio.gather(
|
catch(
|
||||||
*(
|
{
|
||||||
run_coro_with_catch(
|
Exception: _handle_exception(
|
||||||
proc(
|
"<r><bg #f8bbd0>Error when running RunPostProcessors"
|
||||||
matcher=matcher,
|
"</bg #f8bbd0></r>"
|
||||||
exception=exception,
|
)
|
||||||
bot=bot,
|
}
|
||||||
event=event,
|
),
|
||||||
state=matcher.state,
|
):
|
||||||
stack=stack,
|
async with anyio.create_task_group() as tg:
|
||||||
dependency_cache=dependency_cache,
|
for proc in _run_postprocessors:
|
||||||
),
|
tg.start_soon(
|
||||||
(SkippedException,),
|
run_coro_with_catch,
|
||||||
)
|
proc(
|
||||||
for proc in _run_postprocessors
|
matcher=matcher,
|
||||||
|
exception=exception,
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
state=matcher.state,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
),
|
||||||
|
(SkippedException,),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.opt(colors=True, exception=e).error(
|
|
||||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _check_matcher(
|
async def _check_matcher(
|
||||||
Matcher: Type[Matcher],
|
Matcher: type[Matcher],
|
||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
@@ -381,7 +410,7 @@ async def _check_matcher(
|
|||||||
|
|
||||||
|
|
||||||
async def _run_matcher(
|
async def _run_matcher(
|
||||||
Matcher: Type[Matcher],
|
Matcher: type[Matcher],
|
||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
@@ -423,8 +452,9 @@ async def _run_matcher(
|
|||||||
|
|
||||||
exception = None
|
exception = None
|
||||||
|
|
||||||
|
logger.debug(f"Running {matcher}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
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(
|
||||||
@@ -446,7 +476,7 @@ async def _run_matcher(
|
|||||||
|
|
||||||
|
|
||||||
async def check_and_run_matcher(
|
async def check_and_run_matcher(
|
||||||
Matcher: Type[Matcher],
|
Matcher: type[Matcher],
|
||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
@@ -492,8 +522,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
|
|
||||||
用法:
|
用法:
|
||||||
```python
|
```python
|
||||||
import asyncio
|
driver.task_group.start_soon(handle_event, bot, event)
|
||||||
asyncio.create_task(handle_event(bot, event))
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
show_log = True
|
show_log = True
|
||||||
@@ -505,7 +534,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
if show_log:
|
if show_log:
|
||||||
logger.opt(colors=True).success(log_msg)
|
logger.opt(colors=True).success(log_msg)
|
||||||
|
|
||||||
state: Dict[Any, Any] = {}
|
state: dict[Any, Any] = {}
|
||||||
dependency_cache: T_DependencyCache = {}
|
dependency_cache: T_DependencyCache = {}
|
||||||
|
|
||||||
# create event scope context
|
# create event scope context
|
||||||
@@ -528,6 +557,13 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
break_flag = False
|
break_flag = False
|
||||||
|
|
||||||
|
def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:
|
||||||
|
nonlocal break_flag
|
||||||
|
|
||||||
|
break_flag = True
|
||||||
|
logger.debug("Stop event propagation")
|
||||||
|
|
||||||
# iterate through all priority until stop propagation
|
# iterate through all priority until stop propagation
|
||||||
for priority in sorted(matchers.keys()):
|
for priority in sorted(matchers.keys()):
|
||||||
if break_flag:
|
if break_flag:
|
||||||
@@ -536,23 +572,30 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
if show_log:
|
if show_log:
|
||||||
logger.debug(f"Checking for matchers in priority {priority}...")
|
logger.debug(f"Checking for matchers in priority {priority}...")
|
||||||
|
|
||||||
pending_tasks = [
|
if not (priority_matchers := matchers[priority]):
|
||||||
check_and_run_matcher(
|
continue
|
||||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
|
||||||
)
|
with catch(
|
||||||
for matcher in matchers[priority]
|
{
|
||||||
]
|
StopPropagation: _handle_stop_propagation,
|
||||||
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
Exception: _handle_exception(
|
||||||
for result in results:
|
|
||||||
if not isinstance(result, Exception):
|
|
||||||
continue
|
|
||||||
if isinstance(result, StopPropagation):
|
|
||||||
break_flag = True
|
|
||||||
logger.debug("Stop event propagation")
|
|
||||||
else:
|
|
||||||
logger.opt(colors=True, exception=result).error(
|
|
||||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||||
)
|
),
|
||||||
|
}
|
||||||
|
):
|
||||||
|
async with anyio.create_task_group() as tg:
|
||||||
|
for matcher in priority_matchers:
|
||||||
|
tg.start_soon(
|
||||||
|
run_coro_with_shield,
|
||||||
|
check_and_run_matcher(
|
||||||
|
matcher,
|
||||||
|
bot,
|
||||||
|
event,
|
||||||
|
state.copy(),
|
||||||
|
stack,
|
||||||
|
dependency_cache,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if show_log:
|
if show_log:
|
||||||
logger.debug("Checking for matchers completed")
|
logger.debug("Checking for matchers completed")
|
||||||
|
|||||||
@@ -1,42 +1,49 @@
|
|||||||
"""本模块定义了依赖注入的各类参数。
|
"""本模块定义了依赖注入的各类参数。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 4
|
sidebar_position: 4
|
||||||
description: nonebot.params 模块
|
description: nonebot.params 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Match, Tuple, Union, Optional
|
from re import Match
|
||||||
|
from typing import Any, Callable, Literal, Optional, Union, overload
|
||||||
|
|
||||||
from nonebot.typing import T_State
|
|
||||||
from nonebot.matcher import Matcher
|
|
||||||
from nonebot.internal.params import Arg as Arg
|
|
||||||
from nonebot.internal.params import ArgStr as ArgStr
|
|
||||||
from nonebot.internal.params import Depends as Depends
|
|
||||||
from nonebot.internal.params import ArgParam as ArgParam
|
|
||||||
from nonebot.internal.params import BotParam as BotParam
|
|
||||||
from nonebot.adapters import Event, Message, MessageSegment
|
from nonebot.adapters import Event, Message, MessageSegment
|
||||||
from nonebot.internal.params import EventParam as EventParam
|
|
||||||
from nonebot.internal.params import StateParam as StateParam
|
|
||||||
from nonebot.internal.params import DependParam as DependParam
|
|
||||||
from nonebot.internal.params import ArgPlainText as ArgPlainText
|
|
||||||
from nonebot.internal.params import DefaultParam as DefaultParam
|
|
||||||
from nonebot.internal.params import MatcherParam as MatcherParam
|
|
||||||
from nonebot.internal.params import ExceptionParam as ExceptionParam
|
|
||||||
from nonebot.consts import (
|
from nonebot.consts import (
|
||||||
|
CMD_ARG_KEY,
|
||||||
CMD_KEY,
|
CMD_KEY,
|
||||||
|
CMD_START_KEY,
|
||||||
|
CMD_WHITESPACE_KEY,
|
||||||
|
ENDSWITH_KEY,
|
||||||
|
FULLMATCH_KEY,
|
||||||
|
KEYWORD_KEY,
|
||||||
|
PAUSE_PROMPT_RESULT_KEY,
|
||||||
PREFIX_KEY,
|
PREFIX_KEY,
|
||||||
|
RAW_CMD_KEY,
|
||||||
|
RECEIVE_KEY,
|
||||||
|
REGEX_MATCHED,
|
||||||
|
REJECT_PROMPT_RESULT_KEY,
|
||||||
SHELL_ARGS,
|
SHELL_ARGS,
|
||||||
SHELL_ARGV,
|
SHELL_ARGV,
|
||||||
CMD_ARG_KEY,
|
|
||||||
KEYWORD_KEY,
|
|
||||||
RAW_CMD_KEY,
|
|
||||||
ENDSWITH_KEY,
|
|
||||||
CMD_START_KEY,
|
|
||||||
FULLMATCH_KEY,
|
|
||||||
REGEX_MATCHED,
|
|
||||||
STARTSWITH_KEY,
|
STARTSWITH_KEY,
|
||||||
CMD_WHITESPACE_KEY,
|
|
||||||
)
|
)
|
||||||
|
from nonebot.internal.params import Arg as Arg
|
||||||
|
from nonebot.internal.params import ArgParam as ArgParam
|
||||||
|
from nonebot.internal.params import ArgPlainText as ArgPlainText
|
||||||
|
from nonebot.internal.params import ArgPromptResult as ArgPromptResult
|
||||||
|
from nonebot.internal.params import ArgStr as ArgStr
|
||||||
|
from nonebot.internal.params import BotParam as BotParam
|
||||||
|
from nonebot.internal.params import DefaultParam as DefaultParam
|
||||||
|
from nonebot.internal.params import DependParam as DependParam
|
||||||
|
from nonebot.internal.params import Depends as Depends
|
||||||
|
from nonebot.internal.params import EventParam as EventParam
|
||||||
|
from nonebot.internal.params import ExceptionParam as ExceptionParam
|
||||||
|
from nonebot.internal.params import MatcherParam as MatcherParam
|
||||||
|
from nonebot.internal.params import StateParam as StateParam
|
||||||
|
from nonebot.matcher import Matcher
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
|
||||||
|
|
||||||
async def _event_type(event: Event) -> str:
|
async def _event_type(event: Event) -> str:
|
||||||
@@ -79,7 +86,7 @@ def _command(state: T_State) -> Message:
|
|||||||
return state[PREFIX_KEY][CMD_KEY]
|
return state[PREFIX_KEY][CMD_KEY]
|
||||||
|
|
||||||
|
|
||||||
def Command() -> Tuple[str, ...]:
|
def Command() -> tuple[str, ...]:
|
||||||
"""消息命令元组"""
|
"""消息命令元组"""
|
||||||
return Depends(_command)
|
return Depends(_command)
|
||||||
|
|
||||||
@@ -129,7 +136,7 @@ def ShellCommandArgs() -> Any:
|
|||||||
return Depends(_shell_command_args, use_cache=False)
|
return Depends(_shell_command_args, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
|
def _shell_command_argv(state: T_State) -> list[Union[str, MessageSegment]]:
|
||||||
return state[SHELL_ARGV]
|
return state[SHELL_ARGV]
|
||||||
|
|
||||||
|
|
||||||
@@ -147,29 +154,50 @@ def RegexMatched() -> Match[str]:
|
|||||||
return Depends(_regex_matched, use_cache=False)
|
return Depends(_regex_matched, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _regex_str(state: T_State) -> str:
|
def _regex_str(
|
||||||
return _regex_matched(state).group()
|
groups: tuple[Union[str, int], ...],
|
||||||
|
) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]:
|
||||||
|
def _regex_str_dependency(
|
||||||
|
state: T_State,
|
||||||
|
) -> Union[str, tuple[Union[str, Any], ...], Any]:
|
||||||
|
return _regex_matched(state).group(*groups)
|
||||||
|
|
||||||
|
return _regex_str_dependency
|
||||||
|
|
||||||
|
|
||||||
def RegexStr() -> str:
|
@overload
|
||||||
|
def RegexStr(group: Literal[0] = 0, /) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def RegexStr(group: Union[str, int], /) -> Union[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def RegexStr(
|
||||||
|
group1: Union[str, int], group2: Union[str, int], /, *groups: Union[str, int]
|
||||||
|
) -> tuple[Union[str, Any], ...]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def RegexStr(*groups: Union[str, int]) -> Union[str, tuple[Union[str, Any], ...], Any]:
|
||||||
"""正则匹配结果文本"""
|
"""正则匹配结果文本"""
|
||||||
return Depends(_regex_str, use_cache=False)
|
return Depends(_regex_str(groups), use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _regex_group(state: T_State) -> Tuple[Any, ...]:
|
def _regex_group(state: T_State) -> tuple[Any, ...]:
|
||||||
return _regex_matched(state).groups()
|
return _regex_matched(state).groups()
|
||||||
|
|
||||||
|
|
||||||
def RegexGroup() -> Tuple[Any, ...]:
|
def RegexGroup() -> tuple[Any, ...]:
|
||||||
"""正则匹配结果 group 元组"""
|
"""正则匹配结果 group 元组"""
|
||||||
return Depends(_regex_group, use_cache=False)
|
return Depends(_regex_group, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _regex_dict(state: T_State) -> Dict[str, Any]:
|
def _regex_dict(state: T_State) -> dict[str, Any]:
|
||||||
return _regex_matched(state).groupdict()
|
return _regex_matched(state).groupdict()
|
||||||
|
|
||||||
|
|
||||||
def RegexDict() -> Dict[str, Any]:
|
def RegexDict() -> dict[str, Any]:
|
||||||
"""正则匹配结果 group 字典"""
|
"""正则匹配结果 group 字典"""
|
||||||
return Depends(_regex_dict, use_cache=False)
|
return Depends(_regex_dict, use_cache=False)
|
||||||
|
|
||||||
@@ -228,6 +256,26 @@ def LastReceived(default: Any = None) -> Any:
|
|||||||
return Depends(_last_received, use_cache=False)
|
return Depends(_last_received, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ReceivePromptResult(id: Optional[str] = None) -> Any:
|
||||||
|
"""`receive` prompt 发送结果"""
|
||||||
|
|
||||||
|
def _receive_prompt_result(matcher: "Matcher") -> Any:
|
||||||
|
return matcher.state.get(
|
||||||
|
REJECT_PROMPT_RESULT_KEY.format(key=RECEIVE_KEY.format(id=id))
|
||||||
|
)
|
||||||
|
|
||||||
|
return Depends(_receive_prompt_result, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
|
def PausePromptResult() -> Any:
|
||||||
|
"""`pause` prompt 发送结果"""
|
||||||
|
|
||||||
|
def _pause_prompt_result(matcher: "Matcher") -> Any:
|
||||||
|
return matcher.state.get(PAUSE_PROMPT_RESULT_KEY)
|
||||||
|
|
||||||
|
return Depends(_pause_prompt_result, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
"Arg": True,
|
"Arg": True,
|
||||||
"ArgStr": True,
|
"ArgStr": True,
|
||||||
@@ -241,4 +289,5 @@ __autodoc__ = {
|
|||||||
"DefaultParam": True,
|
"DefaultParam": True,
|
||||||
"MatcherParam": True,
|
"MatcherParam": True,
|
||||||
"ExceptionParam": True,
|
"ExceptionParam": True,
|
||||||
|
"ArgPromptResult": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 6
|
sidebar_position: 6
|
||||||
description: nonebot.permission 模块
|
description: nonebot.permission 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from nonebot.params import EventType
|
|
||||||
from nonebot.adapters import Bot, Event
|
from nonebot.adapters import Bot, Event
|
||||||
from nonebot.internal.permission import USER as USER
|
from nonebot.internal.permission import USER as USER
|
||||||
from nonebot.internal.permission import User as User
|
|
||||||
from nonebot.internal.permission import Permission as Permission
|
from nonebot.internal.permission import Permission as Permission
|
||||||
|
from nonebot.internal.permission import User as User
|
||||||
|
from nonebot.params import EventType
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
class Message:
|
||||||
|
|||||||
@@ -32,19 +32,28 @@
|
|||||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 0
|
sidebar_position: 0
|
||||||
description: nonebot.plugin 模块
|
description: nonebot.plugin 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from contextvars import ContextVar
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from contextvars import ContextVar
|
from typing import Optional, TypeVar
|
||||||
from typing import Set, Dict, List, Tuple, Optional
|
|
||||||
|
|
||||||
_plugins: Dict[str, "Plugin"] = {}
|
from pydantic import BaseModel
|
||||||
_managers: List["PluginManager"] = []
|
|
||||||
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
|
from nonebot import get_driver
|
||||||
"_current_plugin_chain", default=()
|
from nonebot.compat import model_dump, type_validate_python
|
||||||
|
|
||||||
|
C = TypeVar("C", bound=BaseModel)
|
||||||
|
|
||||||
|
_plugins: dict[str, "Plugin"] = {}
|
||||||
|
_managers: list["PluginManager"] = []
|
||||||
|
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
|
||||||
|
"_current_plugin", default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,34 +61,87 @@ def _module_name_to_plugin_name(module_name: str) -> str:
|
|||||||
return module_name.rsplit(".", 1)[-1]
|
return module_name.rsplit(".", 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _controlled_modules() -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
plugin_id: module_name
|
||||||
|
for manager in _managers
|
||||||
|
for plugin_id, module_name in manager.controlled_modules.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _find_parent_plugin_id(
|
||||||
|
module_name: str, controlled_modules: Optional[dict[str, str]] = None
|
||||||
|
) -> Optional[str]:
|
||||||
|
if controlled_modules is None:
|
||||||
|
controlled_modules = _controlled_modules()
|
||||||
|
available = {
|
||||||
|
module_name: plugin_id for plugin_id, module_name in controlled_modules.items()
|
||||||
|
}
|
||||||
|
while "." in module_name:
|
||||||
|
module_name, _ = module_name.rsplit(".", 1)
|
||||||
|
if module_name in available:
|
||||||
|
return available[module_name]
|
||||||
|
|
||||||
|
|
||||||
|
def _module_name_to_plugin_id(
|
||||||
|
module_name: str, controlled_modules: Optional[dict[str, str]] = None
|
||||||
|
) -> str:
|
||||||
|
plugin_name = _module_name_to_plugin_name(module_name)
|
||||||
|
if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):
|
||||||
|
return f"{parent_plugin_id}:{plugin_name}"
|
||||||
|
return plugin_name
|
||||||
|
|
||||||
|
|
||||||
def _new_plugin(
|
def _new_plugin(
|
||||||
module_name: str, module: ModuleType, manager: "PluginManager"
|
module_name: str, module: ModuleType, manager: "PluginManager"
|
||||||
) -> "Plugin":
|
) -> "Plugin":
|
||||||
plugin_name = _module_name_to_plugin_name(module_name)
|
plugin_id = _module_name_to_plugin_id(module_name)
|
||||||
if plugin_name in _plugins:
|
if plugin_id in _plugins:
|
||||||
raise RuntimeError("Plugin already exists! Check your plugin name.")
|
raise RuntimeError(
|
||||||
plugin = Plugin(plugin_name, module, module_name, manager)
|
f"Plugin {plugin_id} already exists! Check your plugin name."
|
||||||
_plugins[plugin_name] = plugin
|
)
|
||||||
|
|
||||||
|
parent_plugin_id = _find_parent_plugin_id(module_name)
|
||||||
|
if parent_plugin_id is not None and parent_plugin_id not in _plugins:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Parent plugin {parent_plugin_id} must "
|
||||||
|
f"be loaded before loading {plugin_id}."
|
||||||
|
)
|
||||||
|
parent_plugin = _plugins[parent_plugin_id] if parent_plugin_id is not None else None
|
||||||
|
|
||||||
|
plugin = Plugin(
|
||||||
|
name=_module_name_to_plugin_name(module_name),
|
||||||
|
module=module,
|
||||||
|
module_name=module_name,
|
||||||
|
manager=manager,
|
||||||
|
parent_plugin=parent_plugin,
|
||||||
|
)
|
||||||
|
if parent_plugin:
|
||||||
|
parent_plugin.sub_plugins.add(plugin)
|
||||||
|
|
||||||
|
_plugins[plugin_id] = plugin
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
def _revert_plugin(plugin: "Plugin") -> None:
|
def _revert_plugin(plugin: "Plugin") -> None:
|
||||||
if plugin.name not in _plugins:
|
if plugin.id_ not in _plugins:
|
||||||
raise RuntimeError("Plugin not found!")
|
raise RuntimeError("Plugin not found!")
|
||||||
del _plugins[plugin.name]
|
del _plugins[plugin.id_]
|
||||||
if parent_plugin := plugin.parent_plugin:
|
if parent_plugin := plugin.parent_plugin:
|
||||||
parent_plugin.sub_plugins.remove(plugin)
|
parent_plugin.sub_plugins.discard(plugin)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin(name: str) -> Optional["Plugin"]:
|
def get_plugin(plugin_id: str) -> Optional["Plugin"]:
|
||||||
"""获取已经导入的某个插件。
|
"""获取已经导入的某个插件。
|
||||||
|
|
||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||||
|
|
||||||
|
如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
plugin_id: 插件标识符,即 {ref}`nonebot.plugin.model.Plugin.id_`。
|
||||||
"""
|
"""
|
||||||
return _plugins.get(name)
|
return _plugins.get(plugin_id)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
||||||
@@ -98,40 +160,45 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
|||||||
module_name, *has_parent = module_name.rsplit(".", 1)
|
module_name, *has_parent = module_name.rsplit(".", 1)
|
||||||
|
|
||||||
|
|
||||||
def get_loaded_plugins() -> Set["Plugin"]:
|
def get_loaded_plugins() -> set["Plugin"]:
|
||||||
"""获取当前已导入的所有插件。"""
|
"""获取当前已导入的所有插件。"""
|
||||||
return set(_plugins.values())
|
return set(_plugins.values())
|
||||||
|
|
||||||
|
|
||||||
def get_available_plugin_names() -> Set[str]:
|
def get_available_plugin_names() -> set[str]:
|
||||||
"""获取当前所有可用的插件名(包含尚未加载的插件)。"""
|
"""获取当前所有可用的插件标识符(包含尚未加载的插件)。"""
|
||||||
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
|
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
|
||||||
|
|
||||||
|
|
||||||
from .on import on as on
|
def get_plugin_config(config: type[C]) -> C:
|
||||||
from .manager import PluginManager
|
"""从全局配置获取当前插件需要的配置项。"""
|
||||||
from .on import on_type as on_type
|
return type_validate_python(config, model_dump(get_driver().config))
|
||||||
from .model import Plugin as Plugin
|
|
||||||
from .load import require as require
|
|
||||||
from .on import on_regex as on_regex
|
from .load import inherit_supported_adapters as inherit_supported_adapters
|
||||||
from .on import on_notice as on_notice
|
|
||||||
from .on import on_command as on_command
|
|
||||||
from .on import on_keyword as on_keyword
|
|
||||||
from .on import on_message as on_message
|
|
||||||
from .on import on_request as on_request
|
|
||||||
from .on import on_endswith as on_endswith
|
|
||||||
from .load import load_plugin as load_plugin
|
|
||||||
from .on import CommandGroup as CommandGroup
|
|
||||||
from .on import MatcherGroup as MatcherGroup
|
|
||||||
from .on import on_fullmatch as on_fullmatch
|
|
||||||
from .on import on_metaevent as on_metaevent
|
|
||||||
from .load import load_plugins as load_plugins
|
|
||||||
from .on import on_startswith as on_startswith
|
|
||||||
from .load import load_from_json as load_from_json
|
|
||||||
from .load import load_from_toml as load_from_toml
|
|
||||||
from .model import PluginMetadata as PluginMetadata
|
|
||||||
from .on import on_shell_command as on_shell_command
|
|
||||||
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 .load import load_builtin_plugins as load_builtin_plugins
|
from .load import load_builtin_plugins as load_builtin_plugins
|
||||||
from .load import inherit_supported_adapters as inherit_supported_adapters
|
from .load import load_from_json as load_from_json
|
||||||
|
from .load import load_from_toml as load_from_toml
|
||||||
|
from .load import load_plugin as load_plugin
|
||||||
|
from .load import load_plugins as load_plugins
|
||||||
|
from .load import require as require
|
||||||
|
from .manager import PluginManager
|
||||||
|
from .model import Plugin as Plugin
|
||||||
|
from .model import PluginMetadata as PluginMetadata
|
||||||
|
from .on import CommandGroup as CommandGroup
|
||||||
|
from .on import MatcherGroup as MatcherGroup
|
||||||
|
from .on import on as on
|
||||||
|
from .on import on_command as on_command
|
||||||
|
from .on import on_endswith as on_endswith
|
||||||
|
from .on import on_fullmatch as on_fullmatch
|
||||||
|
from .on import on_keyword as on_keyword
|
||||||
|
from .on import on_message as on_message
|
||||||
|
from .on import on_metaevent as on_metaevent
|
||||||
|
from .on import on_notice as on_notice
|
||||||
|
from .on import on_regex as on_regex
|
||||||
|
from .on import on_request as on_request
|
||||||
|
from .on import on_shell_command as on_shell_command
|
||||||
|
from .on import on_startswith as on_startswith
|
||||||
|
from .on import on_type as on_type
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
"""本模块定义插件加载接口。
|
"""本模块定义插件加载接口。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
description: nonebot.plugin.load 模块
|
description: nonebot.plugin.load 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Set, Union, Iterable, Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
from nonebot.utils import path_to_module_name
|
from nonebot.utils import path_to_module_name
|
||||||
|
|
||||||
from .model import Plugin
|
from . import _managers, _module_name_to_plugin_id, get_plugin
|
||||||
from .manager import PluginManager
|
from .manager import PluginManager
|
||||||
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
from .model import Plugin
|
||||||
|
|
||||||
try: # pragma: py-gte-311
|
try: # pragma: py-gte-311
|
||||||
import tomllib # pyright: ignore[reportMissingImports]
|
import tomllib # pyright: ignore[reportMissingImports]
|
||||||
except ModuleNotFoundError: # pragma: py-lt-311
|
except ModuleNotFoundError: # pragma: py-lt-311
|
||||||
import tomli as tomllib
|
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||||
@@ -39,7 +42,7 @@ def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
|||||||
return manager.load_plugin(module_path)
|
return manager.load_plugin(module_path)
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
|
def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||||
"""导入文件夹下多个插件,以 `_` 开头的插件不会被导入!
|
"""导入文件夹下多个插件,以 `_` 开头的插件不会被导入!
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -52,7 +55,7 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]:
|
|||||||
|
|
||||||
def load_all_plugins(
|
def load_all_plugins(
|
||||||
module_path: Iterable[str], plugin_dir: Iterable[str]
|
module_path: Iterable[str], plugin_dir: Iterable[str]
|
||||||
) -> Set[Plugin]:
|
) -> set[Plugin]:
|
||||||
"""导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入!
|
"""导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入!
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -64,7 +67,7 @@ def load_all_plugins(
|
|||||||
return manager.load_all_plugins()
|
return manager.load_all_plugins()
|
||||||
|
|
||||||
|
|
||||||
def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
def load_from_json(file_path: str, encoding: str = "utf-8") -> set[Plugin]:
|
||||||
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。
|
"""导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。
|
||||||
以 `_` 开头的插件不会被导入!
|
以 `_` 开头的插件不会被导入!
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
|||||||
return load_all_plugins(set(plugins), set(plugin_dirs))
|
return load_all_plugins(set(plugins), set(plugin_dirs))
|
||||||
|
|
||||||
|
|
||||||
def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
def load_from_toml(file_path: str, encoding: str = "utf-8") -> set[Plugin]:
|
||||||
"""导入指定 toml 文件 `[tool.nonebot]` 中的
|
"""导入指定 toml 文件 `[tool.nonebot]` 中的
|
||||||
`plugins` 以及 `plugin_dirs` 下多个插件。
|
`plugins` 以及 `plugin_dirs` 下多个插件。
|
||||||
以 `_` 开头的插件不会被导入!
|
以 `_` 开头的插件不会被导入!
|
||||||
@@ -139,7 +142,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: str) -> Set[Plugin]:
|
def load_builtin_plugins(*plugins: str) -> set[Plugin]:
|
||||||
"""导入多个 NoneBot 内置插件。
|
"""导入多个 NoneBot 内置插件。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -150,41 +153,45 @@ def load_builtin_plugins(*plugins: str) -> Set[Plugin]:
|
|||||||
|
|
||||||
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
||||||
for manager in reversed(_managers):
|
for manager in reversed(_managers):
|
||||||
if name in manager.plugins or name in manager.searched_plugins:
|
if (
|
||||||
|
name in manager.controlled_modules
|
||||||
|
or name in manager.controlled_modules.values()
|
||||||
|
):
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
def require(name: str) -> ModuleType:
|
def require(name: str) -> ModuleType:
|
||||||
"""获取一个插件的导出内容。
|
"""声明依赖插件。
|
||||||
|
|
||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名,即 {ref}`nonebot.plugin.model.Plugin.name`。
|
name: 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
RuntimeError: 插件无法加载
|
RuntimeError: 插件无法加载
|
||||||
"""
|
"""
|
||||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
if "." in name:
|
||||||
|
# name is a module name
|
||||||
|
plugin = get_plugin(_module_name_to_plugin_id(name))
|
||||||
|
else:
|
||||||
|
# name is a plugin id or simple module name (equals to plugin id)
|
||||||
|
plugin = get_plugin(name)
|
||||||
|
|
||||||
# if plugin not loaded
|
# if plugin not loaded
|
||||||
if not plugin:
|
if plugin is None:
|
||||||
# plugin already declared
|
# plugin already declared, module name / plugin id
|
||||||
if manager := _find_manager_by_name(name):
|
if manager := _find_manager_by_name(name):
|
||||||
plugin = manager.load_plugin(name)
|
plugin = manager.load_plugin(name)
|
||||||
|
|
||||||
# plugin not declared, try to declare and load it
|
# plugin not declared, try to declare and load it
|
||||||
else:
|
else:
|
||||||
# clear current plugin chain, ensure plugin loaded in a new context
|
plugin = load_plugin(name)
|
||||||
_t = _current_plugin_chain.set(())
|
|
||||||
try:
|
if plugin is None:
|
||||||
plugin = load_plugin(name)
|
|
||||||
finally:
|
|
||||||
_current_plugin_chain.reset(_t)
|
|
||||||
if not plugin:
|
|
||||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||||
return plugin.module
|
return plugin.module
|
||||||
|
|
||||||
|
|
||||||
def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
|
def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
|
||||||
"""获取已加载插件的适配器支持状态集合。
|
"""获取已加载插件的适配器支持状态集合。
|
||||||
|
|
||||||
如果传入了多个插件名称,返回值会自动取交集。
|
如果传入了多个插件名称,返回值会自动取交集。
|
||||||
@@ -196,25 +203,28 @@ def inherit_supported_adapters(*names: str) -> Optional[Set[str]]:
|
|||||||
RuntimeError: 插件未加载
|
RuntimeError: 插件未加载
|
||||||
ValueError: 插件缺少元数据
|
ValueError: 插件缺少元数据
|
||||||
"""
|
"""
|
||||||
final_supported: Optional[Set[str]] = None
|
final_supported: Optional[set[str]] = None
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
plugin = get_plugin(_module_name_to_plugin_id(name))
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
raise RuntimeError(f'Plugin "{name}" is not loaded!')
|
raise RuntimeError(
|
||||||
|
f'Plugin "{name}" is not loaded! You should require it first.'
|
||||||
|
)
|
||||||
meta = plugin.metadata
|
meta = plugin.metadata
|
||||||
if meta is None:
|
if meta is None:
|
||||||
raise ValueError(f'Plugin "{name}" has no metadata!')
|
raise ValueError(f'Plugin "{name}" has no metadata!')
|
||||||
support = meta.supported_adapters
|
|
||||||
if support is None:
|
if (raw := meta.supported_adapters) is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
support = {
|
||||||
|
f"nonebot.adapters.{adapter[1:]}" if adapter.startswith("~") else adapter
|
||||||
|
for adapter in raw
|
||||||
|
}
|
||||||
|
|
||||||
final_supported = (
|
final_supported = (
|
||||||
support if final_supported is None else (final_supported & support)
|
support if final_supported is None else (final_supported & support)
|
||||||
)
|
)
|
||||||
|
|
||||||
return final_supported and {
|
return final_supported
|
||||||
f"nonebot.adapters.{adapter_name[1:]}"
|
|
||||||
if adapter_name.startswith("~")
|
|
||||||
else adapter_name
|
|
||||||
for adapter_name in final_supported
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,31 +3,34 @@
|
|||||||
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
|
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 5
|
sidebar_position: 5
|
||||||
description: nonebot.plugin.manager 模块
|
description: nonebot.plugin.manager 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
from collections.abc import Iterable, Sequence
|
||||||
import pkgutil
|
|
||||||
import importlib
|
import importlib
|
||||||
from pathlib import Path
|
|
||||||
from itertools import chain
|
|
||||||
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, Iterable, Optional, Sequence
|
from itertools import chain
|
||||||
|
from pathlib import Path
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.utils import escape_tag, path_to_module_name
|
from nonebot.utils import escape_tag, path_to_module_name
|
||||||
|
|
||||||
from .model import Plugin, PluginMetadata
|
|
||||||
from . import (
|
from . import (
|
||||||
|
_current_plugin,
|
||||||
_managers,
|
_managers,
|
||||||
|
_module_name_to_plugin_id,
|
||||||
_new_plugin,
|
_new_plugin,
|
||||||
_revert_plugin,
|
_revert_plugin,
|
||||||
_current_plugin_chain,
|
|
||||||
_module_name_to_plugin_name,
|
|
||||||
)
|
)
|
||||||
|
from .model import Plugin, PluginMetadata
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
@@ -35,7 +38,7 @@ class PluginManager:
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
plugins: 独立插件模块名集合。
|
plugins: 独立插件模块名集合。
|
||||||
search_path: 插件搜索路径(文件夹)。
|
search_path: 插件搜索路径(文件夹),相对于当前工作目录。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -44,60 +47,78 @@ class PluginManager:
|
|||||||
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._third_party_plugin_names: Dict[str, str] = {}
|
self._third_party_plugin_ids: dict[str, str] = {}
|
||||||
self._searched_plugin_names: Dict[str, Path] = {}
|
self._searched_plugin_ids: dict[str, str] = {}
|
||||||
self.prepare_plugins()
|
self._prepare_plugins()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
|
return f"PluginManager(available_plugins={self.controlled_modules})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def third_party_plugins(self) -> Set[str]:
|
def third_party_plugins(self) -> set[str]:
|
||||||
"""返回所有独立插件名称。"""
|
"""返回所有独立插件标识符。"""
|
||||||
return set(self._third_party_plugin_names.keys())
|
return set(self._third_party_plugin_ids.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def searched_plugins(self) -> Set[str]:
|
def searched_plugins(self) -> set[str]:
|
||||||
"""返回已搜索到的插件名称。"""
|
"""返回已搜索到的插件标识符。"""
|
||||||
return set(self._searched_plugin_names.keys())
|
return set(self._searched_plugin_ids.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_plugins(self) -> Set[str]:
|
def available_plugins(self) -> set[str]:
|
||||||
"""返回当前插件管理器中可用的插件名称。"""
|
"""返回当前插件管理器中可用的插件标识符。"""
|
||||||
return self.third_party_plugins | self.searched_plugins
|
return self.third_party_plugins | self.searched_plugins
|
||||||
|
|
||||||
def _previous_plugins(self) -> Set[str]:
|
@property
|
||||||
_pre_managers: List[PluginManager]
|
def controlled_modules(self) -> dict[str, str]:
|
||||||
|
"""返回当前插件管理器中控制的插件标识符与模块路径映射字典。"""
|
||||||
|
return dict(
|
||||||
|
chain(
|
||||||
|
self._third_party_plugin_ids.items(), self._searched_plugin_ids.items()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _previous_controlled_modules(self) -> dict[str, str]:
|
||||||
|
_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(manager.available_plugins for manager in _pre_managers)
|
plugin_id: module_name
|
||||||
|
for manager in _pre_managers
|
||||||
|
for plugin_id, module_name in manager.controlled_modules.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def prepare_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_plugin_ids = self._previous_controlled_modules()
|
||||||
searched_plugins: Dict[str, Path] = {}
|
|
||||||
third_party_plugins: Dict[str, str] = {}
|
# if self not in global managers, merge self's controlled modules
|
||||||
|
def get_controlled_modules():
|
||||||
|
return (
|
||||||
|
previous_plugin_ids
|
||||||
|
if self in _managers
|
||||||
|
else {**previous_plugin_ids, **self.controlled_modules}
|
||||||
|
)
|
||||||
|
|
||||||
# check third party plugins
|
# check third party plugins
|
||||||
for plugin in self.plugins:
|
for plugin in self.plugins:
|
||||||
name = _module_name_to_plugin_name(plugin)
|
plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules())
|
||||||
if name in third_party_plugins or name in previous_plugins:
|
if (
|
||||||
|
plugin_id in self._third_party_plugin_ids
|
||||||
|
or plugin_id in previous_plugin_ids
|
||||||
|
):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Plugin already exists: {name}! Check your plugin name"
|
f"Plugin already exists: {plugin_id}! Check your plugin name"
|
||||||
)
|
)
|
||||||
third_party_plugins[name] = plugin
|
self._third_party_plugin_ids[plugin_id] = plugin
|
||||||
|
|
||||||
self._third_party_plugin_names = third_party_plugins
|
|
||||||
|
|
||||||
# check plugins in search path
|
# 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):
|
||||||
@@ -105,47 +126,55 @@ class PluginManager:
|
|||||||
if module_info.name.startswith("_"):
|
if module_info.name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (
|
|
||||||
module_info.name in searched_plugins
|
|
||||||
or module_info.name in previous_plugins
|
|
||||||
or module_info.name in third_party_plugins
|
|
||||||
):
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Plugin already exists: {module_info.name}! Check your plugin name"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
module_spec := module_info.module_finder.find_spec(
|
module_spec := module_info.module_finder.find_spec(
|
||||||
module_info.name, None
|
module_info.name, None
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if not (module_path := module_spec.origin):
|
|
||||||
continue
|
|
||||||
searched_plugins[module_info.name] = Path(module_path).resolve()
|
|
||||||
|
|
||||||
self._searched_plugin_names = searched_plugins
|
if not module_spec.origin:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get module name from path, pkgutil does not return the actual module name
|
||||||
|
module_path = Path(module_spec.origin).resolve()
|
||||||
|
module_name = path_to_module_name(module_path)
|
||||||
|
plugin_id = _module_name_to_plugin_id(module_name, get_controlled_modules())
|
||||||
|
|
||||||
|
if (
|
||||||
|
plugin_id in previous_plugin_ids
|
||||||
|
or plugin_id in self._third_party_plugin_ids
|
||||||
|
or plugin_id in self._searched_plugin_ids
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Plugin already exists: {plugin_id}! Check your plugin name"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._searched_plugin_ids[plugin_id] = module_name
|
||||||
|
|
||||||
return self.available_plugins
|
return self.available_plugins
|
||||||
|
|
||||||
def load_plugin(self, name: str) -> Optional[Plugin]:
|
def load_plugin(self, name: str) -> Optional[Plugin]:
|
||||||
"""加载指定插件。
|
"""加载指定插件。
|
||||||
|
|
||||||
对于独立插件,可以使用完整插件模块名或者插件名称。
|
可以使用完整插件模块名或者插件标识符加载。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
name: 插件名称。
|
name: 插件名称或插件标识符。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if name in self.plugins:
|
# load using plugin id
|
||||||
|
if name in self._third_party_plugin_ids:
|
||||||
|
module = importlib.import_module(self._third_party_plugin_ids[name])
|
||||||
|
elif name in self._searched_plugin_ids:
|
||||||
|
module = importlib.import_module(self._searched_plugin_ids[name])
|
||||||
|
# load using module name
|
||||||
|
elif (
|
||||||
|
name in self._third_party_plugin_ids.values()
|
||||||
|
or name in self._searched_plugin_ids.values()
|
||||||
|
):
|
||||||
module = importlib.import_module(name)
|
module = importlib.import_module(name)
|
||||||
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(
|
|
||||||
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")
|
||||||
|
|
||||||
@@ -154,13 +183,13 @@ class PluginManager:
|
|||||||
) is None or not isinstance(plugin, Plugin):
|
) is None or not isinstance(plugin, Plugin):
|
||||||
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."
|
f"Make sure not to import it before loading."
|
||||||
)
|
)
|
||||||
logger.opt(colors=True).success(
|
logger.opt(colors=True).success(
|
||||||
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
|
f'Succeeded to load plugin "<y>{escape_tag(plugin.id_)}</y>"'
|
||||||
+ (
|
+ (
|
||||||
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
||||||
if plugin.module_name != plugin.name
|
if plugin.module_name != plugin.id_
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -170,7 +199,7 @@ class PluginManager:
|
|||||||
f'<r><bg #f8bbd0>Failed to import "{escape_tag(name)}"</bg #f8bbd0></r>'
|
f'<r><bg #f8bbd0>Failed to import "{escape_tag(name)}"</bg #f8bbd0></r>'
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_all_plugins(self) -> Set[Plugin]:
|
def load_all_plugins(self) -> set[Plugin]:
|
||||||
"""加载所有可用插件。"""
|
"""加载所有可用插件。"""
|
||||||
|
|
||||||
return set(
|
return set(
|
||||||
@@ -192,21 +221,16 @@ class PluginFinder(MetaPathFinder):
|
|||||||
module_origin = module_spec.origin
|
module_origin = module_spec.origin
|
||||||
if not module_origin:
|
if not module_origin:
|
||||||
return
|
return
|
||||||
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 fullname in manager.controlled_modules.values():
|
||||||
if (
|
|
||||||
fullname in manager.plugins
|
|
||||||
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
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class PluginLoader(SourceFileLoader):
|
class PluginLoader(SourceFileLoader):
|
||||||
def __init__(self, manager: PluginManager, fullname: str, path) -> None:
|
def __init__(self, manager: PluginManager, fullname: str, path: str) -> None:
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
super().__init__(fullname, path)
|
super().__init__(fullname, path)
|
||||||
@@ -226,17 +250,8 @@ class PluginLoader(SourceFileLoader):
|
|||||||
plugin = _new_plugin(self.name, module, self.manager)
|
plugin = _new_plugin(self.name, module, self.manager)
|
||||||
setattr(module, "__plugin__", plugin)
|
setattr(module, "__plugin__", plugin)
|
||||||
|
|
||||||
# detect parent plugin before entering current plugin context
|
|
||||||
parent_plugins = _current_plugin_chain.get()
|
|
||||||
for pre_plugin in reversed(parent_plugins):
|
|
||||||
# ensure parent plugin is declared before current plugin
|
|
||||||
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
|
|
||||||
plugin.parent_plugin = pre_plugin
|
|
||||||
pre_plugin.sub_plugins.add(plugin)
|
|
||||||
break
|
|
||||||
|
|
||||||
# enter plugin context
|
# enter plugin context
|
||||||
_plugin_token = _current_plugin_chain.set(parent_plugins + (plugin,))
|
_plugin_token = _current_plugin.set(plugin)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
super().exec_module(module)
|
super().exec_module(module)
|
||||||
@@ -245,7 +260,7 @@ class PluginLoader(SourceFileLoader):
|
|||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# leave plugin context
|
# leave plugin context
|
||||||
_current_plugin_chain.reset(_plugin_token)
|
_current_plugin.reset(_plugin_token)
|
||||||
|
|
||||||
# get plugin metadata
|
# get plugin metadata
|
||||||
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
"""本模块定义插件相关信息。
|
"""本模块定义插件相关信息。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
description: nonebot.plugin.model 模块
|
description: nonebot.plugin.model 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from dataclasses import field, dataclass
|
from typing import TYPE_CHECKING, Any, Optional, Type # noqa: UP035
|
||||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -35,19 +37,19 @@ class PluginMetadata:
|
|||||||
"""插件类型,用于商店分类"""
|
"""插件类型,用于商店分类"""
|
||||||
homepage: Optional[str] = None
|
homepage: Optional[str] = None
|
||||||
"""插件主页"""
|
"""插件主页"""
|
||||||
config: Optional[Type[BaseModel]] = None
|
config: Optional[Type[BaseModel]] = None # noqa: UP006
|
||||||
"""插件配置项"""
|
"""插件配置项"""
|
||||||
supported_adapters: Optional[Set[str]] = None
|
supported_adapters: Optional[set[str]] = None
|
||||||
"""插件支持的适配器模块路径
|
"""插件支持的适配器模块路径
|
||||||
|
|
||||||
格式为 `<module>[:<Adapter>]`,`~` 为 `nonebot.adapters.` 的缩写。
|
格式为 `<module>[:<Adapter>]`,`~` 为 `nonebot.adapters.` 的缩写。
|
||||||
|
|
||||||
`None` 表示支持**所有适配器**。
|
`None` 表示支持**所有适配器**。
|
||||||
"""
|
"""
|
||||||
extra: Dict[Any, Any] = field(default_factory=dict)
|
extra: dict[Any, Any] = field(default_factory=dict)
|
||||||
"""插件额外信息,可由插件编写者自由扩展定义"""
|
"""插件额外信息,可由插件编写者自由扩展定义"""
|
||||||
|
|
||||||
def get_supported_adapters(self) -> Optional[Set[Type["Adapter"]]]:
|
def get_supported_adapters(self) -> Optional[set[Type["Adapter"]]]: # noqa: UP006
|
||||||
"""获取当前已安装的插件支持适配器类列表"""
|
"""获取当前已安装的插件支持适配器类列表"""
|
||||||
if self.supported_adapters is None:
|
if self.supported_adapters is None:
|
||||||
return None
|
return None
|
||||||
@@ -66,17 +68,24 @@ class Plugin:
|
|||||||
"""存储插件信息"""
|
"""存储插件信息"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
"""插件索引标识,NoneBot 使用 文件/文件夹 名称作为标识符"""
|
"""插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称"""
|
||||||
module: ModuleType
|
module: ModuleType
|
||||||
"""插件模块对象"""
|
"""插件模块对象"""
|
||||||
module_name: str
|
module_name: str
|
||||||
"""点分割模块路径"""
|
"""点分割模块路径"""
|
||||||
manager: "PluginManager"
|
manager: "PluginManager"
|
||||||
"""导入该插件的插件管理器"""
|
"""导入该插件的插件管理器"""
|
||||||
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
|
metadata: Optional[PluginMetadata] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_(self) -> str:
|
||||||
|
"""插件索引标识"""
|
||||||
|
return (
|
||||||
|
f"{self.parent_plugin.id_}:{self.name}" if self.parent_plugin else self.name
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
"""本模块定义事件响应器便携定义函数。
|
"""本模块定义事件响应器便携定义函数。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 2
|
sidebar_position: 2
|
||||||
description: nonebot.plugin.on 模块
|
description: nonebot.plugin.on 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import inspect
|
|
||||||
import warnings
|
|
||||||
from types import ModuleType
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
import inspect
|
||||||
|
import re
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.permission import Permission
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.matcher import Matcher, MatcherSource
|
from nonebot.matcher import Matcher, MatcherSource
|
||||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
from nonebot.permission import Permission
|
||||||
from nonebot.rule import (
|
from nonebot.rule import (
|
||||||
Rule,
|
|
||||||
ArgumentParser,
|
ArgumentParser,
|
||||||
regex,
|
Rule,
|
||||||
command,
|
command,
|
||||||
is_type,
|
|
||||||
keyword,
|
|
||||||
endswith,
|
endswith,
|
||||||
fullmatch,
|
fullmatch,
|
||||||
startswith,
|
is_type,
|
||||||
|
keyword,
|
||||||
|
regex,
|
||||||
shell_command,
|
shell_command,
|
||||||
|
startswith,
|
||||||
)
|
)
|
||||||
|
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
|
||||||
|
|
||||||
from .model import Plugin
|
|
||||||
from . import get_plugin_by_module_name
|
from . import get_plugin_by_module_name
|
||||||
from .manager import _current_plugin_chain
|
from .manager import _current_plugin
|
||||||
|
from .model import Plugin
|
||||||
|
|
||||||
|
|
||||||
def store_matcher(matcher: Type[Matcher]) -> None:
|
def store_matcher(matcher: type[Matcher]) -> None:
|
||||||
"""存储一个事件响应器到插件。
|
"""存储一个事件响应器到插件。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
matcher: 事件响应器
|
matcher: 事件响应器
|
||||||
"""
|
"""
|
||||||
# only store the matcher defined when plugin loading
|
# only store the matcher defined when plugin loading
|
||||||
if plugin_chain := _current_plugin_chain.get():
|
if plugin := _current_plugin.get():
|
||||||
plugin_chain[-1].matcher.add(matcher)
|
plugin.matcher.add(matcher)
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
||||||
@@ -76,7 +78,7 @@ def get_matcher_module(depth: int = 1) -> Optional[ModuleType]: # pragma: no co
|
|||||||
return (source := get_matcher_source(depth + 1)) and source.module
|
return (source := get_matcher_source(depth + 1)) and source.module
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_source(depth: int = 1) -> Optional[MatcherSource]:
|
def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
|
||||||
"""获取事件响应器定义所在源码信息。
|
"""获取事件响应器定义所在源码信息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -85,20 +87,25 @@ def get_matcher_source(depth: int = 1) -> Optional[MatcherSource]:
|
|||||||
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 = current_frame
|
||||||
|
d = depth + 1
|
||||||
|
while d > 0:
|
||||||
|
frame = frame.f_back
|
||||||
|
if frame is None:
|
||||||
|
raise ValueError("Depth out of range")
|
||||||
|
d -= 1
|
||||||
|
|
||||||
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
||||||
|
|
||||||
plugin: Optional["Plugin"] = None
|
|
||||||
# matcher defined when plugin loading
|
# matcher defined when plugin loading
|
||||||
if plugin_chain := _current_plugin_chain.get():
|
plugin: Optional["Plugin"] = _current_plugin.get()
|
||||||
plugin = plugin_chain[-1]
|
|
||||||
# matcher defined when plugin running
|
# matcher defined when plugin running
|
||||||
elif module_name:
|
if plugin is None and module_name:
|
||||||
plugin = get_plugin_by_module_name(module_name)
|
plugin = get_plugin_by_module_name(module_name)
|
||||||
|
|
||||||
return MatcherSource(
|
return MatcherSource(
|
||||||
plugin_name=plugin and plugin.name,
|
plugin_id=plugin and plugin.id_,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
lineno=frame.f_lineno,
|
lineno=frame.f_lineno,
|
||||||
)
|
)
|
||||||
@@ -109,14 +116,14 @@ def on(
|
|||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
||||||
*,
|
*,
|
||||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
|
||||||
temp: bool = False,
|
temp: bool = False,
|
||||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
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]:
|
||||||
"""注册一个基础事件响应器,可自定义类型。
|
"""注册一个基础事件响应器,可自定义类型。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -146,7 +153,7 @@ def on(
|
|||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
|
|
||||||
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
def on_metaevent(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个元事件响应器。
|
"""注册一个元事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -162,7 +169,7 @@ def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
|||||||
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
|
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
|
||||||
|
|
||||||
|
|
||||||
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
def on_message(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器。
|
"""注册一个消息事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -179,7 +186,7 @@ def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
|||||||
return on("message", *args, **kwargs, _depth=_depth + 1)
|
return on("message", *args, **kwargs, _depth=_depth + 1)
|
||||||
|
|
||||||
|
|
||||||
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
def on_notice(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个通知事件响应器。
|
"""注册一个通知事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -195,7 +202,7 @@ def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
|||||||
return on("notice", *args, **kwargs, _depth=_depth + 1)
|
return on("notice", *args, **kwargs, _depth=_depth + 1)
|
||||||
|
|
||||||
|
|
||||||
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
def on_request(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个请求事件响应器。
|
"""注册一个请求事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -212,12 +219,12 @@ def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
|||||||
|
|
||||||
|
|
||||||
def on_startswith(
|
def on_startswith(
|
||||||
msg: Union[str, Tuple[str, ...]],
|
msg: Union[str, tuple[str, ...]],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -236,12 +243,12 @@ def on_startswith(
|
|||||||
|
|
||||||
|
|
||||||
def on_endswith(
|
def on_endswith(
|
||||||
msg: Union[str, Tuple[str, ...]],
|
msg: Union[str, tuple[str, ...]],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -260,12 +267,12 @@ def on_endswith(
|
|||||||
|
|
||||||
|
|
||||||
def on_fullmatch(
|
def on_fullmatch(
|
||||||
msg: Union[str, Tuple[str, ...]],
|
msg: Union[str, tuple[str, ...]],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -284,11 +291,11 @@ def on_fullmatch(
|
|||||||
|
|
||||||
|
|
||||||
def on_keyword(
|
def on_keyword(
|
||||||
keywords: Set[str],
|
keywords: set[str],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -306,13 +313,13 @@ def on_keyword(
|
|||||||
|
|
||||||
|
|
||||||
def on_command(
|
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,
|
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>`_
|
||||||
@@ -341,13 +348,13 @@ def on_command(
|
|||||||
|
|
||||||
|
|
||||||
def on_shell_command(
|
def on_shell_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,
|
||||||
parser: Optional[ArgumentParser] = None,
|
parser: Optional[ArgumentParser] = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||||
|
|
||||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||||
@@ -383,7 +390,7 @@ def on_regex(
|
|||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||||
|
|
||||||
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
|
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
|
||||||
@@ -404,12 +411,12 @@ def on_regex(
|
|||||||
|
|
||||||
|
|
||||||
def on_type(
|
def on_type(
|
||||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
types: Union[type[Event], tuple[type[Event], ...]],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||||
*,
|
*,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -430,14 +437,14 @@ def on_type(
|
|||||||
class _Group:
|
class _Group:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||||
self.matchers: List[Type[Matcher]] = []
|
self.matchers: list[type[Matcher]] = []
|
||||||
"""组内事件响应器列表"""
|
"""组内事件响应器列表"""
|
||||||
self.base_kwargs: Dict[str, Any] = kwargs
|
self.base_kwargs: dict[str, Any] = kwargs
|
||||||
"""其他传递给 `on` 的参数默认值"""
|
"""其他传递给 `on` 的参数默认值"""
|
||||||
|
|
||||||
def _get_final_kwargs(
|
def _get_final_kwargs(
|
||||||
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
|
self, update: dict[str, Any], *, exclude: Optional[set[str]] = None
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取最终传递给 `on` 的参数
|
"""获取最终传递给 `on` 的参数
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -470,18 +477,18 @@ class CommandGroup(_Group):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, cmd: Union[str, Tuple[str, ...]], prefix_aliases: bool = False, **kwargs
|
self, cmd: Union[str, tuple[str, ...]], prefix_aliases: bool = False, **kwargs
|
||||||
):
|
):
|
||||||
"""命令前缀"""
|
"""命令前缀"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**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)
|
self.base_kwargs.pop("aliases", None)
|
||||||
self.prefix_aliases = prefix_aliases
|
self.prefix_aliases = prefix_aliases
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
||||||
|
|
||||||
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
def command(self, cmd: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
|
||||||
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -509,8 +516,8 @@ class CommandGroup(_Group):
|
|||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
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` 命令。新参数将会覆盖命令组默认值
|
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -544,7 +551,7 @@ class MatcherGroup(_Group):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"MatcherGroup(matchers={len(self.matchers)})"
|
return f"MatcherGroup(matchers={len(self.matchers)})"
|
||||||
|
|
||||||
def on(self, **kwargs) -> Type[Matcher]:
|
def on(self, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个基础事件响应器,可自定义类型。
|
"""注册一个基础事件响应器,可自定义类型。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -562,7 +569,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个元事件响应器。
|
"""注册一个元事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -580,7 +587,7 @@ class MatcherGroup(_Group):
|
|||||||
self.matchers.append(matcher)
|
self.matchers.append(matcher)
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_message(self, **kwargs) -> Type[Matcher]:
|
def on_message(self, **kwargs) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器。
|
"""注册一个消息事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -598,7 +605,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个通知事件响应器。
|
"""注册一个通知事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -616,7 +623,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个请求事件响应器。
|
"""注册一个请求事件响应器。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -635,8 +642,8 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -656,7 +663,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -676,7 +683,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -696,7 +703,7 @@ class MatcherGroup(_Group):
|
|||||||
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]:
|
||||||
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -717,11 +724,11 @@ class MatcherGroup(_Group):
|
|||||||
|
|
||||||
def on_command(
|
def on_command(
|
||||||
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,
|
force_whitespace: Optional[Union[str, bool]] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||||
|
|
||||||
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
|
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
|
||||||
@@ -748,11 +755,11 @@ class MatcherGroup(_Group):
|
|||||||
|
|
||||||
def on_shell_command(
|
def on_shell_command(
|
||||||
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,
|
||||||
parser: Optional[ArgumentParser] = None,
|
parser: Optional[ArgumentParser] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||||
|
|
||||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||||
@@ -780,7 +787,7 @@ class MatcherGroup(_Group):
|
|||||||
|
|
||||||
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>`_
|
||||||
@@ -803,8 +810,8 @@ class MatcherGroup(_Group):
|
|||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_type(
|
def on_type(
|
||||||
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
|
self, types: Union[type[Event], tuple[type[Event]]], **kwargs
|
||||||
) -> Type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
from types import ModuleType
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import re
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.permission import Permission
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.rule import Rule, ArgumentParser
|
|
||||||
from nonebot.matcher import Matcher, MatcherSource
|
from nonebot.matcher import Matcher, MatcherSource
|
||||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
from nonebot.permission import Permission
|
||||||
|
from nonebot.rule import ArgumentParser, Rule
|
||||||
|
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
|
||||||
|
|
||||||
from .model import Plugin
|
from .model import Plugin
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def on(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -32,7 +32,7 @@ def on_metaevent(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -43,7 +43,7 @@ def on_message(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -54,7 +54,7 @@ def on_notice(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -65,7 +65,7 @@ def on_request(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -78,7 +78,7 @@ def on_startswith(
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -91,7 +91,7 @@ def on_endswith(
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -104,7 +104,7 @@ def on_fullmatch(
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -116,7 +116,7 @@ def on_keyword(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -130,7 +130,7 @@ def on_command(
|
|||||||
force_whitespace: str | bool | None = ...,
|
force_whitespace: str | bool | None = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -144,7 +144,7 @@ def on_shell_command(
|
|||||||
parser: ArgumentParser | None = ...,
|
parser: ArgumentParser | None = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -157,7 +157,7 @@ def on_regex(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -169,7 +169,7 @@ def on_type(
|
|||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
*,
|
*,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -194,7 +194,7 @@ class CommandGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -209,7 +209,7 @@ class CommandGroup(_Group):
|
|||||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||||
force_whitespace: str | bool | None = ...,
|
force_whitespace: str | bool | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -224,7 +224,7 @@ class CommandGroup(_Group):
|
|||||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||||
parser: ArgumentParser | None = ...,
|
parser: ArgumentParser | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -239,7 +239,7 @@ class MatcherGroup(_Group):
|
|||||||
type: str = ...,
|
type: str = ...,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -252,7 +252,7 @@ class MatcherGroup(_Group):
|
|||||||
type: str = ...,
|
type: str = ...,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -264,7 +264,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -276,7 +276,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -288,7 +288,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -300,7 +300,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -314,7 +314,7 @@ class MatcherGroup(_Group):
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -328,7 +328,7 @@ class MatcherGroup(_Group):
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -342,7 +342,7 @@ class MatcherGroup(_Group):
|
|||||||
ignorecase: bool = ...,
|
ignorecase: bool = ...,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -355,7 +355,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -370,7 +370,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -385,7 +385,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -399,7 +399,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
@@ -412,7 +412,7 @@ class MatcherGroup(_Group):
|
|||||||
*,
|
*,
|
||||||
rule: Rule | T_RuleChecker | None = ...,
|
rule: Rule | T_RuleChecker | None = ...,
|
||||||
permission: Permission | T_PermissionChecker | None = ...,
|
permission: Permission | T_PermissionChecker | None = ...,
|
||||||
handlers: list[T_Handler | Dependent] | None = ...,
|
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||||
temp: bool = ...,
|
temp: bool = ...,
|
||||||
expire_time: datetime | timedelta | None = ...,
|
expire_time: datetime | timedelta | None = ...,
|
||||||
priority: int = ...,
|
priority: int = ...,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
|
||||||
from nonebot.adapters import Message
|
from nonebot.adapters import Message
|
||||||
from nonebot.params import CommandArg
|
from nonebot.params import CommandArg
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="echo",
|
name="echo",
|
||||||
@@ -19,4 +19,5 @@ echo = on_command("echo", to_me())
|
|||||||
|
|
||||||
@echo.handle()
|
@echo.handle()
|
||||||
async def handle_echo(message: Message = CommandArg()):
|
async def handle_echo(message: Message = CommandArg()):
|
||||||
await echo.send(message=message)
|
if any((not seg.is_text()) or str(seg) for seg in message):
|
||||||
|
await echo.send(message=message)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from typing import Dict, AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
from nonebot.message import IgnoredException, event_preprocessor
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
from nonebot.message import IgnoredException, event_preprocessor
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="唯一会话",
|
name="唯一会话",
|
||||||
@@ -15,7 +15,7 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
supported_adapters=None,
|
supported_adapters=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
_running_matcher: Dict[str, int] = {}
|
_running_matcher: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
|
async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
|
||||||
|
|||||||
130
nonebot/rule.py
130
nonebot/rule.py
@@ -5,31 +5,29 @@
|
|||||||
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 5
|
sidebar_position: 5
|
||||||
description: nonebot.rule 模块
|
description: nonebot.rule 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from argparse import Action, ArgumentError
|
||||||
|
from argparse import ArgumentParser as ArgParser
|
||||||
|
from argparse import Namespace as Namespace
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from gettext import gettext
|
||||||
|
from itertools import chain, product
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
from argparse import Action
|
|
||||||
from gettext import gettext
|
|
||||||
from argparse import ArgumentError
|
|
||||||
from contextvars import ContextVar
|
|
||||||
from itertools import chain, product
|
|
||||||
from argparse import Namespace as Namespace
|
|
||||||
from argparse import ArgumentParser as ArgParser
|
|
||||||
from typing import (
|
from typing import (
|
||||||
IO,
|
IO,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
TypedDict,
|
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
|
Optional,
|
||||||
|
TypedDict,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
@@ -37,33 +35,33 @@ from typing import (
|
|||||||
from pygtrie import CharTrie
|
from pygtrie import CharTrie
|
||||||
|
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
from nonebot.log import logger
|
|
||||||
from nonebot.typing import T_State
|
|
||||||
from nonebot.exception import ParserExit
|
|
||||||
from nonebot.internal.rule import Rule as Rule
|
|
||||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||||
from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
|
|
||||||
from nonebot.consts import (
|
from nonebot.consts import (
|
||||||
|
CMD_ARG_KEY,
|
||||||
CMD_KEY,
|
CMD_KEY,
|
||||||
|
CMD_START_KEY,
|
||||||
|
CMD_WHITESPACE_KEY,
|
||||||
|
ENDSWITH_KEY,
|
||||||
|
FULLMATCH_KEY,
|
||||||
|
KEYWORD_KEY,
|
||||||
PREFIX_KEY,
|
PREFIX_KEY,
|
||||||
|
RAW_CMD_KEY,
|
||||||
|
REGEX_MATCHED,
|
||||||
SHELL_ARGS,
|
SHELL_ARGS,
|
||||||
SHELL_ARGV,
|
SHELL_ARGV,
|
||||||
CMD_ARG_KEY,
|
|
||||||
KEYWORD_KEY,
|
|
||||||
RAW_CMD_KEY,
|
|
||||||
ENDSWITH_KEY,
|
|
||||||
CMD_START_KEY,
|
|
||||||
FULLMATCH_KEY,
|
|
||||||
REGEX_MATCHED,
|
|
||||||
STARTSWITH_KEY,
|
STARTSWITH_KEY,
|
||||||
CMD_WHITESPACE_KEY,
|
|
||||||
)
|
)
|
||||||
|
from nonebot.exception import ParserExit
|
||||||
|
from nonebot.internal.rule import Rule as Rule
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.params import Command, CommandArg, CommandWhitespace, EventToMe
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class CMD_RESULT(TypedDict):
|
class CMD_RESULT(TypedDict):
|
||||||
command: Optional[Tuple[str, ...]]
|
command: Optional[tuple[str, ...]]
|
||||||
raw_command: Optional[str]
|
raw_command: Optional[str]
|
||||||
command_arg: Optional[Message]
|
command_arg: Optional[Message]
|
||||||
command_start: Optional[str]
|
command_start: Optional[str]
|
||||||
@@ -72,7 +70,7 @@ class CMD_RESULT(TypedDict):
|
|||||||
|
|
||||||
class TRIE_VALUE(NamedTuple):
|
class TRIE_VALUE(NamedTuple):
|
||||||
command_start: str
|
command_start: str
|
||||||
command: Tuple[str, ...]
|
command: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
parser_message: ContextVar[str] = ContextVar("parser_message")
|
parser_message: ContextVar[str] = ContextVar("parser_message")
|
||||||
@@ -117,6 +115,11 @@ class TrieRule:
|
|||||||
# check whitespace
|
# check whitespace
|
||||||
arg_str = segment_text[len(pf.key) :]
|
arg_str = segment_text[len(pf.key) :]
|
||||||
arg_str_stripped = arg_str.lstrip()
|
arg_str_stripped = arg_str.lstrip()
|
||||||
|
# check next segment until arg detected or no text remain
|
||||||
|
while not arg_str_stripped and msg and msg[0].is_text():
|
||||||
|
arg_str += str(msg.pop(0))
|
||||||
|
arg_str_stripped = arg_str.lstrip()
|
||||||
|
|
||||||
has_arg = arg_str_stripped or msg
|
has_arg = arg_str_stripped or msg
|
||||||
if (
|
if (
|
||||||
has_arg
|
has_arg
|
||||||
@@ -142,9 +145,9 @@ class StartswithRule:
|
|||||||
ignorecase: 是否忽略大小写
|
ignorecase: 是否忽略大小写
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("msg", "ignorecase")
|
__slots__ = ("ignorecase", "msg")
|
||||||
|
|
||||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.ignorecase = ignorecase
|
self.ignorecase = ignorecase
|
||||||
|
|
||||||
@@ -176,7 +179,7 @@ class StartswithRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def startswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||||
"""匹配消息纯文本开头。
|
"""匹配消息纯文本开头。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -197,9 +200,9 @@ class EndswithRule:
|
|||||||
ignorecase: 是否忽略大小写
|
ignorecase: 是否忽略大小写
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("msg", "ignorecase")
|
__slots__ = ("ignorecase", "msg")
|
||||||
|
|
||||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.ignorecase = ignorecase
|
self.ignorecase = ignorecase
|
||||||
|
|
||||||
@@ -231,7 +234,7 @@ class EndswithRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def endswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||||
"""匹配消息纯文本结尾。
|
"""匹配消息纯文本结尾。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -252,9 +255,9 @@ class FullmatchRule:
|
|||||||
ignorecase: 是否忽略大小写
|
ignorecase: 是否忽略大小写
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("msg", "ignorecase")
|
__slots__ = ("ignorecase", "msg")
|
||||||
|
|
||||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):
|
||||||
self.msg = tuple(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
|
||||||
|
|
||||||
@@ -285,7 +288,7 @@ class FullmatchRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def fullmatch(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||||
"""完全匹配消息。
|
"""完全匹配消息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -356,7 +359,7 @@ class CommandRule:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cmds: List[Tuple[str, ...]],
|
cmds: list[tuple[str, ...]],
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: Optional[Union[str, bool]] = None,
|
||||||
):
|
):
|
||||||
self.cmds = tuple(cmds)
|
self.cmds = tuple(cmds)
|
||||||
@@ -375,7 +378,7 @@ class CommandRule:
|
|||||||
|
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
cmd: Optional[tuple[str, ...]] = Command(),
|
||||||
cmd_arg: Optional[Message] = CommandArg(),
|
cmd_arg: Optional[Message] = CommandArg(),
|
||||||
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -389,7 +392,7 @@ class CommandRule:
|
|||||||
|
|
||||||
|
|
||||||
def command(
|
def command(
|
||||||
*cmds: Union[str, Tuple[str, ...]],
|
*cmds: Union[str, tuple[str, ...]],
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: Optional[Union[str, bool]] = None,
|
||||||
) -> Rule:
|
) -> Rule:
|
||||||
"""匹配消息命令。
|
"""匹配消息命令。
|
||||||
@@ -419,7 +422,7 @@ def command(
|
|||||||
config = get_driver().config
|
config = get_driver().config
|
||||||
command_start = config.command_start
|
command_start = config.command_start
|
||||||
command_sep = config.command_sep
|
command_sep = config.command_sep
|
||||||
commands: List[Tuple[str, ...]] = []
|
commands: list[tuple[str, ...]] = []
|
||||||
for command in cmds:
|
for command in cmds:
|
||||||
if isinstance(command, str):
|
if isinstance(command, str):
|
||||||
command = (command,)
|
command = (command,)
|
||||||
@@ -455,45 +458,38 @@ class ArgumentParser(ArgParser):
|
|||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||||
namespace: None = None,
|
namespace: None = None,
|
||||||
) -> Tuple[Namespace, List[Union[str, MessageSegment]]]:
|
) -> tuple[Namespace, list[Union[str, MessageSegment]]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_known_args(
|
def parse_known_args(
|
||||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||||
) -> Tuple[T, List[Union[str, MessageSegment]]]:
|
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_known_args(
|
def parse_known_args(
|
||||||
self, *, namespace: T
|
self, *, namespace: T
|
||||||
) -> Tuple[T, List[Union[str, MessageSegment]]]:
|
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def parse_known_args(
|
def parse_known_args( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||||
namespace: Optional[T] = None,
|
namespace: Optional[T] = None,
|
||||||
) -> Tuple[Union[Namespace, T], List[Union[str, MessageSegment]]]:
|
) -> tuple[Union[Namespace, T], list[Union[str, MessageSegment]]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||||
namespace: None = None,
|
namespace: None = None,
|
||||||
) -> Namespace:
|
) -> Namespace: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||||
) -> T:
|
) -> T: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_args(self, *, namespace: T) -> T:
|
def parse_args(self, *, namespace: T) -> T: ...
|
||||||
...
|
|
||||||
|
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self,
|
self,
|
||||||
@@ -508,7 +504,7 @@ class ArgumentParser(ArgParser):
|
|||||||
|
|
||||||
def _parse_optional(
|
def _parse_optional(
|
||||||
self, arg_string: Union[str, MessageSegment]
|
self, arg_string: Union[str, MessageSegment]
|
||||||
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
|
) -> Optional[tuple[Optional[Action], str, Optional[str]]]:
|
||||||
return (
|
return (
|
||||||
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||||
)
|
)
|
||||||
@@ -535,7 +531,7 @@ 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 = tuple(cmds)
|
self.cmds = tuple(cmds)
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
@@ -555,7 +551,7 @@ class ShellCommandRule:
|
|||||||
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 not in self.cmds or msg is None:
|
if cmd not in self.cmds or msg is None:
|
||||||
@@ -573,7 +569,7 @@ class ShellCommandRule:
|
|||||||
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
|
except ArgumentError as e:
|
||||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
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
|
||||||
@@ -583,7 +579,7 @@ class ShellCommandRule:
|
|||||||
|
|
||||||
|
|
||||||
def shell_command(
|
def shell_command(
|
||||||
*cmds: Union[str, Tuple[str, ...]], parser: Optional[ArgumentParser] = None
|
*cmds: Union[str, tuple[str, ...]], parser: Optional[ArgumentParser] = None
|
||||||
) -> Rule:
|
) -> Rule:
|
||||||
"""匹配 `shell_like` 形式的消息命令。
|
"""匹配 `shell_like` 形式的消息命令。
|
||||||
|
|
||||||
@@ -631,7 +627,7 @@ def shell_command(
|
|||||||
config = get_driver().config
|
config = get_driver().config
|
||||||
command_start = config.command_start
|
command_start = config.command_start
|
||||||
command_sep = config.command_sep
|
command_sep = config.command_sep
|
||||||
commands: List[Tuple[str, ...]] = []
|
commands: list[tuple[str, ...]] = []
|
||||||
for command in cmds:
|
for command in cmds:
|
||||||
if isinstance(command, str):
|
if isinstance(command, str):
|
||||||
command = (command,)
|
command = (command,)
|
||||||
@@ -658,7 +654,7 @@ class RegexRule:
|
|||||||
flags: 正则表达式标记
|
flags: 正则表达式标记
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("regex", "flags")
|
__slots__ = ("flags", "regex")
|
||||||
|
|
||||||
def __init__(self, regex: str, flags: int = 0):
|
def __init__(self, regex: str, flags: int = 0):
|
||||||
self.regex = regex
|
self.regex = regex
|
||||||
@@ -742,7 +738,7 @@ class IsTypeRule:
|
|||||||
|
|
||||||
__slots__ = ("types",)
|
__slots__ = ("types",)
|
||||||
|
|
||||||
def __init__(self, *types: Type[Event]):
|
def __init__(self, *types: type[Event]):
|
||||||
self.types = types
|
self.types = types
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -758,7 +754,7 @@ class IsTypeRule:
|
|||||||
return isinstance(event, self.types)
|
return isinstance(event, self.types)
|
||||||
|
|
||||||
|
|
||||||
def is_type(*types: Type[Event]) -> Rule:
|
def is_type(*types: type[Event]) -> Rule:
|
||||||
"""匹配事件类型。
|
"""匹配事件类型。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|||||||
@@ -6,33 +6,29 @@
|
|||||||
[`typing`](https://docs.python.org/3/library/typing.html)。
|
[`typing`](https://docs.python.org/3/library/typing.html)。
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 11
|
sidebar_position: 11
|
||||||
description: nonebot.typing 模块
|
description: nonebot.typing 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
|
import typing_extensions as t_ext
|
||||||
|
from typing_extensions import ParamSpec, TypeAlias, get_args, get_origin, override
|
||||||
import warnings
|
import warnings
|
||||||
from typing_extensions import ParamSpec, TypeAlias, override
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
Union,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
|
||||||
Optional,
|
|
||||||
Awaitable,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from asyncio import Task
|
|
||||||
|
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
|
from nonebot.internal.params import DependencyCache
|
||||||
from nonebot.permission import Permission
|
from nonebot.permission import Permission
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
|
|
||||||
T_Wrapped: TypeAlias = Callable[P, T]
|
T_Wrapped: TypeAlias = t.Callable[P, T]
|
||||||
|
|
||||||
|
|
||||||
def overrides(InterfaceClass: object):
|
def overrides(InterfaceClass: object):
|
||||||
@@ -47,14 +43,90 @@ def overrides(InterfaceClass: object):
|
|||||||
return override
|
return override
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
def type_has_args(type_: type[t.Any]) -> bool:
|
||||||
|
"""判断类型是否有参数"""
|
||||||
|
return isinstance(type_, (t._GenericAlias, types.GenericAlias)) # type: ignore
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def type_has_args(type_: type[t.Any]) -> bool:
|
||||||
|
return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType)) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
|
||||||
|
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
|
||||||
|
"""判断是否是 Union 类型"""
|
||||||
|
return origin is t.Union
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
|
||||||
|
return origin is t.Union or origin is types.UnionType
|
||||||
|
|
||||||
|
|
||||||
|
def origin_is_literal(origin: t.Optional[type[t.Any]]) -> bool:
|
||||||
|
"""判断是否是 Literal 类型"""
|
||||||
|
return origin is t.Literal or origin is t_ext.Literal
|
||||||
|
|
||||||
|
|
||||||
|
def _literal_values(type_: type[t.Any]) -> tuple[t.Any, ...]:
|
||||||
|
return get_args(type_)
|
||||||
|
|
||||||
|
|
||||||
|
def all_literal_values(type_: type[t.Any]) -> list[t.Any]:
|
||||||
|
"""获取 Literal 类型包含的所有值"""
|
||||||
|
if not origin_is_literal(get_origin(type_)):
|
||||||
|
return [type_]
|
||||||
|
|
||||||
|
return [x for value in _literal_values(type_) for x in all_literal_values(value)]
|
||||||
|
|
||||||
|
|
||||||
|
def origin_is_annotated(origin: t.Optional[type[t.Any]]) -> bool:
|
||||||
|
"""判断是否是 Annotated 类型"""
|
||||||
|
return origin is t_ext.Annotated
|
||||||
|
|
||||||
|
|
||||||
|
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None]}
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
NONE_TYPES.add(types.NoneType)
|
||||||
|
|
||||||
|
|
||||||
|
def is_none_type(type_: type[t.Any]) -> bool:
|
||||||
|
"""判断是否是 None 类型"""
|
||||||
|
return type_ in NONE_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_forwardref(
|
||||||
|
ref: t.ForwardRef, globalns: dict[str, t.Any], localns: dict[str, t.Any]
|
||||||
|
) -> t.Any:
|
||||||
|
# Python 3.13/3.12.4+ made `recursive_guard` a kwarg,
|
||||||
|
# so name it explicitly to avoid:
|
||||||
|
# TypeError: ForwardRef._evaluate()
|
||||||
|
# missing 1 required keyword-only argument: 'recursive_guard'
|
||||||
|
return ref._evaluate(globalns, localns, recursive_guard=frozenset())
|
||||||
|
|
||||||
|
|
||||||
# state
|
# state
|
||||||
T_State: TypeAlias = Dict[Any, Any]
|
# use annotated flag to avoid ForwardRef recreate generic type (py >= 3.11)
|
||||||
|
class StateFlag:
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "StateFlag()"
|
||||||
|
|
||||||
|
|
||||||
|
_STATE_FLAG = StateFlag()
|
||||||
|
|
||||||
|
T_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG]
|
||||||
"""事件处理状态 State 类型"""
|
"""事件处理状态 State 类型"""
|
||||||
|
|
||||||
_DependentCallable: TypeAlias = Union[Callable[..., T], Callable[..., Awaitable[T]]]
|
_DependentCallable: TypeAlias = t.Union[
|
||||||
|
t.Callable[..., T], t.Callable[..., t.Awaitable[T]]
|
||||||
|
]
|
||||||
|
|
||||||
# driver hooks
|
# driver hooks
|
||||||
T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
|
T_BotConnectionHook: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""Bot 连接建立时钩子函数
|
"""Bot 连接建立时钩子函数
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
@@ -63,7 +135,7 @@ T_BotConnectionHook: TypeAlias = _DependentCallable[Any]
|
|||||||
- BotParam: Bot 对象
|
- BotParam: Bot 对象
|
||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
|
T_BotDisconnectionHook: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""Bot 连接断开时钩子函数
|
"""Bot 连接断开时钩子函数
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
@@ -74,15 +146,17 @@ T_BotDisconnectionHook: TypeAlias = _DependentCallable[Any]
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# api hooks
|
# api hooks
|
||||||
T_CallingAPIHook: TypeAlias = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
T_CallingAPIHook: TypeAlias = t.Callable[
|
||||||
|
["Bot", str, dict[str, t.Any]], t.Awaitable[t.Any]
|
||||||
|
]
|
||||||
"""`bot.call_api` 钩子函数"""
|
"""`bot.call_api` 钩子函数"""
|
||||||
T_CalledAPIHook: TypeAlias = Callable[
|
T_CalledAPIHook: TypeAlias = t.Callable[
|
||||||
["Bot", Optional[Exception], str, Dict[str, Any], Any], Awaitable[Any]
|
["Bot", t.Optional[Exception], str, dict[str, t.Any], t.Any], t.Awaitable[t.Any]
|
||||||
]
|
]
|
||||||
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
||||||
|
|
||||||
# event hooks
|
# event hooks
|
||||||
T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
|
T_EventPreProcessor: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""事件预处理函数 EventPreProcessor 类型
|
"""事件预处理函数 EventPreProcessor 类型
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
@@ -93,8 +167,8 @@ T_EventPreProcessor: TypeAlias = _DependentCallable[Any]
|
|||||||
- StateParam: State 对象
|
- StateParam: State 对象
|
||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
|
T_EventPostProcessor: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""事件预处理函数 EventPostProcessor 类型
|
"""事件后处理函数 EventPostProcessor 类型
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
|
|
||||||
@@ -106,7 +180,7 @@ T_EventPostProcessor: TypeAlias = _DependentCallable[Any]
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# matcher run hooks
|
# matcher run hooks
|
||||||
T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
|
T_RunPreProcessor: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""事件响应器运行前预处理函数 RunPreProcessor 类型
|
"""事件响应器运行前预处理函数 RunPreProcessor 类型
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
@@ -118,7 +192,7 @@ T_RunPreProcessor: TypeAlias = _DependentCallable[Any]
|
|||||||
- MatcherParam: Matcher 对象
|
- MatcherParam: Matcher 对象
|
||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
T_RunPostProcessor: TypeAlias = _DependentCallable[Any]
|
T_RunPostProcessor: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
||||||
|
|
||||||
依赖参数:
|
依赖参数:
|
||||||
@@ -155,7 +229,7 @@ T_PermissionChecker: TypeAlias = _DependentCallable[bool]
|
|||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
T_Handler: TypeAlias = _DependentCallable[Any]
|
T_Handler: TypeAlias = _DependentCallable[t.Any]
|
||||||
"""Handler 处理函数。"""
|
"""Handler 处理函数。"""
|
||||||
T_TypeUpdater: TypeAlias = _DependentCallable[str]
|
T_TypeUpdater: TypeAlias = _DependentCallable[str]
|
||||||
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。
|
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。
|
||||||
@@ -183,5 +257,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
|
|||||||
- MatcherParam: Matcher 对象
|
- MatcherParam: Matcher 对象
|
||||||
- DefaultParam: 带有默认值的参数
|
- DefaultParam: 带有默认值的参数
|
||||||
"""
|
"""
|
||||||
T_DependencyCache: TypeAlias = Dict[_DependentCallable[Any], "Task[Any]"]
|
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"]
|
||||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||||
|
|||||||
195
nonebot/utils.py
195
nonebot/utils.py
@@ -1,45 +1,46 @@
|
|||||||
"""本模块包含了 NoneBot 的一些工具函数
|
"""本模块包含了 NoneBot 的一些工具函数
|
||||||
|
|
||||||
FrontMatter:
|
FrontMatter:
|
||||||
|
mdx:
|
||||||
|
format: md
|
||||||
sidebar_position: 8
|
sidebar_position: 8
|
||||||
description: nonebot.utils 模块
|
description: nonebot.utils 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
from collections import deque
|
||||||
import json
|
from collections.abc import AsyncGenerator, Coroutine, Generator, Mapping, Sequence
|
||||||
import asyncio
|
import contextlib
|
||||||
import inspect
|
from contextlib import AbstractContextManager, asynccontextmanager
|
||||||
import importlib
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from functools import partial, wraps
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from contextvars import copy_context
|
import re
|
||||||
from functools import wraps, partial
|
from typing import Any, Callable, Generic, Optional, TypeVar, Union, overload
|
||||||
from contextlib import asynccontextmanager
|
from typing_extensions import ParamSpec, get_args, get_origin, override
|
||||||
from typing_extensions import ParamSpec, get_args, override, get_origin
|
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Type,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
Generic,
|
|
||||||
TypeVar,
|
|
||||||
Callable,
|
|
||||||
Optional,
|
|
||||||
Coroutine,
|
|
||||||
AsyncGenerator,
|
|
||||||
ContextManager,
|
|
||||||
overload,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic.typing import is_union, is_none_type
|
import anyio
|
||||||
|
import anyio.to_thread
|
||||||
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
|
from nonebot.typing import (
|
||||||
|
all_literal_values,
|
||||||
|
is_none_type,
|
||||||
|
origin_is_literal,
|
||||||
|
origin_is_union,
|
||||||
|
type_has_args,
|
||||||
|
)
|
||||||
|
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
K = TypeVar("K")
|
K = TypeVar("K")
|
||||||
V = TypeVar("V")
|
V = TypeVar("V")
|
||||||
|
E = TypeVar("E", bound=BaseException)
|
||||||
|
|
||||||
|
|
||||||
def escape_tag(s: str) -> str:
|
def escape_tag(s: str) -> str:
|
||||||
@@ -53,8 +54,36 @@ def escape_tag(s: str) -> str:
|
|||||||
return re.sub(r"</?((?:[fb]g\s)?[^<>\s]*)>", r"\\\g<0>", s)
|
return re.sub(r"</?((?:[fb]g\s)?[^<>\s]*)>", r"\\\g<0>", s)
|
||||||
|
|
||||||
|
|
||||||
|
def deep_update(
|
||||||
|
mapping: dict[K, Any], *updating_mappings: dict[K, Any]
|
||||||
|
) -> dict[K, Any]:
|
||||||
|
"""深度更新合并字典"""
|
||||||
|
updated_mapping = mapping.copy()
|
||||||
|
for updating_mapping in updating_mappings:
|
||||||
|
for k, v in updating_mapping.items():
|
||||||
|
if (
|
||||||
|
k in updated_mapping
|
||||||
|
and isinstance(updated_mapping[k], dict)
|
||||||
|
and isinstance(v, dict)
|
||||||
|
):
|
||||||
|
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
||||||
|
else:
|
||||||
|
updated_mapping[k] = v
|
||||||
|
return updated_mapping
|
||||||
|
|
||||||
|
|
||||||
|
def lenient_issubclass(
|
||||||
|
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
|
||||||
|
) -> bool:
|
||||||
|
"""检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。"""
|
||||||
|
try:
|
||||||
|
return isinstance(cls, type) and issubclass(cls, class_or_tuple)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def generic_check_issubclass(
|
def generic_check_issubclass(
|
||||||
cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...]]
|
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||||
|
|
||||||
@@ -62,33 +91,59 @@ def generic_check_issubclass(
|
|||||||
|
|
||||||
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||||
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
||||||
|
- 如果 cls 是 `typing.Literal` 类型,
|
||||||
|
则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。
|
||||||
- 如果 cls 是 `typing.TypeVar` 类型,
|
- 如果 cls 是 `typing.TypeVar` 类型,
|
||||||
则会检查其 `__bound__` 或 `__constraints__`
|
则会检查其 `__bound__` 或 `__constraints__`
|
||||||
是否是 class_or_tuple 中一个类型的子类或 None。
|
是否是 class_or_tuple 中一个类型的子类或 None。
|
||||||
"""
|
"""
|
||||||
try:
|
if not type_has_args(cls):
|
||||||
return issubclass(cls, class_or_tuple)
|
with contextlib.suppress(TypeError):
|
||||||
except TypeError:
|
return issubclass(cls, class_or_tuple)
|
||||||
origin = get_origin(cls)
|
|
||||||
if is_union(origin):
|
origin = get_origin(cls)
|
||||||
|
if origin_is_union(origin):
|
||||||
|
return all(
|
||||||
|
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||||
|
for type_ in get_args(cls)
|
||||||
|
)
|
||||||
|
elif origin_is_literal(origin):
|
||||||
|
return all(
|
||||||
|
is_none_type(value) or isinstance(value, class_or_tuple)
|
||||||
|
for value in all_literal_values(cls)
|
||||||
|
)
|
||||||
|
# ensure generic List, Dict can be checked
|
||||||
|
elif origin:
|
||||||
|
# avoid class check error (typing.Final, typing.ClassVar, etc...)
|
||||||
|
try:
|
||||||
|
return issubclass(origin, class_or_tuple)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
elif isinstance(cls, TypeVar):
|
||||||
|
if cls.__constraints__:
|
||||||
return all(
|
return all(
|
||||||
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||||
for type_ in get_args(cls)
|
for type_ in cls.__constraints__
|
||||||
)
|
)
|
||||||
# ensure generic List, Dict can be checked
|
elif cls.__bound__:
|
||||||
elif origin:
|
return generic_check_issubclass(cls.__bound__, class_or_tuple)
|
||||||
return issubclass(origin, class_or_tuple)
|
return False
|
||||||
elif isinstance(cls, TypeVar):
|
|
||||||
if cls.__constraints__:
|
|
||||||
return all(
|
def type_is_complex(type_: type[Any]) -> bool:
|
||||||
is_none_type(type_)
|
"""检查 type_ 是否是复杂类型"""
|
||||||
or generic_check_issubclass(type_, class_or_tuple)
|
origin = get_origin(type_)
|
||||||
for type_ in cls.__constraints__
|
return _type_is_complex_inner(type_) or _type_is_complex_inner(origin)
|
||||||
)
|
|
||||||
elif cls.__bound__:
|
|
||||||
return generic_check_issubclass(cls.__bound__, class_or_tuple)
|
def _type_is_complex_inner(type_: Optional[type[Any]]) -> bool:
|
||||||
|
if lenient_issubclass(type_, (str, bytes)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
return lenient_issubclass(
|
||||||
|
type_, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque)
|
||||||
|
) or dataclasses.is_dataclass(type_)
|
||||||
|
|
||||||
|
|
||||||
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
||||||
"""检查 call 是否是一个 callable 协程函数"""
|
"""检查 call 是否是一个 callable 协程函数"""
|
||||||
@@ -125,18 +180,16 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
|
|||||||
|
|
||||||
@wraps(call)
|
@wraps(call)
|
||||||
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
loop = asyncio.get_running_loop()
|
return await anyio.to_thread.run_sync(
|
||||||
pfunc = partial(call, *args, **kwargs)
|
partial(call, *args, **kwargs), abandon_on_cancel=True
|
||||||
context = copy_context()
|
)
|
||||||
result = await loop.run_in_executor(None, partial(context.run, pfunc))
|
|
||||||
return result
|
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def run_sync_ctx_manager(
|
async def run_sync_ctx_manager(
|
||||||
cm: ContextManager[T],
|
cm: AbstractContextManager[T],
|
||||||
) -> AsyncGenerator[T, None]:
|
) -> AsyncGenerator[T, None]:
|
||||||
"""一个用于包装 sync context manager 为 async context manager 的执行函数"""
|
"""一个用于包装 sync context manager 为 async context manager 的执行函数"""
|
||||||
try:
|
try:
|
||||||
@@ -152,24 +205,22 @@ async def run_sync_ctx_manager(
|
|||||||
@overload
|
@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: None = None,
|
return_on_err: None = None,
|
||||||
) -> Union[T, None]:
|
) -> Union[T, None]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@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,
|
return_on_err: R,
|
||||||
) -> Union[T, R]:
|
) -> Union[T, R]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
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: Optional[R] = None,
|
return_on_err: Optional[R] = None,
|
||||||
) -> Optional[Union[T, R]]:
|
) -> Optional[Union[T, R]]:
|
||||||
"""运行协程并当遇到指定异常时返回指定值。
|
"""运行协程并当遇到指定异常时返回指定值。
|
||||||
@@ -183,10 +234,34 @@ async def run_coro_with_catch(
|
|||||||
协程的返回值或发生异常时的指定值
|
协程的返回值或发生异常时的指定值
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
with catch({exc: lambda exc_group: None}):
|
||||||
return await coro
|
return await coro
|
||||||
except exc:
|
|
||||||
return return_on_err
|
return return_on_err
|
||||||
|
|
||||||
|
|
||||||
|
async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:
|
||||||
|
"""运行协程并在取消时屏蔽取消异常。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
coro: 要运行的协程
|
||||||
|
|
||||||
|
返回:
|
||||||
|
协程的返回值
|
||||||
|
"""
|
||||||
|
|
||||||
|
with anyio.CancelScope(shield=True):
|
||||||
|
return await coro
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_exception_group(
|
||||||
|
exc_group: BaseExceptionGroup[E],
|
||||||
|
) -> Generator[E, None, None]:
|
||||||
|
for exc in exc_group.exceptions:
|
||||||
|
if isinstance(exc, BaseExceptionGroup):
|
||||||
|
yield from flatten_exception_group(exc)
|
||||||
|
else:
|
||||||
|
yield exc
|
||||||
|
|
||||||
|
|
||||||
def get_name(obj: Any) -> str:
|
def get_name(obj: Any) -> str:
|
||||||
@@ -227,7 +302,7 @@ class classproperty(Generic[T]):
|
|||||||
def __init__(self, func: Callable[[Any], T]) -> None:
|
def __init__(self, func: Callable[[Any], T]) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def __get__(self, instance: Any, owner: Optional[Type[Any]] = None) -> T:
|
def __get__(self, instance: Any, owner: Optional[type[Any]] = None) -> T:
|
||||||
return self.func(type(instance) if owner is None else owner)
|
return self.func(type(instance) if owner is None else owner)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.8+-blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
|
|||||||
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.9"
|
||||||
nonebot2 = "^2.0.0"
|
nonebot2 = "^2.0.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
|||||||
3026
poetry.lock
generated
3026
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "nonebot2"
|
name = "nonebot2"
|
||||||
version = "2.1.1"
|
version = "2.4.1"
|
||||||
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,40 +22,36 @@ include = ["nonebot/py.typed"]
|
|||||||
[tool.poetry.urls]
|
[tool.poetry.urls]
|
||||||
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
|
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
|
||||||
"Changelog" = "https://nonebot.dev/changelog"
|
"Changelog" = "https://nonebot.dev/changelog"
|
||||||
"Funding" = "https://afdian.net/@nonebot"
|
"Funding" = "https://afdian.com/@nonebot"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.9"
|
||||||
yarl = "^1.7.2"
|
yarl = "^1.7.2"
|
||||||
|
anyio = "^4.4.0"
|
||||||
pygtrie = "^2.4.1"
|
pygtrie = "^2.4.1"
|
||||||
|
exceptiongroup = "^1.2.2"
|
||||||
loguru = ">=0.6.0,<1.0.0"
|
loguru = ">=0.6.0,<1.0.0"
|
||||||
|
python-dotenv = ">=0.21.0,<2.0.0"
|
||||||
typing-extensions = ">=4.4.0,<5.0.0"
|
typing-extensions = ">=4.4.0,<5.0.0"
|
||||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
pydantic = ">=1.10.0,<3.0.0,!=2.5.0,!=2.5.1,!=2.10.0,!=2.10.1"
|
||||||
|
|
||||||
websockets = { version = ">=10.0", optional = true }
|
websockets = { version = ">=10.0", optional = true }
|
||||||
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||||
fastapi = { version = ">=0.93.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 }
|
aiohttp = { version = "^3.11.0", extras = ["speedups"], optional = true }
|
||||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
httpx = { version = ">=0.26.0,<1.0.0", extras = ["http2"], optional = true }
|
||||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
||||||
"standard",
|
"standard",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
isort = "^5.10.1"
|
ruff = "^0.8.0"
|
||||||
black = "^23.1.0"
|
|
||||||
nonemoji = "^0.1.2"
|
nonemoji = "^0.1.2"
|
||||||
pre-commit = "^3.0.0"
|
pre-commit = "^4.0.0"
|
||||||
ruff = ">=0.0.272,<1.0.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.test.dependencies]
|
[tool.poetry.group.test.dependencies]
|
||||||
nonebug = "^0.3.0"
|
nonebot-test = { path = "./envs/test/", develop = false }
|
||||||
werkzeug = "^2.3.6"
|
|
||||||
pytest-cov = "^4.0.0"
|
|
||||||
pytest-xdist = "^3.0.2"
|
|
||||||
pytest-asyncio = "^0.21.0"
|
|
||||||
coverage-conditional-plugin = "^0.9.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
[tool.poetry.group.docs.dependencies]
|
||||||
nb-autodoc = "^1.0.0a5"
|
nb-autodoc = "^1.0.0a5"
|
||||||
@@ -69,40 +65,58 @@ fastapi = ["fastapi", "uvicorn"]
|
|||||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "strict"
|
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
|
||||||
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
||||||
|
|
||||||
[tool.black]
|
|
||||||
line-length = 88
|
|
||||||
target-version = ["py38", "py39", "py310", "py311"]
|
|
||||||
include = '\.pyi?$'
|
|
||||||
extend-exclude = '''
|
|
||||||
'''
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
line_length = 88
|
|
||||||
length_sort = true
|
|
||||||
skip_gitignore = true
|
|
||||||
force_sort_within_sections = true
|
|
||||||
src_paths = ["nonebot", "tests"]
|
|
||||||
extra_standard_library = ["typing_extensions"]
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"]
|
|
||||||
ignore = ["E402", "C901", "UP037"]
|
|
||||||
|
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = "py38"
|
target-version = "py39"
|
||||||
|
|
||||||
[tool.ruff.flake8-pytest-style]
|
[tool.ruff.format]
|
||||||
|
line-ending = "lf"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"F", # Pyflakes
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"I", # isort
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"ASYNC", # flake8-async
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"T10", # flake8-debugger
|
||||||
|
"T20", # flake8-print
|
||||||
|
"PYI", # flake8-pyi
|
||||||
|
"PT", # flake8-pytest-style
|
||||||
|
"Q", # flake8-quotes
|
||||||
|
"TID", # flake8-tidy-imports
|
||||||
|
"RUF", # Ruff-specific rules
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E402", # module-import-not-at-top-of-file
|
||||||
|
"UP037", # quoted-annotation
|
||||||
|
"RUF001", # ambiguous-unicode-character-string
|
||||||
|
"RUF002", # ambiguous-unicode-character-docstring
|
||||||
|
"RUF003", # ambiguous-unicode-character-comment
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
force-sort-within-sections = true
|
||||||
|
known-first-party = ["nonebot", "tests/*"]
|
||||||
|
extra-standard-library = ["typing_extensions"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-pytest-style]
|
||||||
fixture-parentheses = false
|
fixture-parentheses = false
|
||||||
mark-parentheses = false
|
mark-parentheses = false
|
||||||
|
|
||||||
|
[tool.ruff.lint.pyupgrade]
|
||||||
|
keep-runtime-typing = true
|
||||||
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
pythonVersion = "3.8"
|
pythonVersion = "3.9"
|
||||||
pythonPlatform = "All"
|
pythonPlatform = "All"
|
||||||
|
defineConstant = { PYDANTIC_V2 = true }
|
||||||
executionEnvironments = [
|
executionEnvironments = [
|
||||||
{ root = "./tests", extraPaths = [
|
{ root = "./tests", extraPaths = [
|
||||||
"./",
|
"./",
|
||||||
@@ -110,7 +124,7 @@ executionEnvironments = [
|
|||||||
{ root = "./" },
|
{ root = "./" },
|
||||||
]
|
]
|
||||||
|
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "standard"
|
||||||
reportShadowedImports = false
|
reportShadowedImports = false
|
||||||
disableBytesTypePromotions = true
|
disableBytesTypePromotions = true
|
||||||
|
|
||||||
|
|||||||
11
scripts/build-api-docs.sh
Executable file
11
scripts/build-api-docs.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# cd to the root of the project
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
poetry run nb-autodoc nonebot \
|
||||||
|
-s nonebot.plugins \
|
||||||
|
-u nonebot.internal \
|
||||||
|
-u nonebot.internal.*
|
||||||
|
cp -r ./build/nonebot/* ./website/docs/api/
|
||||||
|
yarn prettier
|
||||||
7
scripts/run-tests.sh
Executable file
7
scripts/run-tests.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# cd to the root of the tests
|
||||||
|
cd "$(dirname "$0")/../tests"
|
||||||
|
|
||||||
|
# Run the tests
|
||||||
|
pytest -n auto --cov-append --cov-report xml --junitxml=./junit.xml $@
|
||||||
14
scripts/setup-envs.sh
Executable file
14
scripts/setup-envs.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# config poetry to install env in project
|
||||||
|
poetry config virtualenvs.in-project true
|
||||||
|
|
||||||
|
# setup dev environment
|
||||||
|
echo "Setting up dev environment"
|
||||||
|
poetry install --all-extras && poetry run pre-commit install && yarn install
|
||||||
|
|
||||||
|
# setup pydantic v2 test environment
|
||||||
|
for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do
|
||||||
|
echo "Setting up $env environment"
|
||||||
|
(cd $env && poetry install --no-root)
|
||||||
|
done
|
||||||
15
scripts/update-envs.sh
Executable file
15
scripts/update-envs.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# update test env
|
||||||
|
echo "Updating test env..."
|
||||||
|
(cd ./envs/test/ && poetry update --lock)
|
||||||
|
|
||||||
|
# update dev env
|
||||||
|
echo "Updating dev env..."
|
||||||
|
poetry update
|
||||||
|
|
||||||
|
# update other envs
|
||||||
|
for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do
|
||||||
|
echo "Updating $env env..."
|
||||||
|
(cd $env && poetry update)
|
||||||
|
done
|
||||||
@@ -12,7 +12,7 @@ exclude_lines =
|
|||||||
@(abc\.)?abstractmethod
|
@(abc\.)?abstractmethod
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
warnings\.warn
|
warnings\.warn
|
||||||
\.\.\.
|
^\.\.\.$
|
||||||
pass
|
pass
|
||||||
if __name__ == .__main__.:
|
if __name__ == .__main__.:
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ rules =
|
|||||||
"sys_platform != 'win32'": py-win32
|
"sys_platform != 'win32'": py-win32
|
||||||
"sys_platform != 'linux'": py-linux
|
"sys_platform != 'linux'": py-linux
|
||||||
"sys_platform != 'darwin'": py-darwin
|
"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-gte-311
|
||||||
"sys_version_info >= (3, 11)": py-lt-311
|
"sys_version_info >= (3, 11)": py-lt-311
|
||||||
|
"package_version('pydantic') < (2,)": pydantic-v2
|
||||||
|
"package_version('pydantic') >= (2,)": pydantic-v1
|
||||||
|
|||||||
17
tests/.env.example
Normal file
17
tests/.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
SIMPLE=simple
|
||||||
|
COMPLEX='
|
||||||
|
[1, 2, 3]
|
||||||
|
'
|
||||||
|
COMPLEX_NONE
|
||||||
|
COMPLEX_UNION=[1, 2, 3]
|
||||||
|
NESTED={"a": 1}
|
||||||
|
NESTED__B=2
|
||||||
|
NESTED__C__C=3
|
||||||
|
NESTED__COMPLEX=[1, 2, 3]
|
||||||
|
NESTED_INNER__A=1
|
||||||
|
NESTED_INNER__B=2
|
||||||
|
OTHER_SIMPLE=simple
|
||||||
|
OTHER_NESTED={"a": 1}
|
||||||
|
OTHER_NESTED__B=2
|
||||||
|
OTHER_NESTED_INNER__A=1
|
||||||
|
OTHER_NESTED_INNER__B=2
|
||||||
@@ -13,3 +13,4 @@ NESTED_MISSING_DICT__A=1
|
|||||||
NESTED_MISSING_DICT__B__C=2
|
NESTED_MISSING_DICT__B__C=2
|
||||||
NOT_NESTED=some string
|
NOT_NESTED=some string
|
||||||
NOT_NESTED__A=1
|
NOT_NESTED__A=1
|
||||||
|
PLUGIN_CONFIG=1
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
|
from collections.abc import Generator
|
||||||
|
from functools import wraps
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Set, Generator
|
import threading
|
||||||
|
from typing import TYPE_CHECKING, Callable, TypeVar
|
||||||
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
import pytest
|
|
||||||
from nonebug import NONEBOT_INIT_KWARGS
|
from nonebug import NONEBOT_INIT_KWARGS
|
||||||
|
import pytest
|
||||||
from werkzeug.serving import BaseWSGIServer, make_server
|
from werkzeug.serving import BaseWSGIServer, make_server
|
||||||
|
|
||||||
import nonebot
|
|
||||||
from nonebot.config import Env
|
|
||||||
from fake_server import request_handler
|
from fake_server import request_handler
|
||||||
from nonebot.drivers import URL, Driver
|
import nonebot
|
||||||
from nonebot import _resolve_combine_expr
|
from nonebot import _resolve_combine_expr
|
||||||
|
from nonebot.config import Env
|
||||||
|
from nonebot.drivers import URL, Driver
|
||||||
|
|
||||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||||
@@ -19,6 +22,11 @@ os.environ["CONFIG_OVERRIDE"] = "new"
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.plugin import Plugin
|
from nonebot.plugin import Plugin
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config: pytest.Config) -> None:
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||||
@@ -35,14 +43,36 @@ def load_driver(request: pytest.FixtureRequest) -> Driver:
|
|||||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")])
|
||||||
|
def anyio_backend(request: pytest.FixtureRequest):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
def run_once(func: Callable[P, R]) -> Callable[P, R]:
|
||||||
|
result = ...
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
|
nonlocal result
|
||||||
|
if result is not Ellipsis:
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
@run_once
|
||||||
|
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||||
# preload global plugins
|
# preload global plugins
|
||||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]:
|
@run_once
|
||||||
|
def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||||
# preload builtin plugins
|
# preload builtin plugins
|
||||||
return nonebot.load_builtin_plugins("echo", "single_session")
|
return nonebot.load_builtin_plugins("echo", "single_session")
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user