mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
913 Commits
v2.0.0-bet
...
v2.0.0
Author | SHA1 | Date | |
---|---|---|---|
|
8dcfe92f13 | ||
|
f3d5c1f226 | ||
|
8af21f6e76 | ||
|
9bf3dc4274 | ||
|
40d2f975cb | ||
|
784ba287aa | ||
|
3b9cf6cc51 | ||
|
f52abc8314 | ||
|
3199fc454a | ||
|
738f8cae3b | ||
|
9406c117a6 | ||
|
7b0e62c128 | ||
|
5d0d91b87b | ||
|
ed687d8ff6 | ||
|
cc214c320d | ||
|
0897f3b0f7 | ||
|
7986c45b3f | ||
|
8ea0241aa2 | ||
|
fabc2faa4c | ||
|
3216479530 | ||
|
6c66a54223 | ||
|
e760290beb | ||
|
3beefdff72 | ||
|
104f610ea7 | ||
|
2b337e1310 | ||
|
b78455f910 | ||
|
c3d6e20120 | ||
|
b32af0f6ba | ||
|
3469b0dbb7 | ||
|
0fd7396665 | ||
|
c6f41e1975 | ||
|
2cfc20c143 | ||
|
99197f30f6 | ||
|
aa48299d5d | ||
|
dd80191761 | ||
|
34c1c33996 | ||
|
2dcbce9cd7 | ||
|
c4fbd1cac3 | ||
|
575e3fb920 | ||
|
50fd4acccb | ||
|
f9e214de93 | ||
|
f28d354875 | ||
|
648b838a75 | ||
|
157fe31051 | ||
|
170fb94896 | ||
|
9616b4c0ca | ||
|
0c4a040394 | ||
|
8592e77e15 | ||
|
fc8850496e | ||
|
227afbfd8d | ||
|
4672af12fe | ||
|
079996d936 | ||
|
bd9b05b990 | ||
|
f2f3f7ab8e | ||
|
22222e79b6 | ||
|
55164e8ece | ||
|
336822ff5c | ||
|
82e8417b9a | ||
|
9c0ecb441f | ||
|
0c0fabcb89 | ||
|
202f437aea | ||
|
a8b06aa7c7 | ||
|
a5e634319a | ||
|
6d1262f402 | ||
|
a5fd182bd0 | ||
|
771cf8bdcf | ||
|
56304aea8d | ||
|
c6e69ddc17 | ||
|
ae55ec3e1b | ||
|
f72243304f | ||
|
5425180aec | ||
|
a496db4ddf | ||
|
26bad8eb4b | ||
|
f526080611 | ||
|
6c269825c9 | ||
|
17959b7056 | ||
|
17a8ed379a | ||
|
163e5001d3 | ||
|
5d27646ef9 | ||
|
38be147e8a | ||
|
93829aeb80 | ||
|
9098dbae9a | ||
|
9edc51c2a4 | ||
|
f1aa2b1cb2 | ||
|
bbb2cb3a2c | ||
|
7a0a32398b | ||
|
272ed8e85c | ||
|
e308d4cfac | ||
|
0162360cfe | ||
|
4ba4c0bebc | ||
|
4b0b1e69a4 | ||
|
22c0f81054 | ||
|
2494e615fd | ||
|
ab637d217b | ||
|
9d8f16f940 | ||
|
dc2c5e3c80 | ||
|
6cfdbbe597 | ||
|
487867a967 | ||
|
f3a692d294 | ||
|
72b798f7ae | ||
|
50237fb778 | ||
|
fa5b8853af | ||
|
0cd2282640 | ||
|
c2cea75bb7 | ||
|
b832ae742b | ||
|
e98d28f3b4 | ||
|
b2e26cd6bd | ||
|
cfe182f452 | ||
|
729a894a04 | ||
|
7231d493d0 | ||
|
abf1d52168 | ||
|
a5618f163f | ||
|
7d6c512f27 | ||
|
f00c0ae71c | ||
|
93b79ddcb3 | ||
|
6691f6ef70 | ||
|
6173836bdb | ||
|
a2a88c1414 | ||
|
6114867e34 | ||
|
3c5cd6046d | ||
|
7dc3702db5 | ||
|
791f75c13e | ||
|
4cfc8fcb44 | ||
|
57fc04e4aa | ||
|
1e74c4eacf | ||
|
e55052ecfd | ||
|
dc0aea9e3e | ||
|
295b55da44 | ||
|
4b9ae5fd68 | ||
|
cc8b6fa7a2 | ||
|
f28de96ea9 | ||
|
5e225b2898 | ||
|
392502dd68 | ||
|
ba329e5ef2 | ||
|
68b64a6004 | ||
|
0f694aa157 | ||
|
0d7c399094 | ||
|
2f6685ab45 | ||
|
2061887276 | ||
|
2f40024edb | ||
|
9799809ebd | ||
|
2a08bc5a14 | ||
|
ff1ace7a04 | ||
|
96f0daf535 | ||
|
1b2f560ad7 | ||
|
1c033d3a53 | ||
|
99b26fccb0 | ||
|
565aba61dc | ||
|
21eb289411 | ||
|
441e772f48 | ||
|
f360e439e9 | ||
|
98b3affe91 | ||
|
04be5cb8e8 | ||
|
7f925536b5 | ||
|
73dfcc53e1 | ||
|
3e543977c9 | ||
|
97829aa122 | ||
|
e42dd109f9 | ||
|
98a6dfc514 | ||
|
d6fd1b8614 | ||
|
dd57aabfdc | ||
|
44c8b4c29d | ||
|
1bc3a8eb02 | ||
|
7371d3e7bb | ||
|
139eeac6ce | ||
|
d33d2653cf | ||
|
ba3fc6abc4 | ||
|
1d60714054 | ||
|
020705bd4b | ||
|
1495b34e39 | ||
|
e0d11226db | ||
|
8749bc9dc5 | ||
|
716a047aba | ||
|
ed6d436a50 | ||
|
028b51facf | ||
|
8f28124237 | ||
|
f3d7a30c66 | ||
|
8f3e9f87cb | ||
|
36e4c02699 | ||
|
1817102a7c | ||
|
20820e72ad | ||
|
fb4d957025 | ||
|
fe5635db62 | ||
|
30a8230eea | ||
|
908622cf61 | ||
|
8e5ec5c4e7 | ||
|
ca071bfc48 | ||
|
4e22252c3e | ||
|
c0eee74968 | ||
|
1e054a4370 | ||
|
76ccd241fc | ||
|
e984b64fe3 | ||
|
3256cf7fce | ||
|
d9eeb690ac | ||
|
b66f4436bf | ||
|
c11bc7b78f | ||
|
3bbb48dd25 | ||
|
73b92be1e4 | ||
|
e977d79ebd | ||
|
d02896065e | ||
|
f468aa992d | ||
|
3bfbbcf111 | ||
|
e2e8b0a8cd | ||
|
c8c5f17fd1 | ||
|
7f6fc56bd8 | ||
|
40855ade01 | ||
|
d116563958 | ||
|
8f603d3112 | ||
|
998752926f | ||
|
890b2ee22f | ||
|
408292d679 | ||
|
ec4761c3a9 | ||
|
0091a03653 | ||
|
e1a63f980f | ||
|
d982e14793 | ||
|
43933920ed | ||
|
fc03c58c70 | ||
|
283560daa7 | ||
|
efc4f5a0d5 | ||
|
9f707469da | ||
|
6396a7558a | ||
|
a8a76393a5 | ||
|
0d0bc656c8 | ||
|
2a2f7b6dce | ||
|
17c86f7da2 | ||
|
1213e89bf5 | ||
|
ae08568daf | ||
|
8fbc85cf50 | ||
|
315dcb329e | ||
|
438e4f57e3 | ||
|
a346efd684 | ||
|
e3151c5f5e | ||
|
47536e6554 | ||
|
79ac7f024f | ||
|
3709e0ba4f | ||
|
c8ffafc1e8 | ||
|
fedb67d4ae | ||
|
076611166a | ||
|
d5234e44f5 | ||
|
64f78c279a | ||
|
744443ab18 | ||
|
9e5cde490e | ||
|
080c0db64b | ||
|
ec41b5f57f | ||
|
c441ec7080 | ||
|
8bb92309d5 | ||
|
9ab7666b6d | ||
|
add1f1473d | ||
|
cba38c399b | ||
|
18beb63d55 | ||
|
8977be2985 | ||
|
00686380b8 | ||
|
d4da953ad8 | ||
|
e5de8c8053 | ||
|
c2a2f8d420 | ||
|
af2ea7b83a | ||
|
909a335106 | ||
|
3e92fccd4e | ||
|
9afaf3d516 | ||
|
8ca56f24e3 | ||
|
8d09dd97f5 | ||
|
78bbf9e623 | ||
|
05a6af46b9 | ||
|
243ad3f896 | ||
|
709c36bf5f | ||
|
ba808c85d5 | ||
|
c5444799f5 | ||
|
36e99bc3ea | ||
|
f65127e655 | ||
|
600c4f3268 | ||
|
53898dfb51 | ||
|
95e3650c51 | ||
|
9f1b9ce2f3 | ||
|
551963c6c3 | ||
|
d59c999554 | ||
|
8f44df371a | ||
|
7822cabe32 | ||
|
ca0b17b46a | ||
|
d1404f6004 | ||
|
a294f0fbe0 | ||
|
3cd0066715 | ||
|
faaef1a387 | ||
|
ad4b244701 | ||
|
51e7bae8f2 | ||
|
f18b6f609e | ||
|
dfbb32937e | ||
|
cf9788ec99 | ||
|
6b83d03094 | ||
|
5508c1a4ee | ||
|
3462295562 | ||
|
fee16082e0 | ||
|
926b257065 | ||
|
fca2d074e0 | ||
|
1a473f171c | ||
|
97eee5a2f7 | ||
|
8f1fbd9b36 | ||
|
856f0b981f | ||
|
f629fc9309 | ||
|
e617bf2762 | ||
|
072c2a2a41 | ||
|
3a142033a1 | ||
|
2832514f49 | ||
|
e7887056b9 | ||
|
500b59905d | ||
|
4d4074ca24 | ||
|
8391de52d9 | ||
|
dd5f3bdea1 | ||
|
c4bfe3a823 | ||
|
3ec76454e3 | ||
|
0e481d96a6 | ||
|
c2b8bbee5f | ||
|
61ad0733de | ||
|
06fa0fb860 | ||
|
e9b1692124 | ||
|
653902c6a2 | ||
|
db0fcc0ceb | ||
|
f0021af6d4 | ||
|
7e614bb2b7 | ||
|
adba6c1890 | ||
|
6116d394e5 | ||
|
31ea5fa306 | ||
|
b209b77235 | ||
|
81870e0d64 | ||
|
40bccbc585 | ||
|
46c2817bba | ||
|
a5302a1872 | ||
|
6642500c1c | ||
|
f9464171fd | ||
|
34d307b881 | ||
|
8dc36aa630 | ||
|
f324b62eb2 | ||
|
a21b511568 | ||
|
df1c13accd | ||
|
c141b3eae7 | ||
|
a2a5af9b5e | ||
|
86a4f4043e | ||
|
f3aa8c6aa5 | ||
|
be81d094b4 | ||
|
d0f832c4cd | ||
|
317a2b8c9b | ||
|
433c672130 | ||
|
f8c67ebdf6 | ||
|
2a95588421 | ||
|
a5fc40f2dc | ||
|
e4ccb683cc | ||
|
34223d6b37 | ||
|
04a7c3bc13 | ||
|
dd04190ca2 | ||
|
5fd5b2f5b3 | ||
|
e8ad79aaf3 | ||
|
ec8bc0424e | ||
|
6063714093 | ||
|
1a0976e834 | ||
|
74743e6176 | ||
|
1befd9ffc6 | ||
|
c688450690 | ||
|
d9e7986f5c | ||
|
d9bdf38a4e | ||
|
1bd1f15c49 | ||
|
b4083ff9f9 | ||
|
2e766a86c2 | ||
|
93976a4162 | ||
|
4dc92ffc0b | ||
|
5747397790 | ||
|
17fb3f92eb | ||
|
8f79ba1ccd | ||
|
e11298d15b | ||
|
728902bfcf | ||
|
97723a0838 | ||
|
e42111f31f | ||
|
8d4eb7faf8 | ||
|
9b19bf63b2 | ||
|
6685d88b44 | ||
|
694db6c278 | ||
|
1dc02bfe8e | ||
|
045ab60699 | ||
|
dc5ebd7a90 | ||
|
676e729df8 | ||
|
dda6b97ef8 | ||
|
b3d22ea8c4 | ||
|
eac72d2d48 | ||
|
955ada47ed | ||
|
4c31683231 | ||
|
a8f3d83947 | ||
|
87e1866cf4 | ||
|
0d39b788bb | ||
|
635668e6d4 | ||
|
6358d07fbd | ||
|
c03e4161c7 | ||
|
4c0d4065c5 | ||
|
720c736343 | ||
|
6d7435bf36 | ||
|
423e055ecd | ||
|
b952f325d2 | ||
|
3c3331a1ef | ||
|
4679e7d9cb | ||
|
cbcd3987d2 | ||
|
d8cc1bd644 | ||
|
09b0b2084d | ||
|
6bc2870ea5 | ||
|
f14580e688 | ||
|
5ae24313f7 | ||
|
14088c6c51 | ||
|
73126535ef | ||
|
22e27cce5f | ||
|
8abab79cfc | ||
|
6b419dc929 | ||
|
8bf8b4760b | ||
|
678c1e1532 | ||
|
325d28fbd4 | ||
|
6bff6e9ad3 | ||
|
d58c1407b5 | ||
|
e8760b6e4a | ||
|
4c92890265 | ||
|
d2f000bb16 | ||
|
b534b3b03f | ||
|
485d6e94b4 | ||
|
2f5ba409ec | ||
|
e2e9bcc260 | ||
|
6e214efde8 | ||
|
da72d20d0e | ||
|
646ab1002f | ||
|
1b4e5d05ab | ||
|
f323feac1b | ||
|
cb715c132f | ||
|
9e1b439128 | ||
|
f07e9bf699 | ||
|
92e410ce0a | ||
|
e14709c7f0 | ||
|
0533058dc2 | ||
|
896e988f0e | ||
|
b39fe9f1a9 | ||
|
02301f6098 | ||
|
c5a0f0bd6d | ||
|
f59d5f0826 | ||
|
c369475603 | ||
|
18e698f2a0 | ||
|
33bab04a5a | ||
|
3cf31998b9 | ||
|
c6d85ac9b0 | ||
|
27286fdc57 | ||
|
735cf9db6b | ||
|
d457bece3d | ||
|
37b2a100d0 | ||
|
cd4037c4fe | ||
|
13a490de0d | ||
|
cec09397cc | ||
|
a11ac82a91 | ||
|
6410af19ba | ||
|
8af08c6417 | ||
|
801d29f66c | ||
|
c518f768d1 | ||
|
27149108d9 | ||
|
5d1582566a | ||
|
eceef1ebec | ||
|
50aa8c53e0 | ||
|
558f740c13 | ||
|
8bffba7efd | ||
|
ce93ea13e7 | ||
|
174182d62a | ||
|
36f047be7f | ||
|
9d73af0513 | ||
|
ecb0d78011 | ||
|
5920efb6c5 | ||
|
5893fbe57d | ||
|
27557af636 | ||
|
b37c7995cb | ||
|
f46addbb85 | ||
|
6f57a290d7 | ||
|
ae66e45287 | ||
|
03cf7f290a | ||
|
f203aaf4ca | ||
|
9a2edbbeb1 | ||
|
bd9ca99f63 | ||
|
8be262d305 | ||
|
b92d47b362 | ||
|
bdf8cb0d57 | ||
|
0cb65214c6 | ||
|
ccc2c5676a | ||
|
6daec67ebd | ||
|
051851faed | ||
|
8d2fca3e12 | ||
|
76f37c485c | ||
|
0c7af0873f | ||
|
31fa4ec5f4 | ||
|
fda490d252 | ||
|
40e443fd1a | ||
|
4a17e581d2 | ||
|
081d212487 | ||
|
3d6774136f | ||
|
fa934a156a | ||
|
bac5356a90 | ||
|
b289065f71 | ||
|
09cf0f29ba | ||
|
244837dd7c | ||
|
a0bc113912 | ||
|
6f6a296105 | ||
|
a0d316127f | ||
|
f0c0d7788f | ||
|
3f7e2604f1 | ||
|
f43c0087f7 | ||
|
e71d841045 | ||
|
a3af8da331 | ||
|
8bdfdaef91 | ||
|
afd13ed65d | ||
|
d83751d0ca | ||
|
63dd3b8fa7 | ||
|
ae689605a5 | ||
|
bbaba1c955 | ||
|
f1ee54e5c9 | ||
|
6f8e532afe | ||
|
6f68ff61e5 | ||
|
a930fc0997 | ||
|
65da0947fe | ||
|
1b64a54421 | ||
|
d4e1bb7bf3 | ||
|
d737679ccd | ||
|
4cef5512ee | ||
|
1d5d1602f0 | ||
|
87e767fa25 | ||
|
c38437a22f | ||
|
cafb7bedb4 | ||
|
ace053f387 | ||
|
d6e176d03b | ||
|
2fca5b9664 | ||
|
cd93ace0dd | ||
|
b118cb6f22 | ||
|
a69ccb4e6c | ||
|
d5ec31d0a0 | ||
|
62560635b2 | ||
|
c00430c53f | ||
|
1dcda4bd77 | ||
|
b60035f0e6 | ||
|
8551b13eab | ||
|
b448a6e083 | ||
|
956b202087 | ||
|
b95d49cd9c | ||
|
006b9dd816 | ||
|
a9125cd696 | ||
|
ee5dcf0d42 | ||
|
f13c1cc980 | ||
|
16c0a87929 | ||
|
39d1554905 | ||
|
37067229b0 | ||
|
5ca708d3f4 | ||
|
53dded52a7 | ||
|
f8cee790e7 | ||
|
10447ff3c4 | ||
|
f08aec7894 | ||
|
69edb98835 | ||
|
c73ca2b43f | ||
|
848c6c5061 | ||
|
58f82bf881 | ||
|
9b3e670cee | ||
|
f0bebb65b4 | ||
|
bc845c94e2 | ||
|
f4668bf0bc | ||
|
3493d69fcd | ||
|
125bcb943f | ||
|
e65eb3fb18 | ||
|
8045420f97 | ||
|
03a90006e5 | ||
|
adbe341076 | ||
|
2a623b1c81 | ||
|
c91926aea6 | ||
|
63329257de | ||
|
c9dc6e648e | ||
|
78a818547e | ||
|
41eed9d0e9 | ||
|
e32019f15d | ||
|
59c033e2dd | ||
|
49cf1ec5d3 | ||
|
e5ad15d6d6 | ||
|
4cf9790a95 | ||
|
7467e66dab | ||
|
516c1c220c | ||
|
136778ae5b | ||
|
17538f7a66 | ||
|
d1da0be0da | ||
|
e92bc24631 | ||
|
73d1e5dd88 | ||
|
ea83ba78ec | ||
|
712f80a307 | ||
|
5c4ef8fc00 | ||
|
2b0973c9f5 | ||
|
767a8ecb08 | ||
|
b342940b69 | ||
|
7880f07db4 | ||
|
ac702d7eb7 | ||
|
9f3b3b2c32 | ||
|
2d08465426 | ||
|
827d8fbc0e | ||
|
0320be1947 | ||
|
aca65954bd | ||
|
1da9376fc8 | ||
|
909b811f68 | ||
|
ceecf9c692 | ||
|
3de2922773 | ||
|
a5d26b7747 | ||
|
932f50d1fb | ||
|
c69619f142 | ||
|
5a49d1e0e2 | ||
|
4f9f3f449c | ||
|
60733c97be | ||
|
7a1aa0c204 | ||
|
6d1383a10c | ||
|
86006fafdc | ||
|
44ca4f729a | ||
|
cf29209a55 | ||
|
5e78e2bb5d | ||
|
440e15e204 | ||
|
6711a84cab | ||
|
5c2d2141e3 | ||
|
8ec1552fd6 | ||
|
c1dca723ae | ||
|
b6cd0424fa | ||
|
1beee94c1d | ||
|
f2a618f663 | ||
|
c4286f1f39 | ||
|
cd9e30bd52 | ||
|
24ae0dfa15 | ||
|
19a20a3407 | ||
|
36d7b44741 | ||
|
8176cd189c | ||
|
b25de93eb1 | ||
|
79e636ad89 | ||
|
9c9973d8d8 | ||
|
04d4954d50 | ||
|
2c81dc1975 | ||
|
850096ceaa | ||
|
6bb15f6533 | ||
|
92c6a8dd6e | ||
|
723eef10bb | ||
|
06c33ad6ea | ||
|
9826bc29ca | ||
|
f35b5b57b7 | ||
|
433b50c79c | ||
|
89f3496a2a | ||
|
df7e8f6d83 | ||
|
b57f17d447 | ||
|
665de1da3e | ||
|
091232e996 | ||
|
9ee2d94f3c | ||
|
78bdfe65ba | ||
|
5006cf7be6 | ||
|
a818e0056e | ||
|
3efae8bfbc | ||
|
024d97b997 | ||
|
c90ab949d2 | ||
|
e8ffa63b78 | ||
|
6c27ec7357 | ||
|
9bf08593d7 | ||
|
b016a59a38 | ||
|
794395e737 | ||
|
a758e6f06e | ||
|
11feb2c0d0 | ||
|
59a2ed7c2e | ||
|
d83866f03b | ||
|
cb83e76e16 | ||
|
1644615462 | ||
|
89d8abf863 | ||
|
f8cf7c94ae | ||
|
bef494615f | ||
|
6e110e725e | ||
|
85390a14b6 | ||
|
276041e314 | ||
|
2922da7b2f | ||
|
c783ab5e9b | ||
|
139190bff7 | ||
|
1524434444 | ||
|
b6857d59b8 | ||
|
a7b0eb10a0 | ||
|
0eadb44e20 | ||
|
6b43209d37 | ||
|
a50990bef2 | ||
|
f1525c1ecd | ||
|
7df9756205 | ||
|
376a720881 | ||
|
c7377647fa | ||
|
cfbd6f1e4d | ||
|
cecc853f25 | ||
|
fe5f85517e | ||
|
66040a7e44 | ||
|
81d2f017f6 | ||
|
4355025f87 | ||
|
0bc8a39578 | ||
|
7308f57776 | ||
|
7de8912edb | ||
|
42a4edc3ee | ||
|
4a12429a38 | ||
|
092f6d05f5 | ||
|
2c0c05dca1 | ||
|
31bafc832f | ||
|
e1720d8ea0 | ||
|
08be5724b9 | ||
|
828714a4e3 | ||
|
a79eeb73a6 | ||
|
c1aec637d5 | ||
|
12cc08efbd | ||
|
f410af72dc | ||
|
113021cdf4 | ||
|
fcc23f98f8 | ||
|
0b693c419e | ||
|
36e5b81510 | ||
|
71f17bebaa | ||
|
2f45f25d13 | ||
|
17d52446c3 | ||
|
2304aaf22b | ||
|
a70c37de69 | ||
|
1cabc18277 | ||
|
ed1235ed11 | ||
|
9952b4e838 | ||
|
e81390a104 | ||
|
7bc22289b4 | ||
|
bfa4a079bf | ||
|
403850262b | ||
|
ed25e7aa39 | ||
|
7ab480f044 | ||
|
d7d6152094 | ||
|
2954e58c77 | ||
|
f684b96433 | ||
|
4388d1c9e6 | ||
|
876acf3e88 | ||
|
963e73f517 | ||
|
bdc9e44142 | ||
|
4f2efb7304 | ||
|
6ae891124f | ||
|
145bd4d4e1 | ||
|
ab54049909 | ||
|
7aa554f5a2 | ||
|
5566777374 | ||
|
b2594f61de | ||
|
1ad1e0606c | ||
|
583d5060db | ||
|
eaa3dbdfa8 | ||
|
03f378690a | ||
|
512c66ccc0 | ||
|
9d20c7510a | ||
|
29ad8a6686 | ||
|
db534b8824 | ||
|
67b96528af | ||
|
a5929f80f7 | ||
|
9619477a27 | ||
|
8377680fd7 | ||
|
3e3d6f91a5 | ||
|
1d3d886004 | ||
|
ebc5a3cc9e | ||
|
1092767a51 | ||
|
ab227ee64b | ||
|
00b37fb3d2 | ||
|
e6494dc98e | ||
|
c80869f952 | ||
|
2be72eac5e | ||
|
830c4f8c6a | ||
|
138fb458b7 | ||
|
2c93f82ef3 | ||
|
77aa16c2fc | ||
|
945da7151e | ||
|
41ea0df0a5 | ||
|
cec45cf89c | ||
|
2de8c66c70 | ||
|
0bcc4277e5 | ||
|
d3dd93b36c | ||
|
e9bd81d9bb | ||
|
997d4f5042 | ||
|
3bb321c519 | ||
|
fe92d29322 | ||
|
3234871b53 | ||
|
03543f01f2 | ||
|
ba5c0303c7 | ||
|
e56fdd04ad | ||
|
9f10bb70db | ||
|
71aad502d1 | ||
|
ab85b8651e | ||
|
4fe8929441 | ||
|
5c303710f6 | ||
|
68d2ada94b | ||
|
75470fe157 | ||
|
47b3fc516a | ||
|
84c24b014f | ||
|
756cde6525 | ||
|
57ef19af94 | ||
|
5927b517e2 | ||
|
132205bfcc | ||
|
b31dfa9ab0 | ||
|
b249802c38 | ||
|
9df705aaa7 | ||
|
a0df535f0c | ||
|
31022a653d | ||
|
ba77443dde | ||
|
984f743097 | ||
|
638a9c94af | ||
|
92f1d5a4d7 | ||
|
248af2ae1a | ||
|
4c37be7312 | ||
|
2cb8eafa81 | ||
|
05bff5ec17 | ||
|
13245cb58f | ||
|
37bc7326b5 | ||
|
f6d189d8c5 | ||
|
600ef7031f | ||
|
7bedf7c8d0 | ||
|
f62ee5893c | ||
|
71234e9a68 | ||
|
3bbca0fa70 | ||
|
20f144ba93 | ||
|
4c8bc9f0cb | ||
|
064509f26b | ||
|
8c42490a7e | ||
|
179d7105c9 | ||
|
1c14e638c8 | ||
|
c6eef06b55 | ||
|
beef564a22 | ||
|
672f2ceecc | ||
|
28142402d7 | ||
|
b886329fb8 | ||
|
a0b186aff3 | ||
|
595c64e760 | ||
|
5114749073 | ||
|
af2d7b5797 | ||
|
56943c0908 | ||
|
45478deb95 | ||
|
d281ec5bf9 | ||
|
291a7cbb8b | ||
|
41259546bd | ||
|
f96038241f | ||
|
b051320d78 | ||
|
d12efac9f4 | ||
|
f87a38a30a | ||
|
bf016b3f69 | ||
|
373f5255f1 | ||
|
5a35015195 | ||
|
d3a2f1dc08 | ||
|
f1aec4eb10 | ||
|
cd30be21ba | ||
|
f150a9ee89 | ||
|
e68281f60f | ||
|
32be64485a | ||
|
c76f492305 | ||
|
29b0351644 | ||
|
ef3350fd9c | ||
|
459699de5c | ||
|
31c3eb8fd6 | ||
|
1cfdee2645 | ||
|
4e76518a58 | ||
|
b53e029df1 | ||
|
c1faf68806 | ||
|
fe64b904ff | ||
|
a621ade449 | ||
|
3fda978064 | ||
|
60ab93164c | ||
|
2b22d5abda | ||
|
e3a4834383 | ||
|
21087036af | ||
|
94d336ef4d | ||
|
07707213a5 | ||
|
7579878fb4 | ||
|
6599b6420e | ||
|
135c6e8168 | ||
|
743e7363ea | ||
|
a101428c81 | ||
|
0d2b1f693e | ||
|
e5a53dfd5c | ||
|
915c2b3e43 | ||
|
767b6a9913 | ||
|
3f8af04803 | ||
|
00af815b8a | ||
|
24df594b97 | ||
|
d6567f9288 | ||
|
4eb158245e | ||
|
ef35266d3e | ||
|
1d1beb100a | ||
|
06ab6093b7 | ||
|
c1ce7fb940 | ||
|
6e03ddbf12 | ||
|
40c8787828 | ||
|
be459e0bbb | ||
|
1056828f90 | ||
|
ef18e8943d | ||
|
92ff1df419 | ||
|
be5ac88a18 | ||
|
fac647370a | ||
|
05a3891903 | ||
|
4deae8f00c | ||
|
0f70e975b0 | ||
|
982680be91 | ||
|
96b0a863e6 | ||
|
d64bb37c6d | ||
|
51d7f1783d | ||
|
64c18379c9 | ||
|
2735f0cba9 | ||
|
660dbaf3b8 | ||
|
898c29d7ee | ||
|
cdc507bab9 | ||
|
f32bcdc1fc | ||
|
013602da21 | ||
|
4974c596ec | ||
|
0620bec51f | ||
|
8870e6a26e | ||
|
0e3ed0e7ab | ||
|
6cc3b68447 | ||
|
549a37b172 | ||
|
16394ad68b | ||
|
57e580c255 | ||
|
6c23d89494 | ||
|
675e70f579 | ||
|
7a098b96f8 | ||
|
c9794bf91d | ||
|
1766d4da69 | ||
|
6583bc8c61 | ||
|
179f16346a | ||
|
badb0c9ff4 | ||
|
ee0ea85e40 |
@@ -1,15 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux/.devcontainer/base.Dockerfile
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
|
||||
|
||||
# ** [Optional] Uncomment this section to install additional packages. **
|
||||
# USER root
|
||||
#
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
USER codespace
|
||||
|
||||
# [Required] Poetry
|
||||
RUN curl -sSL https://install.python-poetry.org | python - -y
|
||||
RUN poetry config virtualenvs.in-project true
|
@@ -1,32 +1,13 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux
|
||||
{
|
||||
"name": "GitHub Codespaces (Default)",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
"name": "Default Linux Universal",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
@@ -50,7 +31,6 @@
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true
|
||||
},
|
||||
@@ -59,10 +39,7 @@
|
||||
"**/__pycache__": true
|
||||
}
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
@@ -72,27 +49,5 @@
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"remoteUser": "codespace",
|
||||
|
||||
"overrideCommand": false,
|
||||
|
||||
"mounts": [
|
||||
"source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
|
||||
],
|
||||
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// "oryx build" will automatically install your dependencies and attempt to build your project
|
||||
"postCreateCommand": "poetry install && poetry run pre-commit install && yarn install"
|
||||
}
|
||||
}
|
||||
|
57
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/adapter_publish.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: 发布适配器
|
||||
title: "Adapter: {name}"
|
||||
description: 发布适配器到 NoneBot 官方商店
|
||||
labels: ["Adapter"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: 适配器名称
|
||||
description: 适配器名称
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: 适配器描述
|
||||
description: 适配器描述
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: pypi
|
||||
attributes:
|
||||
label: PyPI 项目名
|
||||
description: PyPI 项目名
|
||||
placeholder: e.g. nonebot-adapter-xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: module
|
||||
attributes:
|
||||
label: 适配器 import 包名
|
||||
description: 适配器 import 包名
|
||||
placeholder: e.g. nonebot_adapter_xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: 适配器项目仓库/主页链接
|
||||
description: 适配器项目仓库/主页链接
|
||||
placeholder: e.g. https://github.com/xxx/xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tags
|
||||
attributes:
|
||||
label: 标签
|
||||
description: 标签
|
||||
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
|
||||
value: "[]"
|
||||
validations:
|
||||
required: true
|
37
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bot_publish.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: 发布机器人
|
||||
title: "Bot: {name}"
|
||||
description: 发布机器人到 NoneBot 官方商店
|
||||
labels: ["Bot"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: 机器人名称
|
||||
description: 机器人名称
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: 机器人描述
|
||||
description: 机器人描述
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: 机器人项目仓库/主页链接
|
||||
description: 机器人项目仓库/主页链接
|
||||
placeholder: e.g. https://github.com/xxx/xxx
|
||||
|
||||
- type: input
|
||||
id: tags
|
||||
attributes:
|
||||
label: 标签
|
||||
description: 标签
|
||||
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
|
||||
value: "[]"
|
||||
validations:
|
||||
required: true
|
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
title: 'Bug: Something went wrong'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述问题:**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**如何复现?**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**期望的结果**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**环境信息:**
|
||||
|
||||
- OS: [e.g. Linux]
|
||||
- Python Version: [e.g. 3.8]
|
||||
- Nonebot Version: [e.g. 2.0.0]
|
||||
|
||||
**协议端信息:**
|
||||
|
||||
- 协议端: [e.g. go-cqhttp]
|
||||
- 协议端版本: [e.g. 1.0.0]
|
||||
|
||||
**截图或日志**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Bug 反馈
|
||||
title: "Bug: 出现异常"
|
||||
description: 提交 Bug 反馈以帮助我们改进代码
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: env-os
|
||||
attributes:
|
||||
label: 操作系统
|
||||
description: 选择运行 NoneBot 的系统
|
||||
options:
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: env-python-ver
|
||||
attributes:
|
||||
label: Python 版本
|
||||
description: 填写运行 NoneBot 的 Python 版本
|
||||
placeholder: e.g. 3.11.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: env-nb-ver
|
||||
attributes:
|
||||
label: NoneBot 版本
|
||||
description: 填写 NoneBot 版本
|
||||
placeholder: e.g. 2.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: env-adapter
|
||||
attributes:
|
||||
label: 适配器
|
||||
description: 填写使用的适配器以及版本
|
||||
placeholder: e.g. OneBot v11 2.2.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: env-protocol
|
||||
attributes:
|
||||
label: 协议端
|
||||
description: 填写连接 NoneBot 的协议端及版本
|
||||
placeholder: e.g. go-cqhttp 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: describe
|
||||
attributes:
|
||||
label: 描述问题
|
||||
description: 清晰简洁地说明问题是什么
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供能复现此问题的详细操作步骤
|
||||
placeholder: |
|
||||
1. 首先……
|
||||
2. 然后……
|
||||
3. 发生……
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望的结果
|
||||
description: 清晰简洁地描述你期望发生的事情
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 截图或日志
|
||||
description: 提供有助于诊断问题的任何日志和截图
|
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
- name: NoneBot 论坛
|
||||
url: https://discussions.nonebot.dev/
|
||||
about: Ask questions about nonebot
|
||||
- name: Plugin Publish
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your plugin to nonebot homepage and nb-cli
|
||||
- name: Adapter Publish
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your adapter to nonebot homepage and nb-cli
|
||||
- name: Bot Publish
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your bot to nonebot homepage and nb-cli
|
||||
about: 前往 NoneBot 论坛提问
|
||||
|
17
.github/ISSUE_TEMPLATE/document.md
vendored
17
.github/ISSUE_TEMPLATE/document.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Document improvement
|
||||
about: Feedback on documentation, including errors and ideas
|
||||
title: 'Docs: some description'
|
||||
labels: documentation
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述问题或主题:**
|
||||
|
||||
|
||||
**需做出的修改:**
|
||||
|
||||
* [ ] 一些修改
|
||||
* [ ] 一些修改
|
||||
* [ ] 一些修改
|
18
.github/ISSUE_TEMPLATE/document.yml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/document.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: 文档改进
|
||||
title: "Docs: 描述"
|
||||
description: 文档错误及改进意见反馈
|
||||
labels: ["documentation"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 描述问题或主题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: improve
|
||||
attributes:
|
||||
label: 需做出的修改
|
||||
validations:
|
||||
required: true
|
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Feature: Something you want'
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**是否在使用中遇到某些问题而需要新的特性?请描述:**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**描述你所需要的特性:**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
20
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 功能建议
|
||||
title: "Feature: 功能描述"
|
||||
description: 提出关于项目新功能的想法
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 希望能解决的问题
|
||||
description: 在使用中遇到什么问题而需要新的功能?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: 描述所需要的功能
|
||||
description: 请说明需要的功能或解决方法
|
||||
validations:
|
||||
required: true
|
57
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: 发布插件
|
||||
title: "Plugin: {name}"
|
||||
description: 发布插件到 NoneBot 官方商店
|
||||
labels: ["Plugin"]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: 插件名称
|
||||
description: 插件名称
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: 插件描述
|
||||
description: 插件描述
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: pypi
|
||||
attributes:
|
||||
label: PyPI 项目名
|
||||
description: PyPI 项目名
|
||||
placeholder: e.g. nonebot-plugin-xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: module
|
||||
attributes:
|
||||
label: 插件 import 包名
|
||||
description: 插件 import 包名
|
||||
placeholder: e.g. nonebot_plugin_xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: 插件项目仓库/主页链接
|
||||
description: 插件项目仓库/主页链接
|
||||
placeholder: e.g. https://github.com/xxx/xxx
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tags
|
||||
attributes:
|
||||
label: 标签
|
||||
description: 标签
|
||||
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
|
||||
value: "[]"
|
||||
validations:
|
||||
required: true
|
5
.github/actions/build-api-doc/action.yml
vendored
5
.github/actions/build-api-doc/action.yml
vendored
@@ -5,7 +5,10 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: |
|
||||
poetry run nb-autodoc nonebot
|
||||
poetry run nb-autodoc nonebot \
|
||||
-s nonebot.plugins \
|
||||
-u nonebot.internal \
|
||||
-u nonebot.internal.*
|
||||
cp -r ./build/nonebot/* ./website/docs/api/
|
||||
yarn prettier
|
||||
shell: bash
|
||||
|
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -9,10 +9,10 @@ runs:
|
||||
node-version: "16"
|
||||
|
||||
- id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
21
.github/actions/setup-python/action.yml
vendored
21
.github/actions/setup-python/action.yml
vendored
@@ -5,27 +5,20 @@ inputs:
|
||||
python-version:
|
||||
description: Python version
|
||||
required: false
|
||||
default: "3.9"
|
||||
default: "3.10"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
shell: bash
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
architecture: "x64"
|
||||
|
||||
- uses: Gr1N/setup-poetry@v7
|
||||
|
||||
- id: poetry-cache
|
||||
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.poetry-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
cache: "poetry"
|
||||
|
||||
- run: poetry install -E all
|
||||
shell: bash
|
||||
|
6
.github/workflows/codecov.yml
vendored
6
.github/workflows/codecov.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- "nonebot/**"
|
||||
- "packages/**"
|
||||
- "tests/**"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -15,7 +19,7 @@ jobs:
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
fail-fast: false
|
||||
env:
|
||||
|
88
.github/workflows/noneflow.yml
vendored
Normal file
88
.github/workflows/noneflow.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: NoneFlow
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
plugin_test:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 plugin test
|
||||
if: |
|
||||
!(
|
||||
(
|
||||
github.event.pull_request &&
|
||||
(
|
||||
github.event.pull_request.head.repo.fork ||
|
||||
!(
|
||||
contains(github.event.pull_request.labels.*.name, 'Plugin') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'Adapter') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'Bot')
|
||||
)
|
||||
)
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'issue_comment' && github.event.issue.pull_request
|
||||
)
|
||||
)
|
||||
permissions:
|
||||
issues: read
|
||||
outputs:
|
||||
result: ${{ steps.plugin-test.outputs.RESULT }}
|
||||
output: ${{ steps.plugin-test.outputs.OUTPUT }}
|
||||
steps:
|
||||
- name: Install Poetry
|
||||
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Test Plugin
|
||||
id: plugin-test
|
||||
run: |
|
||||
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
|
||||
noneflow:
|
||||
runs-on: ubuntu-latest
|
||||
name: noneflow
|
||||
needs: plugin_test
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: NoneFlow
|
||||
uses: docker://ghcr.io/nonebot/noneflow:latest
|
||||
with:
|
||||
config: >
|
||||
{
|
||||
"base": "master",
|
||||
"plugin_path": "website/static/plugins.json",
|
||||
"bot_path": "website/static/bots.json",
|
||||
"adapter_path": "website/static/adapters.json"
|
||||
}
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
29
.github/workflows/publish-bot.yml
vendored
29
.github/workflows/publish-bot.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: NoneBot2 Publish Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
publish_bot:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 publish bot
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: NoneBot2 Publish Bot
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
config: >
|
||||
{
|
||||
"base": "master",
|
||||
"plugin_path": "website/static/plugins.json",
|
||||
"bot_path": "website/static/bots.json",
|
||||
"adapter_path": "website/static/adapters.json"
|
||||
}
|
13
.github/workflows/release-drafter.yml
vendored
13
.github/workflows/release-drafter.yml
vendored
@@ -18,9 +18,16 @@ jobs:
|
||||
group: pull-request-changelog
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -43,8 +50,8 @@ jobs:
|
||||
- name: Commit and Push
|
||||
run: |
|
||||
yarn prettier
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email github-actions[bot]@users.noreply.github.com
|
||||
git config user.name noneflow[bot]
|
||||
git config user.email 129742071+noneflow[bot]@users.noreply.github.com
|
||||
git add .
|
||||
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
|
||||
git push
|
||||
|
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -6,12 +6,17 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Generate token
|
||||
id: generate-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_KEY }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
@@ -39,8 +44,8 @@ jobs:
|
||||
|
||||
- name: Push Tag
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email github-actions[bot]@users.noreply.github.com
|
||||
git config user.name noneflow[bot]
|
||||
git config user.email 129742071+noneflow[bot]@users.noreply.github.com
|
||||
git add .
|
||||
git commit -m ":bookmark: Release $(poetry version -s)"
|
||||
git tag ${{ env.TAG_NAME }}
|
||||
|
2
.github/workflows/website-deploy.yml
vendored
2
.github/workflows/website-deploy.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: true
|
||||
|
2
.github/workflows/website-preview.yml
vendored
2
.github/workflows/website-preview.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: false
|
||||
|
@@ -6,27 +6,33 @@ ci:
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/hadialqattan/pycln
|
||||
rev: v2.1.3
|
||||
hooks:
|
||||
- id: pycln
|
||||
args: [--config, pyproject.toml]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
rev: v3.0.0-alpha.9-for-vscode
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml]
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
rev: v0.1.2
|
||||
rev: v0.1.4
|
||||
hooks:
|
||||
- id: nonemoji
|
||||
stages: [prepare-commit-msg]
|
||||
|
@@ -1,3 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
See [changelog.md](./website/src/pages/changelog.md) or <https://v2.nonebot.dev/changelog>
|
||||
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>
|
||||
|
@@ -33,7 +33,7 @@ pre-commit install
|
||||
|
||||
### 使用 GitHub Codespaces(Dev Container)
|
||||
|
||||
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)
|
||||
|
||||
### Commit 规范
|
||||
|
||||
@@ -66,16 +66,17 @@ yarn start
|
||||
|
||||
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||
|
||||
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
|
||||
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
|
||||
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。`
|
||||
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
|
||||
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
|
||||
5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。
|
||||
|
||||
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>。
|
||||
以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。
|
||||
|
||||
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件。
|
||||
如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。
|
||||
|
||||
### 参与开发
|
||||
|
||||
@@ -83,7 +84,7 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
|
||||
|
||||
## 为社区做贡献
|
||||
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://nonebot.dev/docs/developer/plugin-publishing) 一节。
|
||||
|
||||
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
|
||||
|
||||
|
87
README.md
87
README.md
@@ -1,6 +1,6 @@
|
||||
<!-- markdownlint-disable MD033 MD041 -->
|
||||
<p align="center">
|
||||
<a href="https://v2.nonebot.dev/"><img src="https://v2.nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
|
||||
<a href="https://nonebot.dev/"><img src="https://nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python">
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="python">
|
||||
<a href="https://codecov.io/gh/nonebot/nonebot2">
|
||||
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
|
||||
</a>
|
||||
@@ -38,18 +38,21 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://onebot.dev/">
|
||||
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||
</a>
|
||||
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk">
|
||||
</a>
|
||||
<a href="https://core.telegram.org/bots/api">
|
||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||
</a>
|
||||
<a href="https://open.feishu.cn/document/home/index">
|
||||
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
|
||||
</a>
|
||||
<a href="https://docs.github.com/en/developers/apps">
|
||||
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
|
||||
</a>
|
||||
<a href="https://bot.q.qq.com/wiki/">
|
||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
|
||||
</a>
|
||||
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
|
||||
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat Group">
|
||||
@@ -66,18 +69,16 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://v2.nonebot.dev/">文档</a>
|
||||
<a href="https://nonebot.dev/">文档</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
|
||||
<a href="https://nonebot.dev/docs/quick-start">快速上手</a>
|
||||
·
|
||||
<a href="#插件">文档打不开?</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://asciinema.org/a/464654">
|
||||
<img src="https://v2.nonebot.dev/img/setup.svg">
|
||||
<a href="https://asciinema.org/a/569440">
|
||||
<img src="https://nonebot.dev/img/setup.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -89,22 +90,37 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
|
||||
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
|
||||
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://nonebot.dev/docs/editor-support))
|
||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||
- [OneBot 协议](https://onebot.dev/) (QQ 等)
|
||||
- [钉钉](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p)
|
||||
- [Telegram](https://core.telegram.org/bots/api)
|
||||
- [飞书](https://open.feishu.cn/document/home/index)
|
||||
- [QQ 频道](https://bot.q.qq.com/wiki/)
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换
|
||||
- [FastAPI](https://fastapi.tiangolo.com/)
|
||||
- [Quart](https://pgjones.gitlab.io/quart/) (异步 Flask)
|
||||
- [aiohttp](https://docs.aiohttp.org/en/stable/)
|
||||
- [httpx](https://www.python-httpx.org/)
|
||||
- [websockets](https://websockets.readthedocs.io/en/stable/)
|
||||
|
||||
更多:[概览](https://v2.nonebot.dev/docs/)
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
|
||||
| OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | |
|
||||
| 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | |
|
||||
| GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP |
|
||||
| QQ 频道([仓库](https://github.com/nonebot/adapter-qqguild),[协议](https://bot.q.qq.com/wiki/)) | ✅ | 官方接口调整较多 |
|
||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||
| Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 |
|
||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||
| Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
| 驱动框架 | 类型 |
|
||||
| :-----------------------------------------------------------------: | :----: |
|
||||
| [FastAPI](https://fastapi.tiangolo.com/) | 服务端 |
|
||||
| [Quart](https://quart.palletsprojects.com/en/latest/)(异步 Flask) | 服务端 |
|
||||
| [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 |
|
||||
| [httpx](https://www.python-httpx.org/) | 客户端 |
|
||||
| [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 |
|
||||
|
||||
更多:[概览](https://nonebot.dev/docs/)
|
||||
|
||||
## 什么不是 NoneBot2
|
||||
|
||||
@@ -116,16 +132,21 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
|
||||
## 即刻开始
|
||||
|
||||
~~完整~~文档可以在 [这里](https://v2.nonebot.dev/) 查看。
|
||||
~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。
|
||||
|
||||
懒得看文档?下面是快速安装指南:
|
||||
|
||||
1. (**强烈建议**)使用你喜欢的 Python 环境管理工具创建新的虚拟环境。
|
||||
|
||||
2. 使用 `pip` (或其他) 安装 NoneBot 脚手架。
|
||||
1. 安装 [pipx](https://pypa.github.io/pipx/)
|
||||
|
||||
```bash
|
||||
pip install nb-cli
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
2. 安装脚手架
|
||||
|
||||
```bash
|
||||
pipx install nb-cli
|
||||
```
|
||||
|
||||
3. 使用脚手架创建项目
|
||||
@@ -134,6 +155,12 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
nb create
|
||||
```
|
||||
|
||||
4. 运行项目
|
||||
|
||||
```bash
|
||||
nb run
|
||||
```
|
||||
|
||||
## 社区资源
|
||||
|
||||
### 常见问题
|
||||
@@ -162,7 +189,7 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
||||
- [文档镜像(Vercel)](https://nonebot2-vercel-mirror.vercel.app)
|
||||
|
||||
- 其他插件请查看 [商店](https://v2.nonebot.dev/store)
|
||||
- 其他插件请查看 [商店](https://nonebot.dev/store)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
@@ -16,6 +16,7 @@
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
|
||||
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
|
||||
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
|
||||
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
|
||||
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
|
||||
@@ -29,7 +30,6 @@
|
||||
- `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
|
||||
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
||||
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
FrontMatter:
|
||||
@@ -37,24 +37,25 @@ FrontMatter:
|
||||
description: nonebot 模块
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from typing import Any, Dict, Type, Optional
|
||||
import os
|
||||
from importlib.metadata import version
|
||||
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
|
||||
|
||||
import loguru
|
||||
from pydantic.env_settings import DotenvType
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.log import logger, default_filter
|
||||
from nonebot.log import logger as logger
|
||||
from nonebot.adapters import Bot, Adapter
|
||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
|
||||
__version__ = _dist.version
|
||||
VERSION = _dist.parsed_version
|
||||
__version__ = version("nonebot2")
|
||||
except Exception: # pragma: no cover
|
||||
__version__ = None
|
||||
VERSION = None
|
||||
|
||||
A = TypeVar("A", bound=Adapter)
|
||||
|
||||
_driver: Optional[Driver] = None
|
||||
|
||||
@@ -80,6 +81,56 @@ def get_driver() -> Driver:
|
||||
return _driver
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: str) -> Adapter:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: Type[A]) -> A:
|
||||
...
|
||||
|
||||
|
||||
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
|
||||
异常:
|
||||
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
from nonebot.adapters.console import Adapter
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
```
|
||||
"""
|
||||
adapters = get_adapters()
|
||||
target = name if isinstance(name, str) else name.get_name()
|
||||
if target not in adapters:
|
||||
raise ValueError(f"Adapter {target} not registered.")
|
||||
return adapters[target]
|
||||
|
||||
|
||||
def get_adapters() -> Dict[str, Adapter]:
|
||||
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
所有 {ref}`nonebot.adapters.Adapter` 实例字典
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
adapters = nonebot.get_adapters()
|
||||
```
|
||||
"""
|
||||
return get_driver()._adapters.copy()
|
||||
|
||||
|
||||
def get_app() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
|
||||
|
||||
@@ -172,42 +223,35 @@ def get_bots() -> Dict[str, Bot]:
|
||||
bots = nonebot.get_bots()
|
||||
```
|
||||
"""
|
||||
driver = get_driver()
|
||||
return driver.bots
|
||||
|
||||
|
||||
def _resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
return get_driver().bots
|
||||
|
||||
|
||||
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
||||
drivers = obj_str.split("+")
|
||||
DriverClass = _resolve_dot_notation(
|
||||
DriverClass = resolve_dot_notation(
|
||||
drivers[0], "Driver", default_prefix="nonebot.drivers."
|
||||
)
|
||||
if len(drivers) == 1:
|
||||
logger.trace(f"Detected driver {DriverClass} with no mixins.")
|
||||
return DriverClass
|
||||
mixins = [
|
||||
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
for mixin in drivers[1:]
|
||||
]
|
||||
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
|
||||
return combine_driver(DriverClass, *mixins)
|
||||
|
||||
|
||||
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
def _log_patcher(record: "loguru.Record"):
|
||||
record["name"] = (
|
||||
plugin.name
|
||||
if (module_name := record["name"])
|
||||
and (plugin := get_plugin_by_module_name(module_name))
|
||||
else (module_name and module_name.split(".")[0])
|
||||
)
|
||||
|
||||
|
||||
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
||||
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||
|
||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||
@@ -227,13 +271,17 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
if not _driver:
|
||||
logger.success("NoneBot is initializing...")
|
||||
env = Env()
|
||||
_env_file = _env_file or f".env.{env.environment}"
|
||||
config = Config(
|
||||
**kwargs,
|
||||
_common_config=env.dict(),
|
||||
_env_file=_env_file or f".env.{env.environment}",
|
||||
_env_file=(".env", _env_file)
|
||||
if isinstance(_env_file, (str, os.PathLike))
|
||||
else _env_file,
|
||||
)
|
||||
|
||||
default_filter.level = config.log_level
|
||||
logger.configure(
|
||||
extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher
|
||||
)
|
||||
logger.opt(colors=True).info(
|
||||
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
||||
)
|
||||
@@ -241,7 +289,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
|
||||
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
|
||||
)
|
||||
|
||||
DriverClass: Type[Driver] = _resolve_combine_expr(config.driver)
|
||||
DriverClass = _resolve_combine_expr(config.driver)
|
||||
_driver = DriverClass(env, config)
|
||||
|
||||
|
||||
@@ -262,7 +310,7 @@ def run(*args: Any, **kwargs: Any) -> None:
|
||||
|
||||
|
||||
from nonebot.plugin import on as on
|
||||
from nonebot.plugin import export as export
|
||||
from nonebot.plugin import on_type as on_type
|
||||
from nonebot.plugin import require as require
|
||||
from nonebot.plugin import on_regex as on_regex
|
||||
from nonebot.plugin import on_notice as on_notice
|
||||
|
@@ -7,21 +7,6 @@ FrontMatter:
|
||||
description: nonebot.adapters 模块
|
||||
"""
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
del pkg_resources
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
|
||||
__path__: Iterable[str] = pkgutil.extend_path(__path__, __name__) # type: ignore
|
||||
del pkgutil
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from nonebot.internal.adapter import Bot as Bot
|
||||
from nonebot.internal.adapter import Event as Event
|
||||
from nonebot.internal.adapter import Adapter as Adapter
|
||||
@@ -34,6 +19,11 @@ __autodoc__ = {
|
||||
"Event": True,
|
||||
"Adapter": True,
|
||||
"Message": True,
|
||||
"Message.__getitem__": True,
|
||||
"Message.__contains__": True,
|
||||
"Message._construct": True,
|
||||
"MessageSegment": True,
|
||||
"MessageSegment.__str__": True,
|
||||
"MessageSegment.__add__": True,
|
||||
"MessageTemplate": True,
|
||||
}
|
||||
|
@@ -9,23 +9,21 @@ FrontMatter:
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
|
||||
|
||||
from pydantic import BaseSettings, IPvAnyAddress
|
||||
from pydantic.utils import deep_update
|
||||
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
||||
from pydantic.env_settings import (
|
||||
DotenvType,
|
||||
SettingsError,
|
||||
EnvSettingsSource,
|
||||
InitSettingsSource,
|
||||
SettingsSourceCallable,
|
||||
read_env_file,
|
||||
env_file_sentinel,
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
|
||||
|
||||
class CustomEnvSettings(EnvSettingsSource):
|
||||
@@ -33,33 +31,15 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
"""
|
||||
Build environment variables suitable for passing to the Model.
|
||||
"""
|
||||
d: Dict[str, Optional[str]] = {}
|
||||
d: Dict[str, Any] = {}
|
||||
|
||||
if settings.__config__.case_sensitive:
|
||||
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
|
||||
else:
|
||||
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
||||
|
||||
env_file_vars: Dict[str, Optional[str]] = {}
|
||||
env_file = (
|
||||
self.env_file
|
||||
if self.env_file != env_file_sentinel
|
||||
else settings.__config__.env_file
|
||||
)
|
||||
env_file_encoding = (
|
||||
self.env_file_encoding
|
||||
if self.env_file_encoding is not None
|
||||
else settings.__config__.env_file_encoding
|
||||
)
|
||||
if env_file is not None:
|
||||
env_path = Path(env_file)
|
||||
if env_path.is_file():
|
||||
env_file_vars = read_env_file(
|
||||
env_path,
|
||||
encoding=env_file_encoding, # type: ignore
|
||||
case_sensitive=settings.__config__.case_sensitive,
|
||||
)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
|
||||
for field in settings.__fields__.values():
|
||||
env_val: Optional[str] = None
|
||||
@@ -70,29 +50,56 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
if env_val is not None:
|
||||
break
|
||||
|
||||
if env_val is None:
|
||||
continue
|
||||
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||
if is_complex:
|
||||
if env_val is None:
|
||||
if env_val_built := self.explode_env_vars(field, env_vars):
|
||||
d[field.alias] = env_val_built
|
||||
else:
|
||||
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
|
||||
try:
|
||||
env_val = settings.__config__.parse_env_var(field.name, env_val)
|
||||
except ValueError as e:
|
||||
if not allow_parse_failure:
|
||||
raise SettingsError(
|
||||
f'error parsing env var "{env_name}"' # type: ignore
|
||||
) from e
|
||||
|
||||
if field.is_complex():
|
||||
try:
|
||||
env_val = settings.__config__.json_loads(env_val)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise SettingsError(
|
||||
f'error parsing JSON for "{env_name}"' # type: ignore
|
||||
) from e
|
||||
d[field.alias] = env_val
|
||||
if isinstance(env_val, dict):
|
||||
d[field.alias] = deep_update(
|
||||
env_val, self.explode_env_vars(field, env_vars)
|
||||
)
|
||||
else:
|
||||
d[field.alias] = env_val
|
||||
elif env_val is not None:
|
||||
# simplest case, field is not complex, we only need to add the value if it was found
|
||||
d[field.alias] = env_val
|
||||
|
||||
if env_file_vars:
|
||||
for env_name in env_file_vars.keys():
|
||||
env_val = env_vars[env_name]
|
||||
# remain user custom config
|
||||
for env_name in env_file_vars:
|
||||
env_val = env_vars[env_name]
|
||||
if env_val and (val_striped := env_val.strip()):
|
||||
# there's a value, decode that as JSON
|
||||
try:
|
||||
if env_val:
|
||||
env_val = settings.__config__.json_loads(env_val.strip())
|
||||
env_val = settings.__config__.parse_env_var(env_name, val_striped)
|
||||
except ValueError as e:
|
||||
logger.opt(colors=True, exception=e).trace(
|
||||
f"Error while parsing JSON for {escape_tag(env_name)}. Assumed as string."
|
||||
logger.trace(
|
||||
"Error while parsing JSON for "
|
||||
f"{env_name!r}={val_striped!r}. "
|
||||
"Assumed as string."
|
||||
)
|
||||
|
||||
# explode value when it's a nested dict
|
||||
env_name, *nested_keys = env_name.split(self.env_nested_delimiter)
|
||||
if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):
|
||||
result = {}
|
||||
*keys, last_key = nested_keys
|
||||
_tmp = result
|
||||
for key in keys:
|
||||
_tmp = _tmp.setdefault(key, {})
|
||||
_tmp[last_key] = env_val
|
||||
d[env_name] = deep_update(d.get(env_name, {}), result)
|
||||
elif not nested_keys:
|
||||
d[env_name] = env_val
|
||||
|
||||
return d
|
||||
@@ -105,6 +112,9 @@ class BaseConfig(BaseSettings):
|
||||
return self.__dict__.get(name)
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
env_nested_delimiter = "__"
|
||||
|
||||
@classmethod
|
||||
def customise_sources(
|
||||
cls,
|
||||
@@ -116,7 +126,10 @@ class BaseConfig(BaseSettings):
|
||||
return (
|
||||
init_settings,
|
||||
CustomEnvSettings(
|
||||
env_settings.env_file, env_settings.env_file_encoding
|
||||
env_settings.env_file,
|
||||
env_settings.env_file_encoding,
|
||||
env_settings.env_nested_delimiter,
|
||||
env_settings.env_prefix_len,
|
||||
),
|
||||
InitSettingsSource(common_config),
|
||||
file_secret_settings,
|
||||
@@ -136,7 +149,6 @@ class Env(BaseConfig):
|
||||
"""
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
@@ -146,11 +158,10 @@ class Config(BaseConfig):
|
||||
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
||||
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
|
||||
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
|
||||
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
|
||||
"""
|
||||
|
||||
_env_file: str = ".env"
|
||||
_common_config: Dict[str, Any] = {}
|
||||
_env_file: DotenvType = ".env", ".env.prod"
|
||||
|
||||
# nonebot configs
|
||||
driver: str = "~fastapi"
|
||||
@@ -162,7 +173,7 @@ class Config(BaseConfig):
|
||||
"""
|
||||
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||
port: int = 8080
|
||||
port: int = Field(default=8080, ge=1, le=65535)
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||
log_level: Union[int, str] = "INFO"
|
||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
|
||||
@@ -230,8 +241,7 @@ class Config(BaseConfig):
|
||||
# or from env file using json loads
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
env_file = ".env.prod"
|
||||
env_file = ".env", ".env.prod"
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
|
@@ -4,7 +4,9 @@ FrontMatter:
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
from typing_extensions import Literal
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
# used by Matcher
|
||||
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
|
||||
@@ -30,6 +32,8 @@ CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
|
||||
"""命令参数存储 key"""
|
||||
CMD_START_KEY: Literal["command_start"] = "command_start"
|
||||
"""命令开头存储 key"""
|
||||
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
|
||||
"""命令与参数间空白符存储 key"""
|
||||
|
||||
SHELL_ARGS: Literal["_args"] = "_args"
|
||||
"""shell 命令 parse 后参数字典存储 key"""
|
||||
@@ -38,7 +42,13 @@ SHELL_ARGV: Literal["_argv"] = "_argv"
|
||||
|
||||
REGEX_MATCHED: Literal["_matched"] = "_matched"
|
||||
"""正则匹配结果存储 key"""
|
||||
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
|
||||
"""正则匹配 group 元组存储 key"""
|
||||
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
|
||||
"""正则匹配 group 字典存储 key"""
|
||||
STARTSWITH_KEY: Literal["_startswith"] = "_startswith"
|
||||
"""响应触发前缀 key"""
|
||||
ENDSWITH_KEY: Literal["_endswith"] = "_endswith"
|
||||
"""响应触发后缀 key"""
|
||||
FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch"
|
||||
"""响应触发完整消息 key"""
|
||||
KEYWORD_KEY: Literal["_keyword"] = "_keyword"
|
||||
"""响应触发关键字 key"""
|
||||
|
||||
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
|
||||
|
@@ -6,15 +6,31 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing import Any, Dict, List, Type, Generic, TypeVar, Callable, Optional
|
||||
from dataclasses import field, dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
Optional,
|
||||
Awaitable,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pydantic import BaseConfig
|
||||
from pydantic.schema import get_annotation_from_field_info
|
||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.typing import _DependentCallable
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
from .utils import check_field_type, get_typed_signature
|
||||
@@ -31,25 +47,29 @@ class Param(abc.ABC, FieldInfo):
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: "Dependent", name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
||||
) -> Optional["Param"]:
|
||||
return None
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, dependent: "Dependent", value: Any
|
||||
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
|
||||
) -> Optional["Param"]:
|
||||
return None
|
||||
return
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _solve(self, **kwargs: Any) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
return
|
||||
|
||||
|
||||
class CustomConfig(BaseConfig):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dependent(Generic[R]):
|
||||
"""依赖注入容器
|
||||
|
||||
@@ -61,101 +81,70 @@ class Dependent(Generic[R]):
|
||||
allow_types: 允许的参数类型
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
call: Callable[..., Any],
|
||||
pre_checkers: Optional[List[Param]] = None,
|
||||
params: Optional[List[ModelField]] = None,
|
||||
parameterless: Optional[List[Param]] = None,
|
||||
allow_types: Optional[List[Type[Param]]] = None,
|
||||
) -> None:
|
||||
self.call = call
|
||||
self.pre_checkers = pre_checkers or []
|
||||
self.params = params or []
|
||||
self.parameterless = parameterless or []
|
||||
self.allow_types = allow_types or []
|
||||
call: _DependentCallable[R]
|
||||
params: Tuple[ModelField] = field(default_factory=tuple)
|
||||
parameterless: Tuple[Param] = field(default_factory=tuple)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if inspect.isfunction(self.call) or inspect.isclass(self.call):
|
||||
call_str = self.call.__name__
|
||||
else:
|
||||
call_str = repr(self.call)
|
||||
return (
|
||||
f"<Dependent call={self.call}, params={self.params},"
|
||||
f" parameterless={self.parameterless}>"
|
||||
f"Dependent(call={call_str}"
|
||||
+ (f", parameterless={self.parameterless}" if self.parameterless else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
async def __call__(self, **kwargs: Any) -> R:
|
||||
# do pre-check
|
||||
await self.check(**kwargs)
|
||||
|
||||
# solve param values
|
||||
values = await self.solve(**kwargs)
|
||||
|
||||
# call function
|
||||
if is_coroutine_callable(self.call):
|
||||
return await self.call(**values)
|
||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||
else:
|
||||
return await run_sync(self.call)(**values)
|
||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||
|
||||
def parse_param(self, name: str, param: inspect.Parameter) -> Param:
|
||||
for allow_type in self.allow_types:
|
||||
field_info = allow_type._check_param(self, name, param)
|
||||
if field_info:
|
||||
return field_info
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameter {name} for function {self.call} with type {param.annotation}"
|
||||
)
|
||||
@staticmethod
|
||||
def parse_params(
|
||||
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
||||
) -> Tuple[ModelField]:
|
||||
fields: List[ModelField] = []
|
||||
params = get_typed_signature(call).parameters.values()
|
||||
|
||||
def parse_parameterless(self, value: Any) -> Param:
|
||||
for allow_type in self.allow_types:
|
||||
field_info = allow_type._check_parameterless(self, value)
|
||||
if field_info:
|
||||
return field_info
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameterless {value} for function {self.call} with type {type(value)}"
|
||||
)
|
||||
|
||||
def prepend_parameterless(self, value: Any) -> None:
|
||||
self.parameterless.insert(0, self.parse_parameterless(value))
|
||||
|
||||
def append_parameterless(self, value: Any) -> None:
|
||||
self.parameterless.append(self.parse_parameterless(value))
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls: Type[T],
|
||||
*,
|
||||
call: Callable[..., Any],
|
||||
parameterless: Optional[List[Any]] = None,
|
||||
allow_types: Optional[List[Type[Param]]] = None,
|
||||
) -> T:
|
||||
signature = get_typed_signature(call)
|
||||
params = signature.parameters
|
||||
dependent = cls(
|
||||
call=call,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
|
||||
for param_name, param in params.items():
|
||||
for param in params:
|
||||
default_value = Required
|
||||
if param.default != param.empty:
|
||||
default_value = param.default
|
||||
|
||||
if isinstance(default_value, Param):
|
||||
field_info = default_value
|
||||
default_value = field_info.default
|
||||
else:
|
||||
field_info = dependent.parse_param(param_name, param)
|
||||
default_value = field_info.default
|
||||
for allow_type in allow_types:
|
||||
if field_info := allow_type._check_param(param, allow_types):
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown parameter {param.name} for function {call} with type {param.annotation}"
|
||||
)
|
||||
|
||||
default_value = field_info.default
|
||||
|
||||
annotation: Any = Any
|
||||
required = default_value == Required
|
||||
if param.annotation != param.empty:
|
||||
annotation = param.annotation
|
||||
annotation = get_annotation_from_field_info(
|
||||
annotation, field_info, param_name
|
||||
annotation, field_info, param.name
|
||||
)
|
||||
dependent.params.append(
|
||||
|
||||
fields.append(
|
||||
ModelField(
|
||||
name=param_name,
|
||||
name=param.name,
|
||||
type_=annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
@@ -165,49 +154,72 @@ class Dependent(Generic[R]):
|
||||
)
|
||||
)
|
||||
|
||||
parameterless_params = [
|
||||
dependent.parse_parameterless(param) for param in (parameterless or [])
|
||||
]
|
||||
dependent.parameterless.extend(parameterless_params)
|
||||
return tuple(fields)
|
||||
|
||||
logger.trace(
|
||||
f"Parsed dependent with call={call}, "
|
||||
f"params={[param.field_info for param in dependent.params]}, "
|
||||
f"parameterless={dependent.parameterless}"
|
||||
@staticmethod
|
||||
def parse_parameterless(
|
||||
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
|
||||
) -> Tuple[Param, ...]:
|
||||
parameterless_params: List[Param] = []
|
||||
for value in parameterless:
|
||||
for allow_type in allow_types:
|
||||
if param := allow_type._check_parameterless(value, allow_types):
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Unknown parameterless {value}")
|
||||
parameterless_params.append(param)
|
||||
return tuple(parameterless_params)
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
*,
|
||||
call: _DependentCallable[R],
|
||||
parameterless: Optional[Iterable[Any]] = None,
|
||||
allow_types: Iterable[Type[Param]],
|
||||
) -> "Dependent[R]":
|
||||
allow_types = tuple(allow_types)
|
||||
|
||||
params = cls.parse_params(call, allow_types)
|
||||
parameterless_params = (
|
||||
tuple()
|
||||
if parameterless is None
|
||||
else cls.parse_parameterless(tuple(parameterless), allow_types)
|
||||
)
|
||||
|
||||
return dependent
|
||||
return cls(call, params, parameterless_params)
|
||||
|
||||
async def solve(
|
||||
self,
|
||||
**params: Any,
|
||||
) -> Dict[str, Any]:
|
||||
values: Dict[str, Any] = {}
|
||||
async def check(self, **params: Any) -> None:
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(param._check(**params) for param in self.parameterless)
|
||||
)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
cast(Param, param.field_info)._check(**params)
|
||||
for param in self.params
|
||||
)
|
||||
)
|
||||
except SkippedException as e:
|
||||
logger.trace(f"{self} skipped due to {e}")
|
||||
raise
|
||||
|
||||
for checker in self.pre_checkers:
|
||||
await checker._solve(**params)
|
||||
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
||||
value = await cast(Param, field.field_info)._solve(**params)
|
||||
if value is Undefined:
|
||||
value = field.get_default()
|
||||
return check_field_type(field, value)
|
||||
|
||||
async def solve(self, **params: Any) -> Dict[str, Any]:
|
||||
# solve parameterless
|
||||
for param in self.parameterless:
|
||||
await param._solve(**params)
|
||||
|
||||
for field in self.params:
|
||||
field_info = field.field_info
|
||||
assert isinstance(field_info, Param), "Params must be subclasses of Param"
|
||||
value = await field_info._solve(**params)
|
||||
if value is Undefined:
|
||||
value = field.get_default()
|
||||
|
||||
try:
|
||||
values[field.name] = check_field_type(field, value)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"{field_info} "
|
||||
f"type {type(value)} not match depends {self.call} "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
|
||||
return values
|
||||
# solve param values
|
||||
values = await asyncio.gather(
|
||||
*(self._solve_field(field, params) for field in self.params)
|
||||
)
|
||||
return {field.name: value for field, value in zip(self.params, values)}
|
||||
|
||||
|
||||
__autodoc__ = {"CustomConfig": False}
|
||||
|
@@ -4,11 +4,11 @@ FrontMatter:
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
import inspect
|
||||
from typing import Any, Dict, TypeVar, Callable
|
||||
from typing import Any, Dict, TypeVar, Callable, ForwardRef
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.fields import ModelField
|
||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||||
from pydantic.typing import evaluate_forwardref
|
||||
|
||||
from nonebot.exception import TypeMisMatch
|
||||
|
||||
@@ -28,8 +28,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
return inspect.Signature(typed_params)
|
||||
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
|
@@ -1,149 +0,0 @@
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Set, Union, Callable, Awaitable
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
STARTUP_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
SHUTDOWN_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
|
||||
|
||||
class BlockDriver(Driver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
self.startup_funcs: Set[STARTUP_FUNC] = set()
|
||||
self.shutdown_funcs: Set[SHUTDOWN_FUNC] = set()
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `block_driver`"""
|
||||
return "block_driver"
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def logger(self):
|
||||
"""block driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(Driver)
|
||||
def on_startup(self, func: STARTUP_FUNC) -> STARTUP_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
self.startup_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
self.shutdown_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 block driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.serve())
|
||||
|
||||
async def serve(self):
|
||||
self.install_signal_handlers()
|
||||
await self.startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self.main_loop()
|
||||
await self.shutdown()
|
||||
|
||||
async def startup(self):
|
||||
# run startup
|
||||
cors = [
|
||||
startup() if is_coroutine_callable(startup) else run_sync(startup)()
|
||||
for startup in self.startup_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<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.")
|
||||
# run shutdown
|
||||
cors = [
|
||||
shutdown() if is_coroutine_callable(shutdown) else run_sync(shutdown)()
|
||||
for shutdown in self.shutdown_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
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:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self.handle_exit)
|
||||
|
||||
def handle_exit(self, sig, frame):
|
||||
if self.should_exit.is_set():
|
||||
self.force_exit = True
|
||||
else:
|
||||
self.should_exit.set()
|
45
nonebot/drivers/_lifespan.py
Normal file
45
nonebot/drivers/_lifespan.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Any, List, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
SYNC_LIFESPAN_FUNC = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
||||
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._startup_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
@staticmethod
|
||||
async def _run_lifespan_func(
|
||||
funcs: List[LIFESPAN_FUNC],
|
||||
) -> None:
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
await cast(ASYNC_LIFESPAN_FUNC, func)()
|
||||
else:
|
||||
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||
|
||||
async def startup(self) -> None:
|
||||
if self._startup_funcs:
|
||||
await self._run_lifespan_func(self._startup_funcs)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if self._shutdown_funcs:
|
||||
await self._run_lifespan_func(self._shutdown_funcs)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.startup()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.shutdown()
|
@@ -21,16 +21,16 @@ from contextlib import asynccontextmanager
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -56,7 +56,13 @@ class Mixin(ForwardMixin):
|
||||
files = aiohttp.FormData()
|
||||
for name, file in setup.files:
|
||||
files.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||
|
||||
cookies = {
|
||||
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
|
||||
}
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, version=version, trust_env=True
|
||||
) as session:
|
||||
async with session.request(
|
||||
setup.method,
|
||||
setup.url,
|
||||
@@ -66,13 +72,12 @@ class Mixin(ForwardMixin):
|
||||
timeout=timeout,
|
||||
proxy=setup.proxy,
|
||||
) as response:
|
||||
res = Response(
|
||||
return Response(
|
||||
response.status,
|
||||
headers=response.headers.copy(),
|
||||
content=await response.read(),
|
||||
request=setup,
|
||||
)
|
||||
return res
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@asynccontextmanager
|
||||
@@ -92,8 +97,7 @@ class Mixin(ForwardMixin):
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy,
|
||||
) as ws:
|
||||
websocket = WebSocket(request=setup, session=session, websocket=ws)
|
||||
yield websocket
|
||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||
|
||||
|
||||
class WebSocket(BaseWebSocket):
|
||||
@@ -166,5 +170,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send_bytes(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""AIOHTTP Driver"""
|
||||
|
@@ -1,5 +1,11 @@
|
||||
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
|
||||
|
||||
```bash
|
||||
nb driver install fastapi
|
||||
# 或者
|
||||
pip install nonebot2[fastapi]
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
本驱动仅支持服务端连接
|
||||
:::
|
||||
@@ -9,15 +15,13 @@ FrontMatter:
|
||||
description: nonebot.drivers.fastapi 模块
|
||||
"""
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Any, List, Tuple, Union, Callable, Optional
|
||||
|
||||
import uvicorn
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from pydantic import BaseSettings
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.typing import overrides
|
||||
@@ -28,6 +32,18 @@ from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
|
||||
) from e
|
||||
|
||||
|
||||
def catch_closed(func):
|
||||
@wraps(func)
|
||||
@@ -63,6 +79,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_extra: Dict[str, Any] = {}
|
||||
"""传递给 `FastAPI` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -76,10 +94,14 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.fastapi_config: Config = Config(**config.dict())
|
||||
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
self._server_app = FastAPI(
|
||||
lifespan=self._lifespan_manager,
|
||||
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
||||
docs_url=self.fastapi_config.fastapi_docs_url,
|
||||
redoc_url=self.fastapi_config.fastapi_redoc_url,
|
||||
**self.fastapi_config.fastapi_extra,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -131,14 +153,20 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_startup(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
|
||||
return self.server_app.on_event("startup")(func)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_shutdown(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_"""
|
||||
return self.server_app.on_event("shutdown")(func)
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _lifespan_manager(self, app: FastAPI):
|
||||
await self._lifespan.startup()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await self._lifespan.shutdown()
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def run(
|
||||
@@ -186,14 +214,12 @@ class Driver(ReverseDriver):
|
||||
setup: HTTPServerSetup,
|
||||
) -> Response:
|
||||
json: Any = None
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
json = await request.json()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
data: Optional[dict] = None
|
||||
files: Optional[List[Tuple[str, FileTypes]]] = None
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
form = await request.form()
|
||||
data = {}
|
||||
files = []
|
||||
@@ -204,8 +230,7 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
else:
|
||||
data[key] = value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
http_request = BaseRequest(
|
||||
request.method,
|
||||
str(request.url),
|
||||
@@ -219,7 +244,9 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
response = await setup.handle_func(http_request)
|
||||
return Response(response.content, response.status_code, dict(response.headers))
|
||||
return Response(
|
||||
response.content, response.status_code, dict(response.headers.items())
|
||||
)
|
||||
|
||||
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
|
||||
request = BaseRequest(
|
||||
@@ -261,7 +288,7 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
async def close(
|
||||
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
||||
) -> None:
|
||||
await self.websocket.close(code)
|
||||
await self.websocket.close(code, reason)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
|
@@ -18,7 +18,7 @@ from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import (
|
||||
Request,
|
||||
Response,
|
||||
@@ -31,10 +31,10 @@ from nonebot.drivers import (
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -48,17 +48,18 @@ class Mixin(ForwardMixin):
|
||||
@overrides(ForwardMixin)
|
||||
async def request(self, setup: Request) -> Response:
|
||||
async with httpx.AsyncClient(
|
||||
cookies=setup.cookies.jar,
|
||||
http2=setup.version == HTTPVersion.H2,
|
||||
proxies=setup.proxy, # type: ignore
|
||||
proxies=setup.proxy,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
content=setup.content, # type: ignore
|
||||
data=setup.data, # type: ignore
|
||||
content=setup.content,
|
||||
data=setup.data,
|
||||
json=setup.json,
|
||||
files=setup.files, # type: ignore
|
||||
files=setup.files,
|
||||
headers=tuple(setup.headers.items()),
|
||||
timeout=setup.timeout,
|
||||
)
|
||||
@@ -76,5 +77,5 @@ class Mixin(ForwardMixin):
|
||||
yield ws
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""HTTPX Driver"""
|
||||
|
156
nonebot/drivers/none.py
Normal file
156
nonebot/drivers/none.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""None 驱动适配
|
||||
|
||||
:::tip 提示
|
||||
本驱动不支持任何服务器或客户端连接
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 6
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
if WINDOWS: # pragma: py-win32
|
||||
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
|
||||
|
||||
|
||||
class Driver(BaseDriver):
|
||||
"""None 驱动框架"""
|
||||
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `none`"""
|
||||
return "none"
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def logger(self):
|
||||
"""none driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个启动时执行的函数"""
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个停止时执行的函数"""
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._serve())
|
||||
|
||||
async def _serve(self):
|
||||
self._install_signal_handlers()
|
||||
await self._startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self._main_loop()
|
||||
await self._shutdown()
|
||||
|
||||
async def _startup(self):
|
||||
try:
|
||||
await self._lifespan.startup()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<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:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_exit)
|
||||
|
||||
def _handle_exit(self, sig, frame):
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
|
||||
def exit(self, force: bool = False):
|
||||
"""退出 none driver
|
||||
|
||||
参数:
|
||||
force: 强制退出
|
||||
"""
|
||||
if not self.should_exit.is_set():
|
||||
self.should_exit.set()
|
||||
if force:
|
||||
self.force_exit = True
|
@@ -17,9 +17,8 @@ FrontMatter:
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
@@ -32,15 +31,16 @@ from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from quart import request as _request
|
||||
from quart import websocket as _websocket
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ImportError:
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
||||
|
||||
@@ -69,6 +69,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_extra: Dict[str, Any] = {}
|
||||
"""传递给 `Quart` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -82,7 +84,9 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.quart_config = Config(**config.dict())
|
||||
|
||||
self._server_app = Quart(self.__class__.__qualname__)
|
||||
self._server_app = Quart(
|
||||
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||
)
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
|
@@ -23,17 +23,17 @@ from nonebot.typing import overrides
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||
except ImportError:
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
||||
)
|
||||
) from e
|
||||
|
||||
logger = logging.Logger("websockets.client", "INFO")
|
||||
logger.addHandler(LoguruHandler())
|
||||
@@ -70,7 +70,7 @@ class Mixin(ForwardMixin):
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
connection = Connect(
|
||||
str(setup.url),
|
||||
extra_headers=setup.headers.items(),
|
||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
open_timeout=setup.timeout,
|
||||
)
|
||||
async with connection as ws:
|
||||
@@ -101,8 +101,7 @@ class WebSocket(BaseWebSocket):
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
msg = await self.websocket.recv()
|
||||
return msg
|
||||
return await self.websocket.recv()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
@@ -129,5 +128,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""Websockets Driver"""
|
||||
|
@@ -37,6 +37,9 @@ from pydantic.fields import ModelField
|
||||
class NoneBotException(Exception):
|
||||
"""所有 NoneBot 发生的异常基类。"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
# Rule Exception
|
||||
class ParserExit(NoneBotException):
|
||||
@@ -46,11 +49,12 @@ class ParserExit(NoneBotException):
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ParserExit status={self.status} message={self.message}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"ParserExit(status={self.status}"
|
||||
+ (f", message={self.message!r}" if self.message else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
|
||||
# Processor Exception
|
||||
@@ -68,11 +72,8 @@ class IgnoredException(ProcessException):
|
||||
def __init__(self, reason: Any):
|
||||
self.reason: Any = reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IgnoredException, reason={self.reason}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
def __repr__(self) -> str:
|
||||
return f"IgnoredException(reason={self.reason!r})"
|
||||
|
||||
|
||||
class SkippedException(ProcessException):
|
||||
@@ -99,11 +100,11 @@ class TypeMisMatch(SkippedException):
|
||||
self.param: ModelField = param
|
||||
self.value: Any = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
|
||||
|
||||
def __str__(self):
|
||||
self.__repr__()
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"TypeMisMatch(param={self.param.name}, "
|
||||
f"type={self.param._type_display()}, value={self.value!r}>"
|
||||
)
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
@@ -116,11 +117,8 @@ class MockApiException(ProcessException):
|
||||
def __init__(self, result: Any):
|
||||
self.result = result
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApiCancelledException, result={self.result}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
def __repr__(self) -> str:
|
||||
return f"MockApiException(result={self.result!r})"
|
||||
|
||||
|
||||
class StopPropagation(ProcessException):
|
||||
@@ -195,7 +193,8 @@ class AdapterException(NoneBotException):
|
||||
adapter_name: 标识 adapter
|
||||
"""
|
||||
|
||||
def __init__(self, adapter_name: str) -> None:
|
||||
def __init__(self, adapter_name: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.adapter_name: str = adapter_name
|
||||
|
||||
|
||||
@@ -231,4 +230,8 @@ class WebSocketClosed(DriverException):
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<WebSocketClosed code={self.code} reason={self.reason}>"
|
||||
return (
|
||||
f"WebSocketClosed(code={self.code}"
|
||||
+ (f", reason={self.reason!r}" if self.reason else "")
|
||||
+ ")"
|
||||
)
|
||||
|
@@ -33,6 +33,9 @@ class Adapter(abc.ABC):
|
||||
self.bots: Dict[str, Bot] = {}
|
||||
"""本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Adapter(name={self.get_name()!r})"
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_name(cls) -> str:
|
||||
@@ -63,8 +66,9 @@ class Adapter(abc.ABC):
|
||||
参数:
|
||||
bot: {ref}`nonebot.adapters.Bot` 实例
|
||||
"""
|
||||
if self.bots.pop(bot.self_id, None) is None:
|
||||
raise RuntimeError(f"{bot} not found in adapter {self.get_name()}")
|
||||
self.driver._bot_disconnect(bot)
|
||||
self.bots.pop(bot.self_id, None)
|
||||
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
"""设置一个 HTTP 服务器路由配置"""
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing_extensions import Protocol
|
||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional
|
||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Config
|
||||
@@ -14,10 +13,9 @@ if TYPE_CHECKING:
|
||||
from .adapter import Adapter
|
||||
from .message import Message, MessageSegment
|
||||
|
||||
|
||||
class _ApiCall(Protocol):
|
||||
async def __call__(self, **kwargs: Any) -> Any:
|
||||
...
|
||||
class _ApiCall(Protocol):
|
||||
async def __call__(self, **kwargs: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
class Bot(abc.ABC):
|
||||
@@ -41,7 +39,14 @@ class Bot(abc.ABC):
|
||||
self.self_id: str = self_id
|
||||
"""机器人 ID"""
|
||||
|
||||
def __getattr__(self, name: str) -> _ApiCall:
|
||||
def __repr__(self) -> str:
|
||||
return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
|
||||
|
||||
def __getattr__(self, name: str) -> "_ApiCall":
|
||||
if name.startswith("__") and name.endswith("__"):
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||
)
|
||||
return partial(self.call_api, name)
|
||||
|
||||
@property
|
||||
@@ -72,8 +77,7 @@ class Bot(abc.ABC):
|
||||
skip_calling_api: bool = False
|
||||
exception: Optional[Exception] = None
|
||||
|
||||
coros = list(map(lambda x: x(self, api, data), self._calling_api_hook))
|
||||
if coros:
|
||||
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
||||
try:
|
||||
logger.debug("Running CallingAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
@@ -95,10 +99,9 @@ class Bot(abc.ABC):
|
||||
except Exception as e:
|
||||
exception = e
|
||||
|
||||
coros = list(
|
||||
map(lambda x: x(self, exception, api, data, result), self._called_api_hook)
|
||||
)
|
||||
if coros:
|
||||
if coros := [
|
||||
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
||||
]:
|
||||
try:
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
|
@@ -47,7 +47,7 @@ class Event(abc.ABC, BaseModel):
|
||||
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
|
||||
|
||||
异常:
|
||||
NoLogException
|
||||
NoLogException:
|
||||
"""
|
||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import abc
|
||||
from copy import deepcopy
|
||||
from typing_extensions import Self
|
||||
from dataclasses import field, asdict, dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -12,6 +13,7 @@ from typing import (
|
||||
TypeVar,
|
||||
Iterable,
|
||||
Optional,
|
||||
SupportsIndex,
|
||||
overload,
|
||||
)
|
||||
|
||||
@@ -19,7 +21,6 @@ from pydantic import parse_obj_as
|
||||
|
||||
from .template import MessageTemplate
|
||||
|
||||
T = TypeVar("T")
|
||||
TMS = TypeVar("TMS", bound="MessageSegment")
|
||||
TM = TypeVar("TM", bound="Message")
|
||||
|
||||
@@ -47,7 +48,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
def __len__(self) -> int:
|
||||
return len(str(self))
|
||||
|
||||
def __ne__(self: T, other: T) -> bool:
|
||||
def __ne__(self, other: Self) -> bool:
|
||||
return not self == other
|
||||
|
||||
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
@@ -61,7 +62,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
yield cls._validate
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, value):
|
||||
def _validate(cls, value) -> Self:
|
||||
if isinstance(value, cls):
|
||||
return value
|
||||
if not isinstance(value, dict):
|
||||
@@ -84,7 +85,10 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
def items(self):
|
||||
return asdict(self).items()
|
||||
|
||||
def copy(self: T) -> T:
|
||||
def join(self: TMS, iterable: Iterable[Union[TMS, TM]]) -> TM:
|
||||
return self.get_message_class()(self).join(iterable)
|
||||
|
||||
def copy(self) -> Self:
|
||||
return deepcopy(self)
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -117,7 +121,7 @@ class Message(List[TMS], abc.ABC):
|
||||
self.extend(self._construct(message)) # pragma: no cover
|
||||
|
||||
@classmethod
|
||||
def template(cls: Type[TM], format_string: Union[str, TM]) -> MessageTemplate[TM]:
|
||||
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
|
||||
"""创建消息模板。
|
||||
|
||||
用法和 `str.format` 大致相同, 但是可以输出消息对象, 并且支持以 `Message` 对象作为消息模板
|
||||
@@ -146,7 +150,7 @@ class Message(List[TMS], abc.ABC):
|
||||
yield cls._validate
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, value):
|
||||
def _validate(cls, value) -> Self:
|
||||
if isinstance(value, cls):
|
||||
return value
|
||||
elif isinstance(value, Message):
|
||||
@@ -169,16 +173,16 @@ class Message(List[TMS], abc.ABC):
|
||||
"""构造消息数组"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __add__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
def __add__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||
result = self.copy()
|
||||
result += other
|
||||
return result
|
||||
|
||||
def __radd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
def __radd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||
result = self.__class__(other)
|
||||
return result + self
|
||||
|
||||
def __iadd__(self: TM, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||
def __iadd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||
if isinstance(other, str):
|
||||
self.extend(self._construct(other))
|
||||
elif isinstance(other, MessageSegment):
|
||||
@@ -186,61 +190,66 @@ class Message(List[TMS], abc.ABC):
|
||||
elif isinstance(other, Iterable):
|
||||
self.extend(other)
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(other)}") # pragma: no cover
|
||||
raise TypeError(f"Unsupported type {type(other)!r}")
|
||||
return self
|
||||
|
||||
@overload
|
||||
def __getitem__(self: TM, __args: str) -> TM:
|
||||
"""
|
||||
def __getitem__(self, args: str) -> Self:
|
||||
"""获取仅包含指定消息段类型的消息
|
||||
|
||||
参数:
|
||||
__args: 消息段类型
|
||||
args: 消息段类型
|
||||
|
||||
返回:
|
||||
所有类型为 `__args` 的消息段
|
||||
所有类型为 `args` 的消息段
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __getitem__(self, __args: Tuple[str, int]) -> TMS:
|
||||
"""
|
||||
def __getitem__(self, args: Tuple[str, int]) -> TMS:
|
||||
"""索引指定类型的消息段
|
||||
|
||||
参数:
|
||||
__args: 消息段类型和索引
|
||||
args: 消息段类型和索引
|
||||
|
||||
返回:
|
||||
类型为 `__args[0]` 的消息段第 `__args[1]` 个
|
||||
类型为 `args[0]` 的消息段第 `args[1]` 个
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __getitem__(self: TM, __args: Tuple[str, slice]) -> TM:
|
||||
"""
|
||||
def __getitem__(self, args: Tuple[str, slice]) -> Self:
|
||||
"""切片指定类型的消息段
|
||||
|
||||
参数:
|
||||
__args: 消息段类型和切片
|
||||
args: 消息段类型和切片
|
||||
|
||||
返回:
|
||||
类型为 `__args[0]` 的消息段切片 `__args[1]`
|
||||
类型为 `args[0]` 的消息段切片 `args[1]`
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __getitem__(self, __args: int) -> TMS:
|
||||
"""
|
||||
def __getitem__(self, args: int) -> TMS:
|
||||
"""索引消息段
|
||||
|
||||
参数:
|
||||
__args: 索引
|
||||
args: 索引
|
||||
|
||||
返回:
|
||||
第 `__args` 个消息段
|
||||
第 `args` 个消息段
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __getitem__(self: TM, __args: slice) -> TM:
|
||||
"""
|
||||
def __getitem__(self, args: slice) -> Self:
|
||||
"""切片消息段
|
||||
|
||||
参数:
|
||||
__args: 切片
|
||||
args: 切片
|
||||
|
||||
返回:
|
||||
消息切片 `__args`
|
||||
消息切片 `args`
|
||||
"""
|
||||
|
||||
def __getitem__(
|
||||
self: TM,
|
||||
self,
|
||||
args: Union[
|
||||
str,
|
||||
Tuple[str, int],
|
||||
@@ -248,7 +257,7 @@ class Message(List[TMS], abc.ABC):
|
||||
int,
|
||||
slice,
|
||||
],
|
||||
) -> Union[TMS, TM]:
|
||||
) -> Union[TMS, Self]:
|
||||
arg1, arg2 = args if isinstance(args, tuple) else (args, None)
|
||||
if isinstance(arg1, int) and arg2 is None:
|
||||
return super().__getitem__(arg1)
|
||||
@@ -263,15 +272,52 @@ class Message(List[TMS], abc.ABC):
|
||||
else:
|
||||
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
||||
|
||||
def index(self, value: Union[TMS, str], *args) -> int:
|
||||
def __contains__(self, value: Union[TMS, str]) -> bool:
|
||||
"""检查消息段是否存在
|
||||
|
||||
参数:
|
||||
value: 消息段或消息段类型
|
||||
返回:
|
||||
消息内是否存在给定消息段或给定类型的消息段
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return bool(next((seg for seg in self if seg.type == value), None))
|
||||
return super().__contains__(value)
|
||||
|
||||
def has(self, value: Union[TMS, str]) -> bool:
|
||||
"""与 {ref}``__contains__` <nonebot.adapters.Message.__contains__>` 相同"""
|
||||
return value in self
|
||||
|
||||
def index(self, value: Union[TMS, str], *args: SupportsIndex) -> int:
|
||||
"""索引消息段
|
||||
|
||||
参数:
|
||||
value: 消息段或者消息段类型
|
||||
arg: start 与 end
|
||||
|
||||
返回:
|
||||
索引 index
|
||||
|
||||
异常:
|
||||
ValueError: 消息段不存在
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
first_segment = next((seg for seg in self if seg.type == value), None)
|
||||
if first_segment is None:
|
||||
raise ValueError(f"Segment with type {value} is not in message")
|
||||
raise ValueError(f"Segment with type {value!r} is not in message")
|
||||
return super().index(first_segment, *args)
|
||||
return super().index(value, *args)
|
||||
|
||||
def get(self: TM, type_: str, count: Optional[int] = None) -> TM:
|
||||
def get(self, type_: str, count: Optional[int] = None) -> Self:
|
||||
"""获取指定类型的消息段
|
||||
|
||||
参数:
|
||||
type_: 消息段类型
|
||||
count: 获取个数
|
||||
|
||||
返回:
|
||||
构建的新消息
|
||||
"""
|
||||
if count is None:
|
||||
return self[type_]
|
||||
|
||||
@@ -286,9 +332,30 @@ class Message(List[TMS], abc.ABC):
|
||||
return filtered
|
||||
|
||||
def count(self, value: Union[TMS, str]) -> int:
|
||||
"""计算指定消息段的个数
|
||||
|
||||
参数:
|
||||
value: 消息段或消息段类型
|
||||
|
||||
返回:
|
||||
个数
|
||||
"""
|
||||
return len(self[value]) if isinstance(value, str) else super().count(value)
|
||||
|
||||
def append(self: TM, obj: Union[str, TMS]) -> TM:
|
||||
def only(self, value: Union[TMS, str]) -> bool:
|
||||
"""检查消息中是否仅包含指定消息段
|
||||
|
||||
参数:
|
||||
value: 指定消息段或消息段类型
|
||||
|
||||
返回:
|
||||
是否仅包含指定消息段
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return all(seg.type == value for seg in self)
|
||||
return all(seg == value for seg in self)
|
||||
|
||||
def append(self, obj: Union[str, TMS]) -> Self:
|
||||
"""添加一个消息段到消息数组末尾。
|
||||
|
||||
参数:
|
||||
@@ -302,7 +369,7 @@ class Message(List[TMS], abc.ABC):
|
||||
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
|
||||
return self
|
||||
|
||||
def extend(self: TM, obj: Union[TM, Iterable[TMS]]) -> TM:
|
||||
def extend(self, obj: Union[Self, Iterable[TMS]]) -> Self:
|
||||
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
||||
|
||||
参数:
|
||||
@@ -312,18 +379,52 @@ class Message(List[TMS], abc.ABC):
|
||||
self.append(segment)
|
||||
return self
|
||||
|
||||
def copy(self: TM) -> TM:
|
||||
def join(self, iterable: Iterable[Union[TMS, Self]]) -> Self:
|
||||
"""将多个消息连接并将自身作为分割
|
||||
|
||||
参数:
|
||||
iterable: 要连接的消息
|
||||
|
||||
返回:
|
||||
连接后的消息
|
||||
"""
|
||||
ret = self.__class__()
|
||||
for index, msg in enumerate(iterable):
|
||||
if index != 0:
|
||||
ret.extend(self)
|
||||
if isinstance(msg, MessageSegment):
|
||||
ret.append(msg.copy())
|
||||
else:
|
||||
ret.extend(msg.copy())
|
||||
return ret
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""深拷贝消息"""
|
||||
return deepcopy(self)
|
||||
|
||||
def include(self, *types: str) -> Self:
|
||||
"""过滤消息
|
||||
|
||||
参数:
|
||||
types: 包含的消息段类型
|
||||
|
||||
返回:
|
||||
新构造的消息
|
||||
"""
|
||||
return self.__class__(seg for seg in self if seg.type in types)
|
||||
|
||||
def exclude(self, *types: str) -> Self:
|
||||
"""过滤消息
|
||||
|
||||
参数:
|
||||
types: 不包含的消息段类型
|
||||
|
||||
返回:
|
||||
新构造的消息
|
||||
"""
|
||||
return self.__class__(seg for seg in self if seg.type not in types)
|
||||
|
||||
def extract_plain_text(self) -> str:
|
||||
"""提取消息内纯文本消息"""
|
||||
|
||||
return "".join(str(seg) for seg in self if seg.is_text())
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"MessageSegment.__str__": True,
|
||||
"MessageSegment.__add__": True,
|
||||
"Message.__getitem__": True,
|
||||
"Message._construct": True,
|
||||
}
|
||||
|
@@ -49,13 +49,16 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def __init__( # type:ignore
|
||||
self, template, factory=str
|
||||
) -> None: # TODO: fix type hint here
|
||||
self.template: TF = template
|
||||
self.factory: Type[TF] = factory
|
||||
def __init__(
|
||||
self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
|
||||
) -> None:
|
||||
self.template: TF = template # type: ignore
|
||||
self.factory: Type[TF] = factory # type: ignore
|
||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
||||
|
||||
def add_format_spec(
|
||||
self, spec: FormatSpecFunc_T, name: Optional[str] = None
|
||||
) -> FormatSpecFunc_T:
|
||||
@@ -95,7 +98,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
else:
|
||||
raise TypeError("template must be a string or instance of Message!")
|
||||
|
||||
self.check_unused_args(list(used_args), args, kwargs)
|
||||
self.check_unused_args(used_args, args, kwargs)
|
||||
return cast(TF, full_message)
|
||||
|
||||
def vformat(
|
||||
@@ -116,10 +119,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> Tuple[TF, int]:
|
||||
results: List[Any] = [self.factory()]
|
||||
|
||||
for (literal_text, field_name, format_spec, conversion) in self.parse(
|
||||
for literal_text, field_name, format_spec, conversion in self.parse(
|
||||
format_string
|
||||
):
|
||||
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
results.append(literal_text)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
from nonebot.log import logger
|
||||
@@ -8,8 +8,12 @@ from nonebot.config import Env, Config
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
|
||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_BotConnectionHook,
|
||||
T_BotDisconnectionHook,
|
||||
)
|
||||
|
||||
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
@@ -40,12 +44,18 @@ class Driver(abc.ABC):
|
||||
"""环境名称"""
|
||||
self.config: Config = config
|
||||
"""全局配置对象"""
|
||||
self._clients: Dict[str, "Bot"] = {}
|
||||
self._bots: Dict[str, "Bot"] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Driver(type={self.type!r}, "
|
||||
f"adapters={len(self._adapters)}, bots={len(self._bots)})"
|
||||
)
|
||||
|
||||
@property
|
||||
def bots(self) -> Dict[str, "Bot"]:
|
||||
"""获取当前所有已连接的 Bot"""
|
||||
return self._clients
|
||||
return self._bots
|
||||
|
||||
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
|
||||
"""注册一个协议适配器
|
||||
@@ -124,48 +134,52 @@ class Driver(abc.ABC):
|
||||
|
||||
def _bot_connect(self, bot: "Bot") -> None:
|
||||
"""在连接成功后,调用该函数来注册 bot 对象"""
|
||||
if bot.self_id in self._clients:
|
||||
if bot.self_id in self._bots:
|
||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||
self._clients[bot.self_id] = bot
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_connection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
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>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_connection_hook
|
||||
]:
|
||||
try:
|
||||
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>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||
if bot.self_id in self._clients:
|
||||
del self._clients[bot.self_id]
|
||||
if bot.self_id in self._bots:
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_disconnection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_disconnection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
@@ -233,13 +247,11 @@ def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Dr
|
||||
if not mixins:
|
||||
return driver
|
||||
|
||||
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
|
||||
)
|
||||
def type_(self: ForwardDriver) -> str:
|
||||
return (
|
||||
driver.type.__get__(self)
|
||||
+ "+"
|
||||
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
|
||||
)
|
||||
|
||||
return CombinedDriver
|
||||
return type("CombinedDriver", (*mixins, driver, ForwardDriver), {"type": property(type_)}) # type: ignore
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
@@ -105,12 +106,9 @@ class Request:
|
||||
self.url: URL = url
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# cookies
|
||||
self.cookies = Cookies(cookies)
|
||||
|
||||
@@ -131,9 +129,7 @@ class Request:
|
||||
self.files.append((name, file_info)) # type: ignore
|
||||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
url = str(self.url)
|
||||
return f"<{class_name}({self.method!r}, {url!r})>"
|
||||
return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')"
|
||||
|
||||
|
||||
class Response:
|
||||
@@ -149,24 +145,27 @@ class Response:
|
||||
self.status_code: int = status_code
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# body
|
||||
self.content: ContentTypes = content
|
||||
|
||||
# request
|
||||
self.request: Optional[Request] = request
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
|
||||
|
||||
|
||||
class WebSocket(abc.ABC):
|
||||
def __init__(self, *, request: Request):
|
||||
# request
|
||||
self.request: Request = request
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}('{self.request.url!s}')"
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def closed(self) -> bool:
|
||||
@@ -304,6 +303,11 @@ class Cookies(MutableMapping):
|
||||
for cookie in cookies.jar:
|
||||
self.jar.set_cookie(cookie)
|
||||
|
||||
def as_header(self, request: Request) -> Dict[str, str]:
|
||||
urllib_request = self._CookieCompatRequest(request)
|
||||
self.jar.add_cookie_header(urllib_request)
|
||||
return urllib_request.added_headers
|
||||
|
||||
def __setitem__(self, name: str, value: str) -> None:
|
||||
return self.set(name, value)
|
||||
|
||||
@@ -320,17 +324,28 @@ class Cookies(MutableMapping):
|
||||
return len(self.jar)
|
||||
|
||||
def __iter__(self) -> Iterator[Cookie]:
|
||||
return (cookie for cookie in self.jar)
|
||||
return iter(self.jar)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
cookies_repr = ", ".join(
|
||||
[
|
||||
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
|
||||
for cookie in self.jar
|
||||
]
|
||||
f"Cookie({cookie.name}={cookie.value} for {cookie.domain})"
|
||||
for cookie in self.jar
|
||||
)
|
||||
return f"{self.__class__.__name__}({cookies_repr})"
|
||||
|
||||
return f"<Cookies [{cookies_repr}]>"
|
||||
class _CookieCompatRequest(urllib.request.Request):
|
||||
def __init__(self, request: Request) -> None:
|
||||
super().__init__(
|
||||
url=str(request.url),
|
||||
headers=dict(request.headers),
|
||||
method=request.method,
|
||||
)
|
||||
self.request = request
|
||||
self.added_headers: Dict[str, str] = {}
|
||||
|
||||
def add_unredirected_header(self, key: str, value: str) -> None:
|
||||
super().add_unredirected_header(key, value)
|
||||
self.added_headers[key] = value
|
||||
|
||||
|
||||
@dataclass
|
||||
|
11
nonebot/internal/matcher/__init__.py
Normal file
11
nonebot/internal/matcher/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .manager import MatcherManager as MatcherManager
|
||||
from .provider import MatcherProvider as MatcherProvider
|
||||
from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
matchers = MatcherManager()
|
||||
|
||||
from .matcher import Matcher as Matcher
|
||||
from .matcher import current_bot as current_bot
|
||||
from .matcher import current_event as current_event
|
||||
from .matcher import current_handler as current_handler
|
||||
from .matcher import current_matcher as current_matcher
|
104
nonebot/internal/matcher/manager.py
Normal file
104
nonebot/internal/matcher/manager.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Iterator,
|
||||
KeysView,
|
||||
Optional,
|
||||
ItemsView,
|
||||
ValuesView,
|
||||
MutableMapping,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MatcherManager(MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器管理器
|
||||
|
||||
实现了常用字典操作,用于管理事件响应器。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MatcherManager(provider={self.provider!r})"
|
||||
|
||||
def __contains__(self, o: object) -> bool:
|
||||
return o in self.provider
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
return iter(self.provider)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.provider)
|
||||
|
||||
def __getitem__(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider[key]
|
||||
|
||||
def __setitem__(self, key: int, value: List[Type["Matcher"]]) -> None:
|
||||
self.provider[key] = value
|
||||
|
||||
def __delitem__(self, key: int) -> None:
|
||||
del self.provider[key]
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, MatcherManager) and self.provider == other.provider
|
||||
|
||||
def keys(self) -> KeysView[int]:
|
||||
return self.provider.keys()
|
||||
|
||||
def values(self) -> ValuesView[List[Type["Matcher"]]]:
|
||||
return self.provider.values()
|
||||
|
||||
def items(self) -> ItemsView[int, List[Type["Matcher"]]]:
|
||||
return self.provider.items()
|
||||
|
||||
@overload
|
||||
def get(self, key: int) -> Optional[List[Type["Matcher"]]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get(self, key: int, default: T) -> Union[List[Type["Matcher"]], T]:
|
||||
...
|
||||
|
||||
def get(
|
||||
self, key: int, default: Optional[T] = None
|
||||
) -> Optional[Union[List[Type["Matcher"]], T]]:
|
||||
return self.provider.get(key, default)
|
||||
|
||||
def pop(self, key: int) -> List[Type["Matcher"]]:
|
||||
return self.provider.pop(key)
|
||||
|
||||
def popitem(self) -> Tuple[int, List[Type["Matcher"]]]:
|
||||
return self.provider.popitem()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.provider.clear()
|
||||
|
||||
def update(self, __m: MutableMapping[int, List[Type["Matcher"]]]) -> None:
|
||||
self.provider.update(__m)
|
||||
|
||||
def setdefault(
|
||||
self, key: int, default: List[Type["Matcher"]]
|
||||
) -> List[Type["Matcher"]]:
|
||||
return self.provider.setdefault(key, default)
|
||||
|
||||
def set_provider(self, provider_class: Type[MatcherProvider]) -> None:
|
||||
"""设置事件响应器存储器
|
||||
|
||||
参数:
|
||||
provider_class: 事件响应器存储器类
|
||||
"""
|
||||
self.provider = provider_class(self.provider)
|
@@ -1,23 +1,34 @@
|
||||
from types import ModuleType
|
||||
from contextvars import ContextVar
|
||||
from collections import defaultdict
|
||||
from contextlib import AsyncExitStack
|
||||
from typing_extensions import Self
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
NoReturn,
|
||||
Optional,
|
||||
overload,
|
||||
)
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.internal.permission import User, Permission
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
Message,
|
||||
MessageSegment,
|
||||
MessageTemplate,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
ARG_KEY,
|
||||
RECEIVE_KEY,
|
||||
@@ -34,18 +45,13 @@ from nonebot.typing import (
|
||||
T_PermissionUpdater,
|
||||
)
|
||||
from nonebot.exception import (
|
||||
TypeMisMatch,
|
||||
PausedException,
|
||||
StopPropagation,
|
||||
SkippedException,
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
)
|
||||
|
||||
from .rule import Rule
|
||||
from .permission import USER, Permission
|
||||
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
|
||||
from .params import (
|
||||
from nonebot.internal.params import (
|
||||
Depends,
|
||||
ArgParam,
|
||||
BotParam,
|
||||
@@ -56,13 +62,13 @@ from .params import (
|
||||
MatcherParam,
|
||||
)
|
||||
|
||||
from . import matchers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
|
||||
"""用于存储当前所有的事件响应器"""
|
||||
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
||||
current_event: ContextVar[Event] = ContextVar("current_event")
|
||||
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||
@@ -71,68 +77,55 @@ current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
||||
|
||||
class MatcherMeta(type):
|
||||
if TYPE_CHECKING:
|
||||
module: Optional[str]
|
||||
plugin_name: Optional[str]
|
||||
module_name: Optional[str]
|
||||
module_prefix: Optional[str]
|
||||
type: str
|
||||
rule: Rule
|
||||
permission: Permission
|
||||
handlers: List[T_Handler]
|
||||
priority: int
|
||||
block: bool
|
||||
temp: bool
|
||||
expire_time: Optional[datetime]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Matcher from {self.module_name or 'unknown'}, "
|
||||
f"type={self.type}, priority={self.priority}, "
|
||||
f"temp={self.temp}>"
|
||||
f"{self.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
|
||||
class Matcher(metaclass=MatcherMeta):
|
||||
"""事件响应器类"""
|
||||
|
||||
plugin: Optional["Plugin"] = None
|
||||
plugin: ClassVar[Optional["Plugin"]] = None
|
||||
"""事件响应器所在插件"""
|
||||
module: Optional[ModuleType] = None
|
||||
module: ClassVar[Optional[ModuleType]] = None
|
||||
"""事件响应器所在插件模块"""
|
||||
plugin_name: Optional[str] = None
|
||||
plugin_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在插件名"""
|
||||
module_name: Optional[str] = None
|
||||
module_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在点分割插件模块路径"""
|
||||
|
||||
type: str = ""
|
||||
type: ClassVar[str] = ""
|
||||
"""事件响应器类型"""
|
||||
rule: Rule = Rule()
|
||||
rule: ClassVar[Rule] = Rule()
|
||||
"""事件响应器匹配规则"""
|
||||
permission: Permission = Permission()
|
||||
permission: ClassVar[Permission] = Permission()
|
||||
"""事件响应器触发权限"""
|
||||
handlers: List[Dependent[Any]] = []
|
||||
"""事件响应器拥有的事件处理函数列表"""
|
||||
priority: int = 1
|
||||
priority: ClassVar[int] = 1
|
||||
"""事件响应器优先级"""
|
||||
block: bool = False
|
||||
"""事件响应器是否阻止事件传播"""
|
||||
temp: bool = False
|
||||
temp: ClassVar[bool] = False
|
||||
"""事件响应器是否为临时"""
|
||||
expire_time: Optional[datetime] = None
|
||||
expire_time: ClassVar[Optional[datetime]] = None
|
||||
"""事件响应器过期时间点"""
|
||||
|
||||
_default_state: T_State = {}
|
||||
_default_state: ClassVar[T_State] = {}
|
||||
"""事件响应器默认状态"""
|
||||
|
||||
_default_type_updater: Optional[Dependent[str]] = None
|
||||
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
|
||||
"""事件响应器类型更新函数"""
|
||||
_default_permission_updater: Optional[Dependent[Permission]] = None
|
||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||
"""事件响应器权限更新函数"""
|
||||
|
||||
HANDLER_PARAM_TYPES = [
|
||||
HANDLER_PARAM_TYPES = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
@@ -140,7 +133,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = self.handlers.copy()
|
||||
@@ -148,13 +141,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
|
||||
f"priority={self.priority}, temp={self.temp}>"
|
||||
f"{self.__class__.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
@@ -174,7 +165,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater: Optional[
|
||||
Union[T_PermissionUpdater, Dependent[Permission]]
|
||||
] = None,
|
||||
) -> Type["Matcher"]:
|
||||
) -> Type[Self]:
|
||||
"""
|
||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||
|
||||
@@ -195,8 +186,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
Type[Matcher]: 新的事件响应器类
|
||||
"""
|
||||
NewMatcher = type(
|
||||
"Matcher",
|
||||
(Matcher,),
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
"plugin": plugin,
|
||||
"module": module,
|
||||
@@ -218,27 +209,35 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"temp": temp,
|
||||
"expire_time": (
|
||||
expire_time
|
||||
if isinstance(expire_time, datetime)
|
||||
else expire_time and datetime.now() + expire_time
|
||||
and (
|
||||
expire_time
|
||||
if isinstance(expire_time, datetime)
|
||||
else datetime.now() + expire_time
|
||||
)
|
||||
),
|
||||
"priority": priority,
|
||||
"block": block,
|
||||
"_default_state": default_state or {},
|
||||
"_default_type_updater": (
|
||||
default_type_updater
|
||||
if isinstance(default_type_updater, Dependent)
|
||||
else default_type_updater
|
||||
and Dependent[str].parse(
|
||||
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES
|
||||
and (
|
||||
default_type_updater
|
||||
if isinstance(default_type_updater, Dependent)
|
||||
else Dependent[str].parse(
|
||||
call=default_type_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
)
|
||||
),
|
||||
"_default_permission_updater": (
|
||||
default_permission_updater
|
||||
if isinstance(default_permission_updater, Dependent)
|
||||
else default_permission_updater
|
||||
and Dependent[Permission].parse(
|
||||
call=default_permission_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
and (
|
||||
default_permission_updater
|
||||
if isinstance(default_permission_updater, Dependent)
|
||||
else Dependent[Permission].parse(
|
||||
call=default_permission_updater,
|
||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -250,6 +249,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
return NewMatcher
|
||||
|
||||
@classmethod
|
||||
def destroy(cls) -> None:
|
||||
"""销毁当前的事件响应器"""
|
||||
matchers[cls.priority].remove(cls)
|
||||
|
||||
@classmethod
|
||||
async def check_perm(
|
||||
cls,
|
||||
@@ -326,7 +330,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def append_handler(
|
||||
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
|
||||
cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Dependent[Any]:
|
||||
handler_ = Dependent[Any].parse(
|
||||
call=handler,
|
||||
@@ -338,7 +342,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def handle(
|
||||
cls, parameterless: Optional[List[Any]] = None
|
||||
cls, parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来向事件响应器直接添加一个处理函数
|
||||
|
||||
@@ -354,7 +358,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
@classmethod
|
||||
def receive(
|
||||
cls, id: str = "", parameterless: Optional[List[Any]] = None
|
||||
cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
||||
|
||||
@@ -372,14 +376,20 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject()
|
||||
|
||||
_parameterless = [Depends(_receive), *(parameterless or [])]
|
||||
_parameterless = (Depends(_receive), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
for depend in reversed(_parameterless):
|
||||
func_handler.prepend_parameterless(depend)
|
||||
new_handler = Dependent(
|
||||
call=func_handler.call,
|
||||
params=func_handler.params,
|
||||
parameterless=Dependent.parse_parameterless(
|
||||
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
|
||||
)
|
||||
+ func_handler.parameterless,
|
||||
)
|
||||
cls.handlers[-1] = new_handler
|
||||
else:
|
||||
cls.append_handler(func, parameterless=_parameterless)
|
||||
|
||||
@@ -392,7 +402,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
cls,
|
||||
key: str,
|
||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||
parameterless: Optional[List[Any]] = None,
|
||||
parameterless: Optional[Iterable[Any]] = None,
|
||||
) -> Callable[[T_Handler], T_Handler]:
|
||||
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
||||
|
||||
@@ -413,17 +423,20 @@ class Matcher(metaclass=MatcherMeta):
|
||||
return
|
||||
await matcher.reject(prompt)
|
||||
|
||||
_parameterless = [
|
||||
Depends(_key_getter),
|
||||
*(parameterless or []),
|
||||
]
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
for depend in reversed(_parameterless):
|
||||
func_handler.prepend_parameterless(depend)
|
||||
new_handler = Dependent(
|
||||
call=func_handler.call,
|
||||
params=func_handler.params,
|
||||
parameterless=Dependent.parse_parameterless(
|
||||
tuple(_parameterless), cls.HANDLER_PARAM_TYPES
|
||||
)
|
||||
+ func_handler.parameterless,
|
||||
)
|
||||
cls.handlers[-1] = new_handler
|
||||
else:
|
||||
cls.append_handler(func, parameterless=_parameterless)
|
||||
|
||||
@@ -551,7 +564,17 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""
|
||||
raise SkippedException
|
||||
|
||||
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
|
||||
@overload
|
||||
def get_receive(self, id: str) -> Union[Event, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_receive(self, id: str, default: T) -> Union[Event, T]:
|
||||
...
|
||||
|
||||
def get_receive(
|
||||
self, id: str, default: Optional[T] = None
|
||||
) -> Optional[Union[Event, T]]:
|
||||
"""获取一个 `receive` 事件
|
||||
|
||||
如果没有找到对应的事件,返回 `default` 值
|
||||
@@ -563,14 +586,34 @@ class Matcher(metaclass=MatcherMeta):
|
||||
self.state[RECEIVE_KEY.format(id=id)] = event
|
||||
self.state[LAST_RECEIVE_KEY] = event
|
||||
|
||||
def get_last_receive(self, default: T = None) -> Union[Event, T]:
|
||||
@overload
|
||||
def get_last_receive(self) -> Union[Event, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_last_receive(self, default: T) -> Union[Event, T]:
|
||||
...
|
||||
|
||||
def get_last_receive(
|
||||
self, default: Optional[T] = None
|
||||
) -> Optional[Union[Event, T]]:
|
||||
"""获取最近一次 `receive` 事件
|
||||
|
||||
如果没有事件,返回 `default` 值
|
||||
"""
|
||||
return self.state.get(LAST_RECEIVE_KEY, default)
|
||||
|
||||
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
|
||||
@overload
|
||||
def get_arg(self, key: str) -> Union[Message, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_arg(self, key: str, default: T) -> Union[Message, T]:
|
||||
...
|
||||
|
||||
def get_arg(
|
||||
self, key: str, default: Optional[T] = None
|
||||
) -> Optional[Union[Message, T]]:
|
||||
"""获取一个 `got` 消息
|
||||
|
||||
如果没有找到对应的消息,返回 `default` 值
|
||||
@@ -587,24 +630,59 @@ class Matcher(metaclass=MatcherMeta):
|
||||
else:
|
||||
self.state[REJECT_TARGET] = target
|
||||
|
||||
def get_target(self, default: T = None) -> Union[str, T]:
|
||||
@overload
|
||||
def get_target(self) -> Union[str, None]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get_target(self, default: T) -> Union[str, T]:
|
||||
...
|
||||
|
||||
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
|
||||
return self.state.get(REJECT_TARGET, default)
|
||||
|
||||
def stop_propagation(self):
|
||||
"""阻止事件传播"""
|
||||
self.block = True
|
||||
|
||||
async def update_type(self, bot: Bot, event: Event) -> str:
|
||||
async def update_type(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> str:
|
||||
updater = self.__class__._default_type_updater
|
||||
if not updater:
|
||||
return "message"
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
return (
|
||||
await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
if updater
|
||||
else "message"
|
||||
)
|
||||
|
||||
async def update_permission(self, bot: Bot, event: Event) -> Permission:
|
||||
updater = self.__class__._default_permission_updater
|
||||
if not updater:
|
||||
return USER(event.get_session_id(), perm=self.permission)
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
async def update_permission(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> Permission:
|
||||
if updater := self.__class__._default_permission_updater:
|
||||
return await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
return Permission(User.from_event(event, perm=self.permission))
|
||||
|
||||
async def resolve_reject(self):
|
||||
handler = current_handler.get()
|
||||
@@ -612,6 +690,18 @@ class Matcher(metaclass=MatcherMeta):
|
||||
if REJECT_CACHE_TARGET in self.state:
|
||||
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
|
||||
|
||||
@contextmanager
|
||||
def ensure_context(self, bot: Bot, event: Event):
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
|
||||
async def simple_run(
|
||||
self,
|
||||
bot: Bot,
|
||||
@@ -621,43 +711,34 @@ class Matcher(metaclass=MatcherMeta):
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
):
|
||||
logger.trace(
|
||||
f"Matcher {self} run with incoming args: "
|
||||
f"bot={bot}, event={event}, state={state}"
|
||||
f"{self} run with incoming args: "
|
||||
f"bot={bot}, event={event!r}, state={state!r}"
|
||||
)
|
||||
b_t = current_bot.set(bot)
|
||||
e_t = current_event.set(event)
|
||||
m_t = current_matcher.set(self)
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except TypeMisMatch as e:
|
||||
logger.debug(
|
||||
f"Handler {handler} param {e.param.name} value {e.value} "
|
||||
f"mismatch type {e.param._type_display()}, skipped"
|
||||
)
|
||||
except SkippedException as e:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"Matcher {self} running complete")
|
||||
current_bot.reset(b_t)
|
||||
current_event.reset(e_t)
|
||||
current_matcher.reset(m_t)
|
||||
with self.ensure_context(bot, event):
|
||||
try:
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
while self.handlers:
|
||||
handler = self.handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
|
||||
# 运行handlers
|
||||
async def run(
|
||||
@@ -673,10 +754,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
except RejectedException:
|
||||
await self.resolve_reject()
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
@@ -692,10 +775,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except PausedException:
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
@@ -712,14 +797,3 @@ class Matcher(metaclass=MatcherMeta):
|
||||
)
|
||||
except FinishedException:
|
||||
pass
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"MatcherMeta": False,
|
||||
"Matcher.get_target": False,
|
||||
"Matcher.set_target": False,
|
||||
"Matcher.update_type": False,
|
||||
"Matcher.update_permission": False,
|
||||
"Matcher.resolve_reject": False,
|
||||
"Matcher.simple_run": False,
|
||||
}
|
27
nonebot/internal/matcher/provider.py
Normal file
27
nonebot/internal/matcher/provider.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import abc
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, List, Type, Mapping, MutableMapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .matcher import Matcher
|
||||
|
||||
|
||||
class MatcherProvider(abc.ABC, MutableMapping[int, List[Type["Matcher"]]]):
|
||||
"""事件响应器存储器基类
|
||||
|
||||
参数:
|
||||
matchers: 当前存储器中已有的事件响应器
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _DictProvider(defaultdict, MatcherProvider):
|
||||
def __init__(self, matchers: Mapping[int, List[Type["Matcher"]]]):
|
||||
super().__init__(list, matchers)
|
||||
|
||||
|
||||
DEFAULT_PROVIDER_CLASS = _DictProvider
|
||||
"""默认存储器类型"""
|
@@ -1,14 +1,12 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import warnings
|
||||
from typing_extensions import Literal
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
||||
from typing_extensions import Annotated
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
|
||||
|
||||
from pydantic.typing import get_args, get_origin
|
||||
from pydantic.fields import Required, Undefined, ModelField
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
||||
from nonebot.typing import T_State, T_Handler, T_DependencyCache
|
||||
@@ -40,7 +38,7 @@ class DependsInner:
|
||||
def __repr__(self) -> str:
|
||||
dep = get_name(self.dependency)
|
||||
cache = "" if self.use_cache else ", use_cache=False"
|
||||
return f"{self.__class__.__name__}({dep}{cache})"
|
||||
return f"DependsInner({dep}{cache})"
|
||||
|
||||
|
||||
def Depends(
|
||||
@@ -73,40 +71,55 @@ def Depends(
|
||||
|
||||
|
||||
class DependParam(Param):
|
||||
"""子依赖参数"""
|
||||
"""子依赖注入参数。
|
||||
|
||||
本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。
|
||||
|
||||
本注入应该具有最高优先级,因此应该在其他参数之前检查。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Depends({self.extra['dependent']})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls,
|
||||
dependent: Dependent,
|
||||
name: str,
|
||||
param: inspect.Parameter,
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DependParam"]:
|
||||
if isinstance(param.default, DependsInner):
|
||||
dependency: T_Handler
|
||||
if param.default.dependency is None:
|
||||
assert param.annotation is not param.empty, "Dependency cannot be empty"
|
||||
dependency = param.annotation
|
||||
else:
|
||||
dependency = param.default.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=dependent.allow_types,
|
||||
)
|
||||
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
|
||||
sub_dependent.pre_checkers.clear()
|
||||
return cls(
|
||||
Required, use_cache=param.default.use_cache, dependent=sub_dependent
|
||||
type_annotation, depends_inner = param.annotation, None
|
||||
if get_origin(param.annotation) is Annotated:
|
||||
type_annotation, *extra_args = get_args(param.annotation)
|
||||
depends_inner = next(
|
||||
(x for x in extra_args if isinstance(x, DependsInner)), None
|
||||
)
|
||||
|
||||
depends_inner = (
|
||||
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||
)
|
||||
if depends_inner is None:
|
||||
return
|
||||
|
||||
dependency: T_Handler
|
||||
if depends_inner.dependency is None:
|
||||
assert (
|
||||
type_annotation is not inspect.Signature.empty
|
||||
), "Dependency cannot be empty"
|
||||
dependency = type_annotation
|
||||
else:
|
||||
dependency = depends_inner.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
return cls(Required, use_cache=depends_inner.use_cache, dependent=sub_dependent)
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, dependent: "Dependent", value: Any
|
||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["Param"]:
|
||||
if isinstance(value, DependsInner):
|
||||
assert value.dependency, "Dependency cannot be empty"
|
||||
dependent = Dependent[Any].parse(
|
||||
call=value.dependency, allow_types=dependent.allow_types
|
||||
call=value.dependency, allow_types=allow_types
|
||||
)
|
||||
return cls(Required, use_cache=value.use_cache, dependent=dependent)
|
||||
|
||||
@@ -120,8 +133,7 @@ class DependParam(Param):
|
||||
dependency_cache = {} if dependency_cache is None else dependency_cache
|
||||
|
||||
sub_dependent: Dependent = self.extra["dependent"]
|
||||
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
|
||||
call = sub_dependent.call
|
||||
call = cast(Callable[..., Any], sub_dependent.call)
|
||||
|
||||
# solve sub dependency with current cache
|
||||
sub_values = await sub_dependent.solve(
|
||||
@@ -133,7 +145,7 @@ class DependParam(Param):
|
||||
# run dependency function
|
||||
task: asyncio.Task[Any]
|
||||
if use_cache and call in dependency_cache:
|
||||
solved = await dependency_cache[call]
|
||||
return await dependency_cache[call]
|
||||
elif is_gen_callable(call) or is_async_gen_callable(call):
|
||||
assert isinstance(
|
||||
stack, AsyncExitStack
|
||||
@@ -144,152 +156,170 @@ class DependParam(Param):
|
||||
cm = asynccontextmanager(call)(**sub_values)
|
||||
task = asyncio.create_task(stack.enter_async_context(cm))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
elif is_coroutine_callable(call):
|
||||
task = asyncio.create_task(call(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
else:
|
||||
task = asyncio.create_task(run_sync(call)(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
solved = await task
|
||||
return await task
|
||||
|
||||
return solved
|
||||
|
||||
|
||||
class _BotChecker(Param):
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, bot)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"Bot type {type(bot)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
# run sub dependent pre-checkers
|
||||
sub_dependent: Dependent = self.extra["dependent"]
|
||||
await sub_dependent.check(**kwargs)
|
||||
|
||||
|
||||
class BotParam(Param):
|
||||
"""{ref}`nonebot.adapters.Bot` 参数"""
|
||||
"""{ref}`nonebot.adapters.Bot` 注入参数。
|
||||
|
||||
本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Bot` 及其子类或 `None` 的参数。
|
||||
|
||||
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"BotParam("
|
||||
+ (
|
||||
repr(cast(ModelField, checker).type_)
|
||||
if (checker := self.extra.get("checker"))
|
||||
else ""
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["BotParam"]:
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
if param.default == param.empty:
|
||||
if generic_check_issubclass(param.annotation, Bot):
|
||||
if param.annotation is not Bot:
|
||||
dependent.pre_checkers.append(
|
||||
_BotChecker(
|
||||
Required,
|
||||
field=ModelField(
|
||||
name=name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "bot":
|
||||
return cls(Required)
|
||||
# param type is Bot(s) or subclass(es) of Bot or None
|
||||
if generic_check_issubclass(param.annotation, Bot):
|
||||
checker: Optional[ModelField] = None
|
||||
if param.annotation is not Bot:
|
||||
checker = ModelField(
|
||||
name=param.name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
)
|
||||
return cls(Required, checker=checker)
|
||||
# legacy: param is named "bot" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "bot":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
return bot
|
||||
|
||||
|
||||
class _EventChecker(Param):
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, event)
|
||||
except TypeMisMatch:
|
||||
logger.debug(
|
||||
f"Event type {type(event)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
async def _check(self, bot: "Bot", **kwargs: Any) -> None:
|
||||
if checker := self.extra.get("checker"):
|
||||
check_field_type(checker, bot)
|
||||
|
||||
|
||||
class EventParam(Param):
|
||||
"""{ref}`nonebot.adapters.Event` 参数"""
|
||||
"""{ref}`nonebot.adapters.Event` 注入参数
|
||||
|
||||
本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Event` 及其子类或 `None` 的参数。
|
||||
|
||||
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"EventParam("
|
||||
+ (
|
||||
repr(cast(ModelField, checker).type_)
|
||||
if (checker := self.extra.get("checker"))
|
||||
else ""
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["EventParam"]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
if param.default == param.empty:
|
||||
if generic_check_issubclass(param.annotation, Event):
|
||||
if param.annotation is not Event:
|
||||
dependent.pre_checkers.append(
|
||||
_EventChecker(
|
||||
Required,
|
||||
field=ModelField(
|
||||
name=name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "event":
|
||||
return cls(Required)
|
||||
# param type is Event(s) or subclass(es) of Event or None
|
||||
if generic_check_issubclass(param.annotation, Event):
|
||||
checker: Optional[ModelField] = None
|
||||
if param.annotation is not Event:
|
||||
checker = ModelField(
|
||||
name=param.name,
|
||||
type_=param.annotation,
|
||||
class_validators=None,
|
||||
model_config=CustomConfig,
|
||||
default=None,
|
||||
required=True,
|
||||
)
|
||||
return cls(Required, checker=checker)
|
||||
# legacy: param is named "event" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "event":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
return event
|
||||
|
||||
|
||||
class StateInner(T_State):
|
||||
...
|
||||
|
||||
|
||||
def State() -> T_State:
|
||||
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
|
||||
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
|
||||
return StateInner()
|
||||
async def _check(self, event: "Event", **kwargs: Any) -> Any:
|
||||
if checker := self.extra.get("checker", None):
|
||||
check_field_type(checker, event)
|
||||
|
||||
|
||||
class StateParam(Param):
|
||||
"""事件处理状态参数"""
|
||||
"""事件处理状态注入参数
|
||||
|
||||
本注入解析所有类型为 `T_State` 的参数。
|
||||
|
||||
为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "StateParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["StateParam"]:
|
||||
if isinstance(param.default, StateInner):
|
||||
# param type is T_State
|
||||
if param.annotation is T_State:
|
||||
return cls(Required)
|
||||
# legacy: param is named "state" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "state":
|
||||
return cls(Required)
|
||||
elif param.default == param.empty:
|
||||
if param.annotation is T_State:
|
||||
return cls(Required)
|
||||
elif param.annotation == param.empty and name == "state":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
|
||||
return state
|
||||
|
||||
|
||||
class MatcherParam(Param):
|
||||
"""事件响应器实例参数"""
|
||||
"""事件响应器实例注入参数
|
||||
|
||||
本注入解析所有类型为且仅为 {ref}`nonebot.matcher.Matcher` 及其子类或 `None` 的参数。
|
||||
|
||||
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "MatcherParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["MatcherParam"]:
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
if generic_check_issubclass(param.annotation, Matcher) or (
|
||||
param.annotation == param.empty and name == "matcher"
|
||||
):
|
||||
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||
if generic_check_issubclass(param.annotation, Matcher):
|
||||
return cls(Required)
|
||||
# legacy: param is named "matcher" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "matcher":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
@@ -303,34 +333,49 @@ class ArgInner:
|
||||
self.key = key
|
||||
self.type = type
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
||||
|
||||
|
||||
def Arg(key: Optional[str] = None) -> Any:
|
||||
"""`got` 的 Arg 参数消息"""
|
||||
"""Arg 参数消息"""
|
||||
return ArgInner(key, "message")
|
||||
|
||||
|
||||
def ArgStr(key: Optional[str] = None) -> str:
|
||||
"""`got` 的 Arg 参数消息文本"""
|
||||
"""Arg 参数消息文本"""
|
||||
return ArgInner(key, "str") # type: ignore
|
||||
|
||||
|
||||
def ArgPlainText(key: Optional[str] = None) -> str:
|
||||
"""`got` 的 Arg 参数消息纯文本"""
|
||||
"""Arg 参数消息纯文本"""
|
||||
return ArgInner(key, "plaintext") # type: ignore
|
||||
|
||||
|
||||
class ArgParam(Param):
|
||||
"""`got` 的 Arg 参数"""
|
||||
"""Arg 注入参数
|
||||
|
||||
本注入解析事件响应器操作 `got` 所获取的参数。
|
||||
|
||||
可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数,
|
||||
留空则会根据参数名称获取。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["ArgParam"]:
|
||||
if isinstance(param.default, ArgInner):
|
||||
return cls(Required, key=param.default.key or name, type=param.default.type)
|
||||
return cls(
|
||||
Required, key=param.default.key or param.name, type=param.default.type
|
||||
)
|
||||
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
message = matcher.get_arg(self.extra["key"])
|
||||
key: str = self.extra["key"]
|
||||
message = matcher.get_arg(key)
|
||||
if message is None:
|
||||
return message
|
||||
if self.extra["type"] == "message":
|
||||
@@ -342,15 +387,25 @@ class ArgParam(Param):
|
||||
|
||||
|
||||
class ExceptionParam(Param):
|
||||
"""`run_postprocessor` 的异常参数"""
|
||||
"""{ref}`nonebot.message.run_postprocessor` 的异常注入参数
|
||||
|
||||
本注入解析所有类型为 `Exception` 或 `None` 的参数。
|
||||
|
||||
为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "ExceptionParam()"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["ExceptionParam"]:
|
||||
if generic_check_issubclass(param.annotation, Exception) or (
|
||||
param.annotation == param.empty and name == "exception"
|
||||
):
|
||||
# param type is Exception(s) or subclass(es) of Exception or None
|
||||
if generic_check_issubclass(param.annotation, Exception):
|
||||
return cls(Required)
|
||||
# legacy: param is named "exception" and has no type annotation
|
||||
elif param.annotation == param.empty and param.name == "exception":
|
||||
return cls(Required)
|
||||
|
||||
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
||||
@@ -358,11 +413,19 @@ class ExceptionParam(Param):
|
||||
|
||||
|
||||
class DefaultParam(Param):
|
||||
"""默认值参数"""
|
||||
"""默认值注入参数
|
||||
|
||||
本注入解析所有剩余未能解析且具有默认值的参数。
|
||||
|
||||
本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"DefaultParam(default={self.default!r})"
|
||||
|
||||
@classmethod
|
||||
def _check_param(
|
||||
cls, dependent: Dependent, name: str, param: inspect.Parameter
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DefaultParam"]:
|
||||
if param.default != param.empty:
|
||||
return cls(param.default)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
|
||||
from typing import Set, Tuple, Union, NoReturn, Optional
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
@@ -37,16 +38,19 @@ class Permission:
|
||||
]
|
||||
|
||||
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
|
||||
self.checkers: Set[Dependent[bool]] = set(
|
||||
self.checkers: Set[Dependent[bool]] = {
|
||||
checker
|
||||
if isinstance(checker, Dependent)
|
||||
else Dependent[bool].parse(
|
||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||
)
|
||||
for checker in checkers
|
||||
)
|
||||
}
|
||||
"""存储 `PermissionChecker`"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Permission({', '.join(repr(checker) for checker in self.checkers)})"
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
bot: Bot,
|
||||
@@ -54,7 +58,7 @@ class Permission:
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> bool:
|
||||
"""检查是否满足某个权限
|
||||
"""检查是否满足某个权限。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
@@ -106,7 +110,7 @@ class Permission:
|
||||
|
||||
|
||||
class User:
|
||||
"""检查当前事件是否属于指定会话
|
||||
"""检查当前事件是否属于指定会话。
|
||||
|
||||
参数:
|
||||
users: 会话 ID 元组
|
||||
@@ -121,19 +125,63 @@ class User:
|
||||
self.users = users
|
||||
self.perm = perm
|
||||
|
||||
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||
return bool(
|
||||
event.get_session_id() in self.users
|
||||
and (self.perm is None or await self.perm(bot, event))
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"User(users={self.users}"
|
||||
+ (f", permission={self.perm})" if self.perm else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||
try:
|
||||
session = event.get_session_id()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(
|
||||
session in self.users and (self.perm is None or await self.perm(bot, event))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
||||
if len(perm.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(perm.checkers)[0].call, cls
|
||||
):
|
||||
return user_perm.perm
|
||||
return perm
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: Event, perm: Optional[Permission] = None) -> Self:
|
||||
"""从事件中获取会话 ID。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
event: Event 对象
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
|
||||
|
||||
@classmethod
|
||||
def from_permission(cls, *users: str, perm: Optional[Permission] = None) -> Self:
|
||||
"""指定会话与权限。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
users: 会话白名单
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls(users, perm=perm and cls._clean_permission(perm))
|
||||
|
||||
|
||||
def USER(*users: str, perm: Optional[Permission] = None):
|
||||
"""匹配当前事件属于指定会话
|
||||
"""匹配当前事件属于指定会话。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
user: 会话白名单
|
||||
perm: 需要同时满足的权限
|
||||
"""
|
||||
|
||||
return Permission(User(users, perm))
|
||||
return Permission(User.from_permission(*users, perm=perm))
|
||||
|
@@ -37,16 +37,19 @@ class Rule:
|
||||
]
|
||||
|
||||
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
|
||||
self.checkers: Set[Dependent[bool]] = set(
|
||||
self.checkers: Set[Dependent[bool]] = {
|
||||
checker
|
||||
if isinstance(checker, Dependent)
|
||||
else Dependent[bool].parse(
|
||||
call=checker, allow_types=self.HANDLER_PARAM_TYPES
|
||||
)
|
||||
for checker in checkers
|
||||
)
|
||||
}
|
||||
"""存储 `RuleChecker`"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Rule({', '.join(repr(checker) for checker in self.checkers)})"
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
bot: Bot,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||
|
||||
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/tutorial/custom-logger)
|
||||
自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)
|
||||
以及 [`loguru`][loguru] 文档。
|
||||
|
||||
[loguru]: https://github.com/Delgan/loguru
|
||||
@@ -14,16 +14,14 @@ FrontMatter:
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import loguru
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# avoid sphinx autodoc resolve annotation failed
|
||||
# because loguru module do not have `Logger` class actually
|
||||
from loguru import Logger
|
||||
|
||||
from nonebot.plugin import Plugin
|
||||
from loguru import Logger, Record
|
||||
|
||||
# logger = logging.getLogger("nonebot")
|
||||
logger: "Logger" = loguru.logger
|
||||
@@ -47,26 +45,10 @@ logger: "Logger" = loguru.logger
|
||||
# logger.addHandler(default_handler)
|
||||
|
||||
|
||||
class Filter:
|
||||
def __init__(self) -> None:
|
||||
self.level: Union[int, str] = "INFO"
|
||||
|
||||
def __call__(self, record):
|
||||
module_name: str = record["name"]
|
||||
# TODO: get plugin name instead of module name
|
||||
# module = sys.modules.get(module_name)
|
||||
# if module and hasattr(module, "__plugin__"):
|
||||
# plugin: "Plugin" = getattr(module, "__plugin__")
|
||||
# module_name = plugin.module_name
|
||||
record["name"] = module_name.split(".")[0]
|
||||
levelno = (
|
||||
logger.level(self.level).no if isinstance(self.level, str) else self.level
|
||||
)
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
class LoguruHandler(logging.Handler): # pragma: no cover
|
||||
def emit(self, record):
|
||||
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
@@ -82,9 +64,13 @@ class LoguruHandler(logging.Handler): # pragma: no cover
|
||||
)
|
||||
|
||||
|
||||
logger.remove()
|
||||
default_filter: Filter = Filter()
|
||||
"""默认日志等级过滤器"""
|
||||
def default_filter(record: "Record"):
|
||||
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
||||
log_level = record["extra"].get("nonebot_log_level", "INFO")
|
||||
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
default_format: str = (
|
||||
"<g>{time:MM-DD HH:mm:ss}</g> "
|
||||
"[<lvl>{level}</lvl>] "
|
||||
@@ -93,6 +79,8 @@ default_format: str = (
|
||||
"{message}"
|
||||
)
|
||||
"""默认日志格式"""
|
||||
|
||||
logger.remove()
|
||||
logger_id = logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
@@ -101,4 +89,4 @@ logger_id = logger.add(
|
||||
format=default_format,
|
||||
)
|
||||
|
||||
__autodoc__ = {"Filter": False, "LoguruHandler": False}
|
||||
__autodoc__ = {"logger_id": False}
|
||||
|
@@ -9,10 +9,16 @@ from nonebot.internal.matcher import Matcher as Matcher
|
||||
from nonebot.internal.matcher import matchers as matchers
|
||||
from nonebot.internal.matcher import current_bot as current_bot
|
||||
from nonebot.internal.matcher import current_event as current_event
|
||||
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||
from nonebot.internal.matcher import current_handler as current_handler
|
||||
from nonebot.internal.matcher import current_matcher as current_matcher
|
||||
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||
|
||||
__autodoc__ = {
|
||||
"Matcher": True,
|
||||
"matchers": True,
|
||||
"MatcherManager": True,
|
||||
"MatcherProvider": True,
|
||||
"DEFAULT_PROVIDER_CLASS": True,
|
||||
}
|
||||
|
@@ -8,9 +8,10 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.rule import TrieRule
|
||||
@@ -50,14 +51,14 @@ _event_postprocessors: Set[Dependent[Any]] = set()
|
||||
_run_preprocessors: Set[Dependent[Any]] = set()
|
||||
_run_postprocessors: Set[Dependent[Any]] = set()
|
||||
|
||||
EVENT_PCS_PARAMS = [
|
||||
EVENT_PCS_PARAMS = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
StateParam,
|
||||
DefaultParam,
|
||||
]
|
||||
RUN_PREPCS_PARAMS = [
|
||||
)
|
||||
RUN_PREPCS_PARAMS = (
|
||||
DependParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
@@ -65,8 +66,8 @@ RUN_PREPCS_PARAMS = [
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
RUN_POSTPCS_PARAMS = [
|
||||
)
|
||||
RUN_POSTPCS_PARAMS = (
|
||||
DependParam,
|
||||
ExceptionParam,
|
||||
BotParam,
|
||||
@@ -75,11 +76,14 @@ RUN_POSTPCS_PARAMS = [
|
||||
ArgParam,
|
||||
MatcherParam,
|
||||
DefaultParam,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
|
||||
"""事件预处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。"""
|
||||
"""事件预处理。
|
||||
|
||||
装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。
|
||||
"""
|
||||
_event_preprocessors.add(
|
||||
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
||||
)
|
||||
@@ -87,7 +91,10 @@ def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
|
||||
|
||||
|
||||
def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
|
||||
"""事件后处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。"""
|
||||
"""事件后处理。
|
||||
|
||||
装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。
|
||||
"""
|
||||
_event_postprocessors.add(
|
||||
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
||||
)
|
||||
@@ -95,7 +102,10 @@ def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
|
||||
|
||||
|
||||
def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
|
||||
"""运行预处理。装饰一个函数,使它在每次事件响应器运行前执行。"""
|
||||
"""运行预处理。
|
||||
|
||||
装饰一个函数,使它在每次事件响应器运行前执行。
|
||||
"""
|
||||
_run_preprocessors.add(
|
||||
Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
|
||||
)
|
||||
@@ -103,47 +113,262 @@ def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
|
||||
|
||||
|
||||
def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
"""运行后处理。装饰一个函数,使它在每次事件响应器运行后执行。"""
|
||||
"""运行后处理。
|
||||
|
||||
装饰一个函数,使它在每次事件响应器运行后执行。
|
||||
"""
|
||||
_run_postprocessors.add(
|
||||
Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
|
||||
)
|
||||
return func
|
||||
|
||||
|
||||
async def _apply_event_preprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
state: T_State,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
show_log: bool = True,
|
||||
) -> bool:
|
||||
"""运行事件预处理。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
show_log: 是否显示日志
|
||||
|
||||
返回:
|
||||
是否继续处理事件
|
||||
"""
|
||||
if not _event_preprocessors:
|
||||
return True
|
||||
|
||||
if show_log:
|
||||
logger.debug("Running PreProcessors...")
|
||||
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException as e:
|
||||
logger.opt(colors=True).info(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _apply_event_postprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
state: T_State,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
show_log: bool = True,
|
||||
) -> None:
|
||||
"""运行事件后处理。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
show_log: 是否显示日志
|
||||
"""
|
||||
if not _event_postprocessors:
|
||||
return
|
||||
|
||||
if show_log:
|
||||
logger.debug("Running PostProcessors...")
|
||||
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_postprocessors
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
|
||||
async def _apply_run_preprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
state: T_State,
|
||||
matcher: Matcher,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> bool:
|
||||
"""运行事件响应器运行前处理。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
matcher: 事件响应器
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
|
||||
返回:
|
||||
是否继续处理事件
|
||||
"""
|
||||
if not _run_preprocessors:
|
||||
return True
|
||||
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_preprocessors
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
async def _apply_run_postprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
matcher: Matcher,
|
||||
exception: Optional[Exception] = None,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> None:
|
||||
"""运行事件响应器运行后处理。
|
||||
|
||||
Args:
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
matcher: 事件响应器
|
||||
exception: 事件响应器运行异常
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
"""
|
||||
if not _run_postprocessors:
|
||||
return
|
||||
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_postprocessors
|
||||
)
|
||||
)
|
||||
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(
|
||||
priority: int,
|
||||
Matcher: Type[Matcher],
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
state: T_State,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""检查事件响应器是否符合运行条件。
|
||||
|
||||
请注意,过时的事件响应器将被**销毁**。对于未过时的事件响应器,将会一次检查其响应类型、权限和规则。
|
||||
|
||||
参数:
|
||||
Matcher: 要检查的事件响应器
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
|
||||
返回:
|
||||
bool: 是否符合运行条件
|
||||
"""
|
||||
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
|
||||
try:
|
||||
matchers[priority].remove(Matcher)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
with contextlib.suppress(Exception):
|
||||
Matcher.destroy()
|
||||
return False
|
||||
|
||||
try:
|
||||
if not await Matcher.check_perm(
|
||||
bot, event, stack, dependency_cache
|
||||
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
||||
return
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
|
||||
)
|
||||
return
|
||||
return False
|
||||
|
||||
if Matcher.temp:
|
||||
try:
|
||||
matchers[priority].remove(Matcher)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
|
||||
return True
|
||||
|
||||
|
||||
async def _run_matcher(
|
||||
@@ -154,80 +379,99 @@ async def _run_matcher(
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> None:
|
||||
"""运行事件响应器。
|
||||
|
||||
临时事件响应器将在运行前被**销毁**。
|
||||
|
||||
参数:
|
||||
Matcher: 事件响应器
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
|
||||
异常:
|
||||
StopPropagation: 阻止事件继续传播
|
||||
"""
|
||||
logger.info(f"Event will be handled by {Matcher}")
|
||||
|
||||
if Matcher.temp:
|
||||
with contextlib.suppress(Exception):
|
||||
Matcher.destroy()
|
||||
|
||||
matcher = Matcher()
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_run_preprocessors,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(
|
||||
f"Matcher {matcher} running is <b>cancelled</b>"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
return
|
||||
if not await _apply_run_preprocessors(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
matcher=matcher,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
):
|
||||
return
|
||||
|
||||
exception = None
|
||||
|
||||
try:
|
||||
logger.debug(f"Running matcher {matcher}")
|
||||
logger.debug(f"Running {matcher}")
|
||||
await matcher.run(bot, event, state, stack, dependency_cache)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
|
||||
f"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>"
|
||||
)
|
||||
exception = e
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_run_postprocessors,
|
||||
)
|
||||
await _apply_run_postprocessors(
|
||||
bot=bot,
|
||||
event=event,
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if matcher.block:
|
||||
raise StopPropagation
|
||||
return
|
||||
|
||||
|
||||
async def check_and_run_matcher(
|
||||
Matcher: Type[Matcher],
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
state: T_State,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> None:
|
||||
"""检查并运行事件响应器。
|
||||
|
||||
参数:
|
||||
Matcher: 事件响应器
|
||||
bot: Bot 对象
|
||||
event: Event 对象
|
||||
state: 会话状态
|
||||
stack: 异步上下文栈
|
||||
dependency_cache: 依赖缓存
|
||||
"""
|
||||
if not await _check_matcher(
|
||||
Matcher=Matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
):
|
||||
return
|
||||
|
||||
await _run_matcher(
|
||||
Matcher=Matcher,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
|
||||
|
||||
async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
@@ -244,7 +488,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
```
|
||||
"""
|
||||
show_log = True
|
||||
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | "
|
||||
log_msg = f"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | "
|
||||
try:
|
||||
log_msg += event.get_log_string()
|
||||
except NoLogException:
|
||||
@@ -255,38 +499,16 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
state: Dict[Any, Any] = {}
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
|
||||
# create event scope context
|
||||
async with AsyncExitStack() as stack:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_event_preprocessors,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
if show_log:
|
||||
logger.debug("Running PreProcessors...")
|
||||
await asyncio.gather(*coros)
|
||||
except IgnoredException as e:
|
||||
logger.opt(colors=True).info(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
return
|
||||
if not await _apply_event_preprocessors(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
):
|
||||
return
|
||||
|
||||
# Trie Match
|
||||
try:
|
||||
@@ -297,6 +519,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
)
|
||||
|
||||
break_flag = False
|
||||
# iterate through all priority until stop propagation
|
||||
for priority in sorted(matchers.keys()):
|
||||
if break_flag:
|
||||
break
|
||||
@@ -305,14 +528,12 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
logger.debug(f"Checking for matchers in priority {priority}...")
|
||||
|
||||
pending_tasks = [
|
||||
_check_matcher(
|
||||
priority, matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
check_and_run_matcher(
|
||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
)
|
||||
for matcher in matchers[priority]
|
||||
]
|
||||
|
||||
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
||||
|
||||
for result in results:
|
||||
if not isinstance(result, Exception):
|
||||
continue
|
||||
@@ -324,27 +545,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_event_postprocessors,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
if show_log:
|
||||
logger.debug("Running PostProcessors...")
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
if show_log:
|
||||
logger.debug("Checking for matchers completed")
|
||||
|
||||
await _apply_event_postprocessors(bot, event, state, stack, dependency_cache)
|
||||
|
@@ -5,17 +5,16 @@ FrontMatter:
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from typing import Any, Dict, List, Match, Tuple, Union, Optional
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.internal.params import Arg as Arg
|
||||
from nonebot.internal.params import State as State
|
||||
from nonebot.internal.params import ArgStr as ArgStr
|
||||
from nonebot.internal.params import Depends as Depends
|
||||
from nonebot.internal.params import ArgParam as ArgParam
|
||||
from nonebot.internal.params import BotParam as BotParam
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
from nonebot.internal.params import EventParam as EventParam
|
||||
from nonebot.internal.params import StateParam as StateParam
|
||||
from nonebot.internal.params import DependParam as DependParam
|
||||
@@ -26,14 +25,17 @@ from nonebot.internal.params import ExceptionParam as ExceptionParam
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
|
||||
@@ -109,16 +111,25 @@ def CommandStart() -> str:
|
||||
return Depends(_command_start)
|
||||
|
||||
|
||||
def _command_whitespace(state: T_State) -> str:
|
||||
return state[PREFIX_KEY][CMD_WHITESPACE_KEY]
|
||||
|
||||
|
||||
def CommandWhitespace() -> str:
|
||||
"""消息命令与参数之间的空白"""
|
||||
return Depends(_command_whitespace)
|
||||
|
||||
|
||||
def _shell_command_args(state: T_State) -> Any:
|
||||
return state[SHELL_ARGS]
|
||||
return state[SHELL_ARGS] # Namespace or ParserExit
|
||||
|
||||
|
||||
def ShellCommandArgs():
|
||||
def ShellCommandArgs() -> Any:
|
||||
"""shell 命令解析后的参数字典"""
|
||||
return Depends(_shell_command_args, use_cache=False)
|
||||
|
||||
|
||||
def _shell_command_argv(state: T_State) -> List[str]:
|
||||
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
|
||||
return state[SHELL_ARGV]
|
||||
|
||||
|
||||
@@ -127,17 +138,26 @@ def ShellCommandArgv() -> Any:
|
||||
return Depends(_shell_command_argv, use_cache=False)
|
||||
|
||||
|
||||
def _regex_matched(state: T_State) -> str:
|
||||
def _regex_matched(state: T_State) -> Match[str]:
|
||||
return state[REGEX_MATCHED]
|
||||
|
||||
|
||||
def RegexMatched() -> str:
|
||||
def RegexMatched() -> Match[str]:
|
||||
"""正则匹配结果"""
|
||||
return Depends(_regex_matched, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State):
|
||||
return state[REGEX_GROUP]
|
||||
def _regex_str(state: T_State) -> str:
|
||||
return _regex_matched(state).group()
|
||||
|
||||
|
||||
def RegexStr() -> str:
|
||||
"""正则匹配结果文本"""
|
||||
return Depends(_regex_str, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State) -> Tuple[Any, ...]:
|
||||
return _regex_matched(state).groups()
|
||||
|
||||
|
||||
def RegexGroup() -> Tuple[Any, ...]:
|
||||
@@ -145,8 +165,8 @@ def RegexGroup() -> Tuple[Any, ...]:
|
||||
return Depends(_regex_group, use_cache=False)
|
||||
|
||||
|
||||
def _regex_dict(state: T_State):
|
||||
return state[REGEX_DICT]
|
||||
def _regex_dict(state: T_State) -> Dict[str, Any]:
|
||||
return _regex_matched(state).groupdict()
|
||||
|
||||
|
||||
def RegexDict() -> Dict[str, Any]:
|
||||
@@ -154,10 +174,46 @@ def RegexDict() -> Dict[str, Any]:
|
||||
return Depends(_regex_dict, use_cache=False)
|
||||
|
||||
|
||||
def _startswith(state: T_State) -> str:
|
||||
return state[STARTSWITH_KEY]
|
||||
|
||||
|
||||
def Startswith() -> str:
|
||||
"""响应触发前缀"""
|
||||
return Depends(_startswith, use_cache=False)
|
||||
|
||||
|
||||
def _endswith(state: T_State) -> str:
|
||||
return state[ENDSWITH_KEY]
|
||||
|
||||
|
||||
def Endswith() -> str:
|
||||
"""响应触发后缀"""
|
||||
return Depends(_endswith, use_cache=False)
|
||||
|
||||
|
||||
def _fullmatch(state: T_State) -> str:
|
||||
return state[FULLMATCH_KEY]
|
||||
|
||||
|
||||
def Fullmatch() -> str:
|
||||
"""响应触发完整消息"""
|
||||
return Depends(_fullmatch, use_cache=False)
|
||||
|
||||
|
||||
def _keyword(state: T_State) -> str:
|
||||
return state[KEYWORD_KEY]
|
||||
|
||||
|
||||
def Keyword() -> str:
|
||||
"""响应触发关键字"""
|
||||
return Depends(_keyword, use_cache=False)
|
||||
|
||||
|
||||
def Received(id: Optional[str] = None, default: Any = None) -> Any:
|
||||
"""`receive` 事件参数"""
|
||||
|
||||
def _received(matcher: "Matcher"):
|
||||
def _received(matcher: "Matcher") -> Any:
|
||||
return matcher.get_receive(id or "", default)
|
||||
|
||||
return Depends(_received, use_cache=False)
|
||||
@@ -174,7 +230,6 @@ def LastReceived(default: Any = None) -> Any:
|
||||
|
||||
__autodoc__ = {
|
||||
"Arg": True,
|
||||
"State": True,
|
||||
"ArgStr": True,
|
||||
"Depends": True,
|
||||
"ArgParam": True,
|
||||
|
@@ -20,6 +20,9 @@ class Message:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Message()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "message"
|
||||
|
||||
@@ -29,6 +32,9 @@ class Notice:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Notice()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "notice"
|
||||
|
||||
@@ -38,6 +44,9 @@ class Request:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Request()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "request"
|
||||
|
||||
@@ -47,6 +56,9 @@ class MetaEvent:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "MetaEvent()"
|
||||
|
||||
async def __call__(self, type: str = EventType()) -> bool:
|
||||
return type == "meta_event"
|
||||
|
||||
@@ -78,16 +90,23 @@ class SuperUser:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Superuser()"
|
||||
|
||||
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||
return event.get_type() == "message" and (
|
||||
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{event.get_user_id()}"
|
||||
try:
|
||||
user_id = event.get_user_id()
|
||||
except Exception:
|
||||
return False
|
||||
return (
|
||||
f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{user_id}"
|
||||
in bot.config.superusers
|
||||
or event.get_user_id() in bot.config.superusers # 兼容旧配置
|
||||
or user_id in bot.config.superusers # 兼容旧配置
|
||||
)
|
||||
|
||||
|
||||
SUPERUSER: Permission = Permission(SuperUser())
|
||||
"""匹配任意超级用户消息类型事件"""
|
||||
"""匹配任意超级用户事件"""
|
||||
|
||||
__autodoc__ = {
|
||||
"Permission": True,
|
||||
|
@@ -16,6 +16,7 @@
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
|
||||
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
|
||||
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
|
||||
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
|
||||
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
|
||||
@@ -25,8 +26,8 @@
|
||||
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
|
||||
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
|
||||
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.plugin.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 0
|
||||
@@ -64,6 +65,8 @@ def _revert_plugin(plugin: "Plugin") -> None:
|
||||
if plugin.name not in _plugins:
|
||||
raise RuntimeError("Plugin not found!")
|
||||
del _plugins[plugin.name]
|
||||
if parent_plugin := plugin.parent_plugin:
|
||||
parent_plugin.sub_plugins.remove(plugin)
|
||||
|
||||
|
||||
def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
@@ -85,13 +88,12 @@ def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
||||
参数:
|
||||
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
|
||||
"""
|
||||
splits = module_name.split(".")
|
||||
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
||||
while splits:
|
||||
name = ".".join(splits)
|
||||
if name in loaded:
|
||||
return loaded[name]
|
||||
splits.pop()
|
||||
has_parent = True
|
||||
while has_parent:
|
||||
if module_name in loaded:
|
||||
return loaded[module_name]
|
||||
module_name, *has_parent = module_name.rsplit(".", 1)
|
||||
|
||||
|
||||
def get_loaded_plugins() -> Set["Plugin"]:
|
||||
@@ -106,8 +108,7 @@ def get_available_plugin_names() -> Set[str]:
|
||||
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .export import Export as Export
|
||||
from .export import export as export
|
||||
from .on import on_type as on_type
|
||||
from .load import require as require
|
||||
from .on import on_regex as on_regex
|
||||
from .plugin import Plugin as Plugin
|
||||
|
@@ -1,64 +0,0 @@
|
||||
"""本模块定义了插件导出的内容对象。
|
||||
|
||||
在新版插件系统中,推荐优先使用直接 import 所需要的插件内容。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 4
|
||||
description: nonebot.plugin.export 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from . import _current_plugin_chain
|
||||
|
||||
|
||||
class Export(dict):
|
||||
"""插件导出内容以使得其他插件可以获得。
|
||||
|
||||
用法:
|
||||
```python
|
||||
nonebot.export().default = "bar"
|
||||
|
||||
@nonebot.export()
|
||||
def some_function():
|
||||
pass
|
||||
|
||||
# this doesn't work before python 3.9
|
||||
# use
|
||||
# export = nonebot.export(); @export.sub
|
||||
# instead
|
||||
# See also PEP-614: https://www.python.org/dev/peps/pep-0614/
|
||||
@nonebot.export().sub
|
||||
def something_else():
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
def __call__(self, func, **kwargs):
|
||||
self[func.__name__] = func
|
||||
self.update(kwargs)
|
||||
return func
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, Export(value) if isinstance(value, dict) else value)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self[name] = Export(value) if isinstance(value, dict) else value
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self:
|
||||
self[name] = Export()
|
||||
return self[name]
|
||||
|
||||
|
||||
def export() -> Export:
|
||||
"""获取当前插件的导出内容对象"""
|
||||
warnings.warn(
|
||||
"nonebot.export() is deprecated. "
|
||||
"See https://github.com/nonebot/nonebot2/issues/935.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
plugins = _current_plugin_chain.get()
|
||||
if not plugins:
|
||||
raise RuntimeError("Export outside of the plugin!")
|
||||
return plugins[-1].export
|
@@ -5,24 +5,33 @@ FrontMatter:
|
||||
description: nonebot.plugin.load 模块
|
||||
"""
|
||||
import json
|
||||
import warnings
|
||||
from typing import Set, Iterable, Optional
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Set, Union, Iterable, Optional
|
||||
|
||||
import tomlkit
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from .export import Export
|
||||
from .plugin import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_name
|
||||
from . import _managers, get_plugin, _current_plugin_chain, _module_name_to_plugin_name
|
||||
|
||||
try: # pragma: py-gte-311
|
||||
import tomllib # pyright: ignore[reportMissingImports]
|
||||
except ModuleNotFoundError: # pragma: py-lt-311
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def load_plugin(module_path: str) -> Optional[Plugin]:
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
|
||||
参数:
|
||||
module_path: 插件名称 `path.to.your.plugin`
|
||||
module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||
"""
|
||||
|
||||
module_path = (
|
||||
path_to_module_name(module_path)
|
||||
if isinstance(module_path, Path)
|
||||
else module_path
|
||||
)
|
||||
manager = PluginManager([module_path])
|
||||
_managers.append(manager)
|
||||
return manager.load_plugin(module_path)
|
||||
@@ -74,6 +83,8 @@ def load_from_json(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("json file must contains a dict!")
|
||||
plugins = data.get("plugins")
|
||||
plugin_dirs = data.get("plugin_dirs")
|
||||
assert isinstance(plugins, list), "plugins must be a list of plugin name"
|
||||
@@ -100,18 +111,13 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
```
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
data = tomlkit.parse(f.read()) # type: ignore
|
||||
data = tomllib.loads(f.read())
|
||||
|
||||
nonebot_data = data.get("tool", {}).get("nonebot")
|
||||
if not nonebot_data:
|
||||
nonebot_data = data.get("nonebot", {}).get("plugins")
|
||||
if nonebot_data:
|
||||
warnings.warn(
|
||||
"[nonebot.plugins] table is deprecated. Use [tool.nonebot] instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Cannot find '[tool.nonebot]' in given toml file!")
|
||||
if nonebot_data is None:
|
||||
raise ValueError("Cannot find '[tool.nonebot]' in given toml file!")
|
||||
if not isinstance(nonebot_data, dict):
|
||||
raise TypeError("'[tool.nonebot]' must be a Table!")
|
||||
plugins = nonebot_data.get("plugins", [])
|
||||
plugin_dirs = nonebot_data.get("plugin_dirs", [])
|
||||
assert isinstance(plugins, list), "plugins must be a list of plugin name"
|
||||
@@ -143,7 +149,7 @@ def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
||||
return manager
|
||||
|
||||
|
||||
def require(name: str) -> Export:
|
||||
def require(name: str) -> ModuleType:
|
||||
"""获取一个插件的导出内容。
|
||||
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
@@ -155,12 +161,19 @@ def require(name: str) -> Export:
|
||||
RuntimeError: 插件无法加载
|
||||
"""
|
||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
||||
# if plugin not loaded
|
||||
if not plugin:
|
||||
manager = _find_manager_by_name(name)
|
||||
if manager:
|
||||
# plugin already declared
|
||||
if manager := _find_manager_by_name(name):
|
||||
plugin = manager.load_plugin(name)
|
||||
# plugin not declared, try to declare and load it
|
||||
else:
|
||||
plugin = load_plugin(name)
|
||||
if not plugin:
|
||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||
return plugin.export
|
||||
# clear current plugin chain, ensure plugin loaded in a new context
|
||||
_t = _current_plugin_chain.set(())
|
||||
try:
|
||||
plugin = load_plugin(name)
|
||||
finally:
|
||||
_current_plugin_chain.reset(_t)
|
||||
if not plugin:
|
||||
raise RuntimeError(f'Cannot load plugin "{name}"!')
|
||||
return plugin.module
|
||||
|
@@ -14,10 +14,10 @@ from itertools import chain
|
||||
from types import ModuleType
|
||||
from importlib.abc import MetaPathFinder
|
||||
from importlib.machinery import PathFinder, SourceFileLoader
|
||||
from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
|
||||
from typing import Set, Dict, List, Iterable, Optional, Sequence
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
|
||||
from .plugin import Plugin, PluginMetadata
|
||||
from . import (
|
||||
@@ -51,6 +51,9 @@ class PluginManager:
|
||||
self._searched_plugin_names: Dict[str, Path] = {}
|
||||
self.prepare_plugins()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"PluginManager(plugins={self.plugins}, search_path={self.search_path})"
|
||||
|
||||
@property
|
||||
def third_party_plugins(self) -> Set[str]:
|
||||
"""返回所有独立插件名称。"""
|
||||
@@ -66,13 +69,6 @@ class PluginManager:
|
||||
"""返回当前插件管理器中可用的插件名称。"""
|
||||
return self.third_party_plugins | self.searched_plugins
|
||||
|
||||
def _path_to_module_name(self, path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
def _previous_plugins(self) -> Set[str]:
|
||||
_pre_managers: List[PluginManager]
|
||||
if self in _managers:
|
||||
@@ -86,7 +82,6 @@ class PluginManager:
|
||||
|
||||
def prepare_plugins(self) -> Set[str]:
|
||||
"""搜索插件并缓存插件名称。"""
|
||||
|
||||
# get all previous ready to load plugins
|
||||
previous_plugins = self._previous_plugins()
|
||||
searched_plugins: Dict[str, Path] = {}
|
||||
@@ -118,11 +113,13 @@ class PluginManager:
|
||||
f"Plugin already exists: {module_info.name}! Check your plugin name"
|
||||
)
|
||||
|
||||
module_spec = module_info.module_finder.find_spec(module_info.name, None)
|
||||
if not module_spec:
|
||||
if not (
|
||||
module_spec := module_info.module_finder.find_spec(
|
||||
module_info.name, None
|
||||
)
|
||||
):
|
||||
continue
|
||||
module_path = module_spec.origin
|
||||
if not module_path:
|
||||
if not (module_path := module_spec.origin):
|
||||
continue
|
||||
searched_plugins[module_info.name] = Path(module_path).resolve()
|
||||
|
||||
@@ -146,20 +143,26 @@ class PluginManager:
|
||||
module = importlib.import_module(self._third_party_plugin_names[name])
|
||||
elif name in self._searched_plugin_names:
|
||||
module = importlib.import_module(
|
||||
self._path_to_module_name(self._searched_plugin_names[name])
|
||||
path_to_module_name(self._searched_plugin_names[name])
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
|
||||
)
|
||||
plugin = getattr(module, "__plugin__", None)
|
||||
if plugin is None:
|
||||
if (
|
||||
plugin := getattr(module, "__plugin__", None)
|
||||
) is None or not isinstance(plugin, Plugin):
|
||||
raise RuntimeError(
|
||||
f"Module {module.__name__} is not loaded as a plugin! "
|
||||
"Make sure not to import it before loading."
|
||||
)
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
|
||||
+ (
|
||||
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
||||
if plugin.module_name != plugin.name
|
||||
else ""
|
||||
)
|
||||
)
|
||||
return plugin
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
@@ -178,7 +181,7 @@ class PluginFinder(MetaPathFinder):
|
||||
def find_spec(
|
||||
self,
|
||||
fullname: str,
|
||||
path: Optional[Sequence[Union[bytes, str]]],
|
||||
path: Optional[Sequence[str]],
|
||||
target: Optional[ModuleType] = None,
|
||||
):
|
||||
if _managers:
|
||||
@@ -225,6 +228,7 @@ class PluginLoader(SourceFileLoader):
|
||||
# detect parent plugin before entering current plugin context
|
||||
parent_plugins = _current_plugin_chain.get()
|
||||
for pre_plugin in reversed(parent_plugins):
|
||||
# ensure parent plugin is declared before current plugin
|
||||
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
|
||||
plugin.parent_plugin = pre_plugin
|
||||
pre_plugin.sub_plugins.add(plugin)
|
||||
|
@@ -10,6 +10,7 @@ from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.dependencies import Dependent
|
||||
@@ -19,6 +20,7 @@ from nonebot.rule import (
|
||||
ArgumentParser,
|
||||
regex,
|
||||
command,
|
||||
is_type,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
@@ -26,17 +28,44 @@ from nonebot.rule import (
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .plugin import Plugin
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
|
||||
def _store_matcher(matcher: Type[Matcher]) -> None:
|
||||
plugins = _current_plugin_chain.get()
|
||||
# only store the matcher defined in the plugin
|
||||
if plugins:
|
||||
plugins[-1].matcher.add(matcher)
|
||||
def store_matcher(matcher: Type[Matcher]) -> None:
|
||||
"""存储一个事件响应器到插件。
|
||||
|
||||
参数:
|
||||
matcher: 事件响应器
|
||||
"""
|
||||
# only store the matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
plugin_chain[-1].matcher.add(matcher)
|
||||
|
||||
|
||||
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
|
||||
"""获取事件响应器定义所在插件。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
# matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
return plugin_chain[-1]
|
||||
|
||||
# matcher defined when plugin running
|
||||
if module := get_matcher_module(depth + 1):
|
||||
if plugin := get_plugin_by_module_name(module.__name__):
|
||||
return plugin
|
||||
|
||||
|
||||
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
"""获取事件响应器定义所在模块。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
current_frame = inspect.currentframe()
|
||||
if current_frame is None:
|
||||
return None
|
||||
@@ -70,7 +99,6 @@ def on(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
type,
|
||||
Rule() & rule,
|
||||
@@ -80,29 +108,20 @@ def on(
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
plugin=get_matcher_plugin(_depth + 1),
|
||||
module=get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
store_matcher(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个元事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -110,36 +129,10 @@ def on_metaevent(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"meta_event",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_message(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = True,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器。
|
||||
|
||||
参数:
|
||||
@@ -152,39 +145,16 @@ def on_message(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"message",
|
||||
Rule() & rule,
|
||||
Permission() | permission,
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
kwargs.setdefault("block", True)
|
||||
return on("message", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个通知事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -192,39 +162,15 @@ def on_notice(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"notice",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("notice", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个请求事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -232,22 +178,7 @@ def on_request(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"request",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("request", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_startswith(
|
||||
@@ -348,6 +279,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
@@ -359,6 +291,7 @@ def on_command(
|
||||
cmd: 指定命令内容
|
||||
rule: 事件响应规则
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
@@ -368,10 +301,12 @@ def on_command(
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
commands = set([cmd]) | (aliases or set())
|
||||
block = kwargs.pop("block", False)
|
||||
commands = {cmd} | (aliases or set())
|
||||
kwargs.setdefault("block", False)
|
||||
return on_message(
|
||||
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
|
||||
command(*commands, force_whitespace=force_whitespace) & rule,
|
||||
**kwargs,
|
||||
_depth=_depth + 1,
|
||||
)
|
||||
|
||||
|
||||
@@ -403,7 +338,7 @@ def on_shell_command(
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
commands = set([cmd]) | (aliases or set())
|
||||
commands = {cmd} | (aliases or set())
|
||||
return on_message(
|
||||
shell_command(*commands, parser=parser) & rule,
|
||||
**kwargs,
|
||||
@@ -437,7 +372,57 @@ def on_regex(
|
||||
return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
class CommandGroup:
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
event_types = types if isinstance(types, tuple) else (types,)
|
||||
return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
class _Group:
|
||||
def __init__(self, **kwargs):
|
||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||
self.matchers: List[Type[Matcher]] = []
|
||||
"""组内事件响应器列表"""
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on` 的参数默认值"""
|
||||
|
||||
def _get_final_kwargs(
|
||||
self, update: Dict[str, Any], *, exclude: Optional[Set[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取最终传递给 `on` 的参数
|
||||
|
||||
参数:
|
||||
update: 更新的关键字参数
|
||||
exclude: 需要排除的参数
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(update)
|
||||
if exclude:
|
||||
for key in exclude:
|
||||
final_kwargs.pop(key, None)
|
||||
final_kwargs["_depth"] = 1
|
||||
return final_kwargs
|
||||
|
||||
|
||||
class CommandGroup(_Group):
|
||||
"""命令组,用于声明一组有相同名称前缀的命令。
|
||||
|
||||
参数:
|
||||
@@ -453,12 +438,13 @@ class CommandGroup:
|
||||
"""
|
||||
|
||||
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
"""命令前缀"""
|
||||
if "aliases" in kwargs:
|
||||
del kwargs["aliases"]
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on_command` 的参数默认值"""
|
||||
super().__init__(**kwargs)
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
self.base_kwargs.pop("aliases", None)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
||||
|
||||
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
||||
@@ -466,6 +452,7 @@ class CommandGroup:
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -477,10 +464,9 @@ class CommandGroup:
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
return on_command(cmd, **final_kwargs, _depth=1)
|
||||
matcher = on_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def shell_command(
|
||||
self, cmd: Union[str, Tuple[str, ...]], **kwargs
|
||||
@@ -502,21 +488,16 @@ class CommandGroup:
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
return on_shell_command(cmd, **final_kwargs, _depth=1)
|
||||
matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
class MatcherGroup:
|
||||
class MatcherGroup(_Group):
|
||||
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||
self.matchers: List[Type[Matcher]] = []
|
||||
"""组内事件响应器列表"""
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""其他传递给 `on` 的参数默认值"""
|
||||
def __repr__(self) -> str:
|
||||
return f"MatcherGroup(matchers={len(self.matchers)})"
|
||||
|
||||
def on(self, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个基础事件响应器,可自定义类型。
|
||||
@@ -532,9 +513,7 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
matcher = on(**final_kwargs, _depth=1)
|
||||
matcher = on(**self._get_final_kwargs(kwargs))
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -543,6 +522,7 @@ class MatcherGroup:
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -550,11 +530,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
final_kwargs.pop("permission", None)
|
||||
matcher = on_metaevent(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_metaevent(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -571,10 +548,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_message(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_message(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -583,6 +558,7 @@ class MatcherGroup:
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -590,10 +566,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_notice(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_notice(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -602,6 +576,7 @@ class MatcherGroup:
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -609,10 +584,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_request(**final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"})
|
||||
matcher = on_request(**final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -633,10 +606,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_startswith(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_startswith(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -655,10 +626,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_endswith(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_endswith(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -677,10 +646,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_fullmatch(msg, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_fullmatch(msg, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -698,10 +665,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_keyword(keywords, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_keyword(keywords, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -709,6 +674,7 @@ class MatcherGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
@@ -718,6 +684,7 @@ class MatcherGroup:
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -727,10 +694,10 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_command(cmd, aliases=aliases, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_command(
|
||||
cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs
|
||||
)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -760,12 +727,8 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_shell_command(
|
||||
cmd, aliases=aliases, parser=parser, **final_kwargs, _depth=1
|
||||
)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
@@ -788,9 +751,28 @@ class MatcherGroup:
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_regex(pattern, flags=flags, **final_kwargs, _depth=1)
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_regex(pattern, flags=flags, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def on_type(
|
||||
self, types: Union[Type[Event], Tuple[Type[Event]]], **kwargs
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_type(types, **final_kwargs)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
@@ -1,13 +1,20 @@
|
||||
import re
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.rule import Rule, ArgumentParser
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
|
||||
from .plugin import Plugin
|
||||
|
||||
def store_matcher(matcher: Type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ...
|
||||
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ...
|
||||
def on(
|
||||
type: str = "",
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
@@ -22,6 +29,7 @@ def on(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -43,6 +51,7 @@ def on_message(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -53,6 +62,7 @@ def on_notice(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -116,6 +126,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
@@ -152,6 +163,18 @@ def on_regex(
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
|
||||
class CommandGroup:
|
||||
def __init__(
|
||||
@@ -171,8 +194,9 @@ class CommandGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
*,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -186,7 +210,7 @@ class CommandGroup:
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
parser: Optional[ArgumentParser] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
@@ -228,6 +252,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -251,6 +276,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -262,6 +288,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -328,6 +355,7 @@ class MatcherGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
@@ -367,3 +395,16 @@ class MatcherGroup:
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
self,
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
|
@@ -1,9 +1,11 @@
|
||||
"""本模块定义插件对象。
|
||||
"""本模块定义插件相关信息。
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 3
|
||||
description: nonebot.plugin.plugin 模块
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from types import ModuleType
|
||||
from dataclasses import field, dataclass
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||
@@ -11,11 +13,11 @@ from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from .export import Export
|
||||
from . import _plugins as plugins # FIXME: backport for nonebug
|
||||
from nonebot.utils import resolve_dot_notation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.adapters import Adapter
|
||||
|
||||
from .manager import PluginManager
|
||||
|
||||
|
||||
@@ -24,14 +26,39 @@ class PluginMetadata:
|
||||
"""插件元信息,由插件编写者提供"""
|
||||
|
||||
name: str
|
||||
"""插件可阅读名称"""
|
||||
"""插件名称"""
|
||||
description: str
|
||||
"""插件功能介绍"""
|
||||
usage: str
|
||||
"""插件使用方法"""
|
||||
type: Optional[str] = None
|
||||
"""插件类型,用于商店分类"""
|
||||
homepage: Optional[str] = None
|
||||
"""插件主页"""
|
||||
config: Optional[Type[BaseModel]] = None
|
||||
"""插件配置项"""
|
||||
supported_adapters: Optional[Set[str]] = None
|
||||
"""插件支持的适配器模块路径
|
||||
|
||||
格式为 `<module>[:<Adapter>]`,`~` 为 `nonebot.adapters.` 的缩写。
|
||||
|
||||
`None` 表示支持**所有适配器**。
|
||||
"""
|
||||
extra: Dict[Any, Any] = field(default_factory=dict)
|
||||
"""插件额外信息,可由插件编写者自由扩展定义"""
|
||||
|
||||
def get_supported_adapters(self) -> Optional[Set[Type["Adapter"]]]:
|
||||
"""获取当前已安装的插件支持适配器类列表"""
|
||||
if self.supported_adapters is None:
|
||||
return None
|
||||
|
||||
adapters = set()
|
||||
for adapter in self.supported_adapters:
|
||||
with contextlib.suppress(ModuleNotFoundError, AttributeError):
|
||||
adapters.add(
|
||||
resolve_dot_notation(adapter, "Adapter", "nonebot.adapters.")
|
||||
)
|
||||
return adapters
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
@@ -46,10 +73,8 @@ class Plugin:
|
||||
"""点分割模块路径"""
|
||||
manager: "PluginManager"
|
||||
"""导入该插件的插件管理器"""
|
||||
export: Export = field(default_factory=Export)
|
||||
"""**Deprecated:** 插件内定义的导出内容"""
|
||||
matcher: Set[Type[Matcher]] = field(default_factory=set)
|
||||
"""插件内定义的 `Matcher`"""
|
||||
"""插件加载时定义的 `Matcher`"""
|
||||
parent_plugin: Optional["Plugin"] = None
|
||||
"""父插件"""
|
||||
sub_plugins: Set["Plugin"] = field(default_factory=set)
|
||||
|
@@ -7,5 +7,5 @@ echo = on_command("echo", to_me())
|
||||
|
||||
|
||||
@echo.handle()
|
||||
async def echo_escape(message: Message = CommandArg()):
|
||||
async def handle_echo(message: Message = CommandArg()):
|
||||
await echo.send(message=message)
|
||||
|
@@ -15,8 +15,7 @@ async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:
|
||||
yield result
|
||||
else:
|
||||
current_event_id = id(event)
|
||||
event_id = _running_matcher.get(session_id, None)
|
||||
if event_id:
|
||||
if event_id := _running_matcher.get(session_id, None):
|
||||
result = event_id != current_event_id
|
||||
else:
|
||||
_running_matcher[session_id] = current_event_id
|
||||
|
458
nonebot/rule.py
458
nonebot/rule.py
@@ -10,11 +10,28 @@ FrontMatter:
|
||||
|
||||
import re
|
||||
import shlex
|
||||
from itertools import product
|
||||
from argparse import Namespace
|
||||
from typing_extensions import TypedDict
|
||||
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 Any, List, Tuple, Union, Optional, Sequence, NamedTuple
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
TypeVar,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypedDict,
|
||||
NamedTuple,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pygtrie import CharTrie
|
||||
|
||||
@@ -24,34 +41,33 @@ 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.params import (
|
||||
Command,
|
||||
EventToMe,
|
||||
EventType,
|
||||
CommandArg,
|
||||
EventMessage,
|
||||
EventPlainText,
|
||||
)
|
||||
from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CMD_RESULT = TypedDict(
|
||||
"CMD_RESULT",
|
||||
{
|
||||
"command": Optional[Tuple[str, ...]],
|
||||
"raw_command": Optional[str],
|
||||
"command_arg": Optional[Message[MessageSegment]],
|
||||
"command_arg": Optional[Message],
|
||||
"command_start": Optional[str],
|
||||
"command_whitespace": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -59,6 +75,8 @@ TRIE_VALUE = NamedTuple(
|
||||
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
|
||||
)
|
||||
|
||||
parser_message: ContextVar[str] = ContextVar("parser_message")
|
||||
|
||||
|
||||
class TrieRule:
|
||||
prefix: CharTrie = CharTrie()
|
||||
@@ -73,7 +91,11 @@ class TrieRule:
|
||||
@classmethod
|
||||
def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:
|
||||
prefix = CMD_RESULT(
|
||||
command=None, raw_command=None, command_arg=None, command_start=None
|
||||
command=None,
|
||||
raw_command=None,
|
||||
command_arg=None,
|
||||
command_start=None,
|
||||
command_whitespace=None,
|
||||
)
|
||||
state[PREFIX_KEY] = prefix
|
||||
if event.get_type() != "message":
|
||||
@@ -83,17 +105,30 @@ class TrieRule:
|
||||
message_seg: MessageSegment = message[0]
|
||||
if message_seg.is_text():
|
||||
segment_text = str(message_seg).lstrip()
|
||||
pf = cls.prefix.longest_prefix(segment_text)
|
||||
if pf:
|
||||
if pf := cls.prefix.longest_prefix(segment_text):
|
||||
value: TRIE_VALUE = pf.value
|
||||
prefix[RAW_CMD_KEY] = pf.key
|
||||
prefix[CMD_START_KEY] = value.command_start
|
||||
prefix[CMD_KEY] = value.command
|
||||
|
||||
msg = message.copy()
|
||||
msg.pop(0)
|
||||
new_message = msg.__class__(segment_text[len(pf.key) :].lstrip())
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
|
||||
# check whitespace
|
||||
arg_str = segment_text[len(pf.key) :]
|
||||
arg_str_stripped = arg_str.lstrip()
|
||||
has_arg = arg_str_stripped or msg
|
||||
if (
|
||||
has_arg
|
||||
and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0
|
||||
):
|
||||
prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len]
|
||||
|
||||
# construct command arg
|
||||
if arg_str_stripped:
|
||||
new_message = msg.__class__(arg_str_stripped)
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
prefix[CMD_ARG_KEY] = msg
|
||||
|
||||
return prefix
|
||||
@@ -113,19 +148,33 @@ class StartswithRule:
|
||||
self.msg = msg
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> Any:
|
||||
if type != "message":
|
||||
return False
|
||||
return bool(
|
||||
re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
return f"Startswith(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, StartswithRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if match := re.match(
|
||||
f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[STARTSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def startswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""匹配消息纯文本开头。
|
||||
@@ -154,19 +203,33 @@ class EndswithRule:
|
||||
self.msg = msg
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> Any:
|
||||
if type != "message":
|
||||
return False
|
||||
return bool(
|
||||
re.search(
|
||||
f"(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
return f"Endswith(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, EndswithRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if match := re.search(
|
||||
f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$",
|
||||
text,
|
||||
re.IGNORECASE if self.ignorecase else 0,
|
||||
):
|
||||
state[ENDSWITH_KEY] = match.group()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""匹配消息纯文本结尾。
|
||||
@@ -192,17 +255,35 @@ class FullmatchRule:
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = frozenset(map(str.casefold, msg) if ignorecase else msg)
|
||||
self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type_: str = EventType(), text: str = EventPlainText()
|
||||
) -> bool:
|
||||
def __repr__(self) -> str:
|
||||
return f"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
type_ == "message"
|
||||
and (text.casefold() if self.ignorecase else text) in self.msg
|
||||
isinstance(other, FullmatchRule)
|
||||
and frozenset(self.msg) == frozenset(other.msg)
|
||||
and self.ignorecase == other.ignorecase
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
if not text:
|
||||
return False
|
||||
text = text.casefold() if self.ignorecase else text
|
||||
if text in self.msg:
|
||||
state[FULLMATCH_KEY] = text
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""完全匹配消息。
|
||||
@@ -229,12 +310,28 @@ class KeywordsRule:
|
||||
def __init__(self, *keywords: str):
|
||||
self.keywords = keywords
|
||||
|
||||
async def __call__(
|
||||
self, type: str = EventType(), text: str = EventPlainText()
|
||||
) -> bool:
|
||||
if type != "message":
|
||||
def __repr__(self) -> str:
|
||||
return f"Keywords(keywords={self.keywords})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, KeywordsRule) and frozenset(
|
||||
self.keywords
|
||||
) == frozenset(other.keywords)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(frozenset(self.keywords))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
return False
|
||||
return bool(text and any(keyword in text for keyword in self.keywords))
|
||||
if not text:
|
||||
return False
|
||||
if key := next((k for k in self.keywords if k in text), None):
|
||||
state[KEYWORD_KEY] = key
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def keyword(*keywords: str) -> Rule:
|
||||
@@ -252,21 +349,49 @@ class CommandRule:
|
||||
|
||||
参数:
|
||||
cmds: 指定命令元组列表
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
"""
|
||||
|
||||
__slots__ = ("cmds",)
|
||||
__slots__ = ("cmds", "force_whitespace")
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]]):
|
||||
self.cmds = cmds
|
||||
def __init__(
|
||||
self,
|
||||
cmds: List[Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
):
|
||||
self.cmds = tuple(cmds)
|
||||
self.force_whitespace = force_whitespace
|
||||
|
||||
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
|
||||
return cmd in self.cmds
|
||||
def __repr__(self) -> str:
|
||||
return f"Command(cmds={self.cmds})"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Command {self.cmds}>"
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset(
|
||||
other.cmds
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds),))
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||
cmd_arg: Optional[Message] = CommandArg(),
|
||||
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
||||
) -> bool:
|
||||
if cmd not in self.cmds:
|
||||
return False
|
||||
if self.force_whitespace is None or not cmd_arg:
|
||||
return True
|
||||
if isinstance(self.force_whitespace, str):
|
||||
return self.force_whitespace == cmd_whitespace
|
||||
return self.force_whitespace == (cmd_whitespace is not None)
|
||||
|
||||
|
||||
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
def command(
|
||||
*cmds: Union[str, Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
) -> Rule:
|
||||
"""匹配消息命令。
|
||||
|
||||
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
|
||||
@@ -278,6 +403,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
|
||||
参数:
|
||||
cmds: 命令文本或命令元组
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
|
||||
用法:
|
||||
使用默认 `command_start`, `command_sep` 配置
|
||||
@@ -309,7 +435,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
|
||||
)
|
||||
|
||||
return Rule(CommandRule(commands))
|
||||
return Rule(CommandRule(commands, force_whitespace))
|
||||
|
||||
|
||||
class ArgumentParser(ArgParser):
|
||||
@@ -320,25 +446,81 @@ class ArgumentParser(ArgParser):
|
||||
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
|
||||
"""
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
old_message: str = getattr(self, "message", "")
|
||||
if old_message:
|
||||
old_message += "\n"
|
||||
old_message += message
|
||||
setattr(self, "message", old_message)
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
raise ParserExit(
|
||||
status=status, message=message or getattr(self, "message", None)
|
||||
)
|
||||
@overload
|
||||
def parse_known_args(
|
||||
self,
|
||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||
namespace: None = None,
|
||||
) -> Tuple[Namespace, List[Union[str, MessageSegment]]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_known_args(
|
||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||
) -> Tuple[T, List[Union[str, MessageSegment]]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_known_args(
|
||||
self, *, namespace: T
|
||||
) -> Tuple[T, List[Union[str, MessageSegment]]]:
|
||||
...
|
||||
|
||||
def parse_known_args(
|
||||
self,
|
||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||
namespace: Optional[T] = None,
|
||||
) -> Tuple[Union[Namespace, T], List[Union[str, MessageSegment]]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_args(
|
||||
self,
|
||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||
namespace: None = None,
|
||||
) -> Namespace:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_args(
|
||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||
) -> T:
|
||||
...
|
||||
|
||||
@overload
|
||||
def parse_args(self, *, namespace: T) -> T:
|
||||
...
|
||||
|
||||
def parse_args(
|
||||
self,
|
||||
args: Optional[Sequence[str]] = None,
|
||||
namespace: Optional[Namespace] = None,
|
||||
) -> Namespace:
|
||||
setattr(self, "message", "")
|
||||
return super().parse_args(args=args, namespace=namespace) # type: ignore
|
||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||
namespace: Optional[T] = None,
|
||||
) -> Union[Namespace, T]:
|
||||
result, argv = self.parse_known_args(args, namespace)
|
||||
if argv:
|
||||
msg = gettext("unrecognized arguments: %s")
|
||||
self.error(msg % " ".join(map(str, argv)))
|
||||
return cast(Union[Namespace, T], result)
|
||||
|
||||
def _parse_optional(
|
||||
self, arg_string: Union[str, MessageSegment]
|
||||
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
|
||||
return (
|
||||
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||
)
|
||||
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||
if (msg := parser_message.get(None)) is not None:
|
||||
parser_message.set(msg + message)
|
||||
else:
|
||||
super()._print_message(message, file)
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
if message:
|
||||
self._print_message(message)
|
||||
raise ParserExit(status=status, message=parser_message.get(None))
|
||||
|
||||
|
||||
class ShellCommandRule:
|
||||
@@ -352,28 +534,51 @@ class ShellCommandRule:
|
||||
__slots__ = ("cmds", "parser")
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]], parser: Optional[ArgumentParser]):
|
||||
self.cmds = cmds
|
||||
self.cmds = tuple(cmds)
|
||||
self.parser = parser
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ShellCommand(cmds={self.cmds}, parser={self.parser})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, ShellCommandRule)
|
||||
and frozenset(self.cmds) == frozenset(other.cmds)
|
||||
and self.parser is other.parser
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds), self.parser))
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
state: T_State,
|
||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||
msg: Optional[Message] = CommandArg(),
|
||||
) -> bool:
|
||||
if cmd in self.cmds and msg is not None:
|
||||
message = str(msg)
|
||||
state[SHELL_ARGV] = shlex.split(message)
|
||||
if self.parser:
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
return True
|
||||
else:
|
||||
if cmd not in self.cmds or msg is None:
|
||||
return False
|
||||
|
||||
state[SHELL_ARGV] = list(
|
||||
chain.from_iterable(
|
||||
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
|
||||
for seg in msg
|
||||
)
|
||||
)
|
||||
|
||||
if self.parser:
|
||||
t = parser_message.set("")
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ArgumentError as e: # pragma: py-gte-39
|
||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
finally:
|
||||
parser_message.reset(t)
|
||||
return True
|
||||
|
||||
|
||||
def shell_command(
|
||||
*cmds: Union[str, Tuple[str, ...]], parser: Optional[ArgumentParser] = None
|
||||
@@ -452,19 +657,26 @@ class RegexRule:
|
||||
self.regex = regex
|
||||
self.flags = flags
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
state: T_State,
|
||||
type: str = EventType(),
|
||||
msg: Message = EventMessage(),
|
||||
) -> bool:
|
||||
if type != "message":
|
||||
def __repr__(self) -> str:
|
||||
return f"Regex(regex={self.regex!r}, flags={self.flags})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, RegexRule)
|
||||
and self.regex == other.regex
|
||||
and self.flags == other.flags
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.regex, self.flags))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
try:
|
||||
msg = event.get_message()
|
||||
except Exception:
|
||||
return False
|
||||
matched = re.search(self.regex, str(msg), self.flags)
|
||||
if matched:
|
||||
state[REGEX_MATCHED] = matched.group()
|
||||
state[REGEX_GROUP] = matched.groups()
|
||||
state[REGEX_DICT] = matched.groupdict()
|
||||
if matched := re.search(self.regex, str(msg), self.flags):
|
||||
state[REGEX_MATCHED] = matched
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -473,7 +685,7 @@ class RegexRule:
|
||||
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
|
||||
"""匹配符合正则表达式的消息字符串。
|
||||
|
||||
可以通过 {ref}`nonebot.params.RegexMatched` 获取匹配成功的字符串,
|
||||
可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
|
||||
通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组,
|
||||
通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。
|
||||
|
||||
@@ -498,6 +710,15 @@ class ToMeRule:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "ToMe()"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, ToMeRule)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.__class__,))
|
||||
|
||||
async def __call__(self, to_me: bool = EventToMe()) -> bool:
|
||||
return to_me
|
||||
|
||||
@@ -508,6 +729,37 @@ def to_me() -> Rule:
|
||||
return Rule(ToMeRule())
|
||||
|
||||
|
||||
class IsTypeRule:
|
||||
"""检查事件类型是否为指定类型。"""
|
||||
|
||||
__slots__ = ("types",)
|
||||
|
||||
def __init__(self, *types: Type[Event]):
|
||||
self.types = types
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"IsType(types={tuple(type.__name__ for type in self.types)})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, IsTypeRule) and self.types == other.types
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.types,))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
return isinstance(event, self.types)
|
||||
|
||||
|
||||
def is_type(*types: Type[Event]) -> Rule:
|
||||
"""匹配事件类型。
|
||||
|
||||
参数:
|
||||
types: 事件类型
|
||||
"""
|
||||
|
||||
return Rule(IsTypeRule(*types))
|
||||
|
||||
|
||||
__autodoc__ = {
|
||||
"Rule": True,
|
||||
"Rule.__call__": True,
|
||||
|
@@ -11,6 +11,7 @@ FrontMatter:
|
||||
sidebar_position: 11
|
||||
description: nonebot.typing 模块
|
||||
"""
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -28,6 +29,8 @@ if TYPE_CHECKING:
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import Permission
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
T_Wrapped = TypeVar("T_Wrapped", bound=Callable)
|
||||
|
||||
|
||||
@@ -41,10 +44,14 @@ def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
|
||||
return overrider
|
||||
|
||||
|
||||
# state
|
||||
T_State = Dict[Any, Any]
|
||||
"""事件处理状态 State 类型"""
|
||||
|
||||
T_BotConnectionHook = Callable[..., Awaitable[Any]]
|
||||
_DependentCallable = Union[Callable[..., T], Callable[..., Awaitable[T]]]
|
||||
|
||||
# driver hooks
|
||||
T_BotConnectionHook = _DependentCallable[Any]
|
||||
"""Bot 连接建立时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -53,7 +60,7 @@ T_BotConnectionHook = Callable[..., Awaitable[Any]]
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
|
||||
T_BotDisconnectionHook = _DependentCallable[Any]
|
||||
"""Bot 连接断开时钩子函数
|
||||
|
||||
依赖参数:
|
||||
@@ -62,6 +69,8 @@ T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
# api hooks
|
||||
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
||||
"""`bot.call_api` 钩子函数"""
|
||||
T_CalledAPIHook = Callable[
|
||||
@@ -69,7 +78,8 @@ T_CalledAPIHook = Callable[
|
||||
]
|
||||
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
||||
|
||||
T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
# event hooks
|
||||
T_EventPreProcessor = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -80,7 +90,7 @@ T_EventPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
T_EventPostProcessor = _DependentCallable[Any]
|
||||
"""事件预处理函数 EventPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -91,7 +101,9 @@ T_EventPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
|
||||
# matcher run hooks
|
||||
T_RunPreProcessor = _DependentCallable[Any]
|
||||
"""事件响应器运行前预处理函数 RunPreProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -103,7 +115,7 @@ T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
T_RunPostProcessor = _DependentCallable[Any]
|
||||
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
@@ -117,7 +129,8 @@ T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
# rule, permission
|
||||
T_RuleChecker = _DependentCallable[bool]
|
||||
"""RuleChecker 即判断是否响应事件的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -128,7 +141,7 @@ T_RuleChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
- StateParam: State 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
T_PermissionChecker = _DependentCallable[bool]
|
||||
"""PermissionChecker 即判断事件是否满足权限的处理函数。
|
||||
|
||||
依赖参数:
|
||||
@@ -139,9 +152,9 @@ T_PermissionChecker = Callable[..., Union[bool, Awaitable[bool]]]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
|
||||
T_Handler = Callable[..., Any]
|
||||
T_Handler = _DependentCallable[Any]
|
||||
"""Handler 处理函数。"""
|
||||
T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
|
||||
T_TypeUpdater = _DependentCallable[str]
|
||||
"""TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。默认会更新为 `message`。
|
||||
|
||||
依赖参数:
|
||||
@@ -153,7 +166,7 @@ T_TypeUpdater = Callable[..., Union[str, Awaitable[str]]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]]
|
||||
T_PermissionUpdater = _DependentCallable["Permission"]
|
||||
"""PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。默认会更新为当前事件的触发对象。
|
||||
|
||||
依赖参数:
|
||||
@@ -165,5 +178,5 @@ T_PermissionUpdater = Callable[..., Union["Permission", Awaitable["Permission"]]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_DependencyCache = Dict[Callable[..., Any], "Task[Any]"]
|
||||
T_DependencyCache = Dict[_DependentCallable[Any], "Task[Any]"]
|
||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||
|
@@ -9,7 +9,10 @@ import re
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import importlib
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from contextvars import copy_context
|
||||
from functools import wraps, partial
|
||||
from contextlib import asynccontextmanager
|
||||
from typing_extensions import ParamSpec, get_args, get_origin
|
||||
@@ -24,6 +27,7 @@ from typing import (
|
||||
Coroutine,
|
||||
AsyncGenerator,
|
||||
ContextManager,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic.typing import is_union, is_none_type
|
||||
@@ -55,7 +59,7 @@ def generic_check_issubclass(
|
||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||
|
||||
特别的,如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
则会检查其中的类型是否是 class_or_tuple 中的一个类型子类。(None 会被忽略)
|
||||
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
"""
|
||||
try:
|
||||
return issubclass(cls, class_or_tuple)
|
||||
@@ -108,7 +112,8 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
|
||||
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
loop = asyncio.get_running_loop()
|
||||
pfunc = partial(call, *args, **kwargs)
|
||||
result = await loop.run_in_executor(None, pfunc)
|
||||
context = copy_context()
|
||||
result = await loop.run_in_executor(None, partial(context.run, pfunc))
|
||||
return result
|
||||
|
||||
return _wrapper
|
||||
@@ -129,11 +134,29 @@ async def run_sync_ctx_manager(
|
||||
await run_sync(cm.__exit__)(None, None, None)
|
||||
|
||||
|
||||
@overload
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: R = None,
|
||||
return_on_err: None = None,
|
||||
) -> Union[T, None]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: R,
|
||||
) -> Union[T, R]:
|
||||
...
|
||||
|
||||
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: Optional[R] = None,
|
||||
) -> Optional[Union[T, R]]:
|
||||
try:
|
||||
return await coro
|
||||
except exc:
|
||||
@@ -147,8 +170,33 @@ def get_name(obj: Any) -> str:
|
||||
return obj.__class__.__name__
|
||||
|
||||
|
||||
def path_to_module_name(path: Path) -> str:
|
||||
"""转换路径为模块名"""
|
||||
rel_path = path.resolve().relative_to(Path.cwd().resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
|
||||
def resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
"""解析并导入点分表示法的对象"""
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
|
||||
|
||||
class DataclassEncoder(json.JSONEncoder):
|
||||
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
"""在JSON序列化 {ref}`nonebot.adapters.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
|
||||
@overrides(json.JSONEncoder)
|
||||
def default(self, o):
|
||||
@@ -173,7 +221,7 @@ def logger_wrapper(logger_name: str):
|
||||
|
||||
def log(level: str, message: str, exception: Optional[Exception] = None):
|
||||
logger.opt(colors=True, exception=exception).log(
|
||||
level, f"<m>{escape_tag(logger_name)}</m> | " + message
|
||||
level, f"<m>{escape_tag(logger_name)}</m> | {message}"
|
||||
)
|
||||
|
||||
return log
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"start": "yarn workspace nonebot start",
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/**/*.md\""
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://v2.nonebot.dev/"><img src="https://raw.githubusercontent.com/nonebot/nonebot2/master/docs/.vuepress/public/logo.png" width="200" height="200" alt="nonebot"></a>
|
||||
<a href="https://nonebot.dev/"><img src="https://nonebot.dev/logo.png" width="200" height="200" alt="nonebot"></a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -17,7 +17,7 @@ _✨ NoneBot 本地文档插件 ✨_
|
||||
<a href="https://pypi.python.org/pypi/nonebot-plugin-docs">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot-plugin-docs.svg" alt="pypi">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="python">
|
||||
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="python">
|
||||
</p>
|
||||
|
||||
## 使用方式
|
||||
|
@@ -12,7 +12,7 @@ include = ["nonebot_plugin_docs/dist/**/*"]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7.3"
|
||||
python = "^3.8"
|
||||
nonebot2 = "^2.0.0-beta.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
3593
poetry.lock
generated
3593
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.0-beta.5"
|
||||
version = "2.0.0"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://v2.nonebot.dev/"
|
||||
homepage = "https://nonebot.dev/"
|
||||
repository = "https://github.com/nonebot/nonebot2"
|
||||
documentation = "https://v2.nonebot.dev/"
|
||||
documentation = "https://nonebot.dev/"
|
||||
keywords = ["bot", "qq", "qqbot", "mirai", "coolq"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -21,42 +21,55 @@ packages = [
|
||||
]
|
||||
include = ["nonebot/py.typed"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7.3"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
fastapi = "^0.79.0"
|
||||
tomlkit = ">=0.10.0,<1.0.0"
|
||||
typing-extensions = ">=3.10.0,<5.0.0"
|
||||
Quart = { version = "^0.17.0", optional = true }
|
||||
websockets = { version="^10.0", optional = true }
|
||||
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = "^0.18.0", extras = ["standard"] }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
|
||||
"Changelog" = "https://nonebot.dev/changelog"
|
||||
"Funding" = "https://afdian.net/@nonebot"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
yarl = "^1.7.2"
|
||||
pygtrie = "^2.4.1"
|
||||
loguru = ">=0.6.0,<1.0.0"
|
||||
typing-extensions = ">=4.0.0,<5.0.0"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||
|
||||
websockets = { version = ">=10.0", optional = true }
|
||||
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pycln = "^2.1.2"
|
||||
isort = "^5.10.1"
|
||||
black = "^22.1.0"
|
||||
black = "^23.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
pre-commit = "^2.16.0"
|
||||
pytest-xdist = "^2.5.0"
|
||||
pytest-asyncio = "^0.19.0"
|
||||
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
|
||||
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
|
||||
pre-commit = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebug = "^0.3.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
coverage-conditional-plugin = "^0.8.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
nb-autodoc = "^1.0.0a5"
|
||||
|
||||
[tool.poetry.extras]
|
||||
quart = ["quart"]
|
||||
httpx = ["httpx"]
|
||||
aiohttp = ["aiohttp"]
|
||||
websockets = ["websockets"]
|
||||
all = ["quart", "aiohttp", "httpx", "websockets"]
|
||||
quart = ["quart", "uvicorn"]
|
||||
fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
@@ -64,7 +77,7 @@ filterwarnings = [
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py37", "py38", "py39", "py310"]
|
||||
target-version = ["py38", "py39", "py310", "py311"]
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
'''
|
||||
@@ -78,6 +91,20 @@ force_sort_within_sections = true
|
||||
src_paths = ["nonebot", "tests"]
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.pycln]
|
||||
path = "."
|
||||
all = false
|
||||
|
||||
[tool.pyright]
|
||||
reportShadowedImports = false
|
||||
pythonVersion = "3.8"
|
||||
pythonPlatform = "All"
|
||||
executionEnvironments = [
|
||||
{ root = "./tests", extraPaths = ["./"] },
|
||||
{ root = "./" },
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry_core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
@@ -1,8 +1,25 @@
|
||||
[run]
|
||||
plugins =
|
||||
coverage_conditional_plugin
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
def __repr__
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
def __repr__
|
||||
def __str__
|
||||
@(typing\.)?overload
|
||||
if (typing\.)?TYPE_CHECKING( is True)?:
|
||||
@(abc\.)?abstractmethod
|
||||
raise NotImplementedError
|
||||
\.\.\.
|
||||
pass
|
||||
if __name__ == .__main__.:
|
||||
|
||||
[coverage_conditional_plugin]
|
||||
rules =
|
||||
"sys_platform != 'win32'": py-win32
|
||||
"sys_platform != 'linux'": py-linux
|
||||
"sys_platform != 'darwin'": py-darwin
|
||||
"sys_version_info < (3, 9)": py-gte-39
|
||||
"sys_version_info < (3, 11)": py-gte-311
|
||||
"sys_version_info >= (3, 11)": py-lt-311
|
||||
|
@@ -1,2 +1,3 @@
|
||||
ENVIRONMENT=test
|
||||
COMMON_CONFIG=common
|
||||
COMMON_OVERRIDE=old
|
||||
|
@@ -1,5 +1,13 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
COMMON_OVERRIDE=new
|
||||
CONFIG_FROM_ENV=
|
||||
CONFIG_OVERRIDE=old
|
||||
NESTED_DICT={"a": 1}
|
||||
NESTED_DICT__B=2
|
||||
NESTED_DICT__C__D=3
|
||||
NESTED_MISSING_DICT__A=1
|
||||
NESTED_MISSING_DICT__B__C=2
|
||||
NOT_NESTED=some string
|
||||
NOT_NESTED__A=1
|
||||
|
@@ -1,21 +1,30 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
|
||||
import nonebot
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# preload global plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_example(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "examples"))
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# preload builtin plugins
|
||||
return nonebot.load_builtin_plugins("echo", "single_session")
|
||||
|
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/simple.py
Normal file
0
tests/dynamic/simple.py
Normal file
@@ -1,29 +0,0 @@
|
||||
from nonebot import on_command
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import Arg, CommandArg, ArgPlainText
|
||||
|
||||
weather = on_command("weather", rule=to_me(), aliases={"天气", "天气预报"}, priority=5)
|
||||
|
||||
|
||||
@weather.handle()
|
||||
async def handle_first_receive(matcher: Matcher, args: Message = CommandArg()):
|
||||
plain_text = args.extract_plain_text() # 首次发送命令时跟随的参数,例:/天气 上海,则args为上海
|
||||
if plain_text:
|
||||
matcher.set_arg("city", args) # 如果用户发送了参数则直接赋值
|
||||
|
||||
|
||||
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
|
||||
async def handle_city(city: Message = Arg(), city_name: str = ArgPlainText("city")):
|
||||
if city_name not in ["北京", "上海"]: # 如果参数不符合要求,则提示用户重新输入
|
||||
# 可以使用平台的 Message 类直接构造模板消息
|
||||
await weather.reject(city.template("你想查询的城市 {city} 暂不支持,请重新输入!"))
|
||||
|
||||
city_weather = await get_weather(city_name)
|
||||
await weather.finish(city_weather)
|
||||
|
||||
|
||||
# 在这里编写获取天气信息的函数
|
||||
async def get_weather(city: str) -> str:
|
||||
return f"{city}的天气是..."
|
0
tests/plugins.empty.toml
Normal file
0
tests/plugins.empty.toml
Normal file
1
tests/plugins.invalid.json
Normal file
1
tests/plugins.invalid.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
2
tests/plugins.invalid.toml
Normal file
2
tests/plugins.invalid.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool]
|
||||
nonebot = []
|
4
tests/plugins.json
Normal file
4
tests/plugins.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": [],
|
||||
"plugin_dirs": []
|
||||
}
|
3
tests/plugins.toml
Normal file
3
tests/plugins.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[tool.nonebot]
|
||||
plugins = []
|
||||
plugin_dirs = []
|
@@ -1,6 +1,2 @@
|
||||
from nonebot import export
|
||||
|
||||
|
||||
@export()
|
||||
def test():
|
||||
return "export"
|
||||
|
@@ -3,5 +3,7 @@ from datetime import datetime, timedelta
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
test_temp_matcher = Matcher.new("test", temp=True)
|
||||
test_datetime_matcher = Matcher.new("test", expire_time=datetime.now())
|
||||
test_datetime_matcher = Matcher.new(
|
||||
"test", expire_time=datetime.now() - timedelta(seconds=1)
|
||||
)
|
||||
test_timedelta_matcher = Matcher.new("test", expire_time=timedelta(seconds=-1))
|
||||
|
@@ -1,10 +1,14 @@
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.permission import USER, Permission
|
||||
|
||||
default_permission = Permission()
|
||||
|
||||
test_permission_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
test_user_permission_updater = Matcher.new(
|
||||
permission=USER("test", perm=default_permission)
|
||||
)
|
||||
|
||||
test_custom_updater = Matcher.new(permission=default_permission)
|
||||
|
||||
|
||||
|
@@ -90,3 +90,6 @@ async def overload(event: FakeEvent):
|
||||
@test_overload.handle()
|
||||
async def finish():
|
||||
await test_overload.finish()
|
||||
|
||||
|
||||
test_destroy = on_message()
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.adapters import Adapter
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
|
||||
@@ -7,10 +8,17 @@ class Config(BaseModel):
|
||||
custom: str = ""
|
||||
|
||||
|
||||
class FakeAdapter(Adapter):
|
||||
...
|
||||
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="测试插件",
|
||||
description="测试插件元信息",
|
||||
usage="无法使用",
|
||||
type="application",
|
||||
homepage="https://nonebot.dev",
|
||||
config=Config,
|
||||
supported_adapters={"~onebot.v11", "plugins.metadata:FakeAdapter"},
|
||||
extra={"author": "NoneBot"},
|
||||
)
|
||||
|
@@ -1 +1 @@
|
||||
from .nested_subplugin2 import a
|
||||
from .nested_subplugin2 import a # nopycln: import
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.params import Depends
|
||||
@@ -47,3 +48,17 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
|
||||
|
||||
async def class_depend(c: ClassDependency = Depends()):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
||||
return x
|
||||
|
||||
|
||||
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_prior_depend(
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||
):
|
||||
return x
|
||||
|
@@ -1,17 +1,23 @@
|
||||
from typing import List, Tuple
|
||||
from typing import List, Match, Tuple
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import (
|
||||
Command,
|
||||
Keyword,
|
||||
Endswith,
|
||||
RegexStr,
|
||||
Fullmatch,
|
||||
RegexDict,
|
||||
CommandArg,
|
||||
RawCommand,
|
||||
RegexGroup,
|
||||
Startswith,
|
||||
CommandStart,
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
ShellCommandArgv,
|
||||
CommandWhitespace,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,6 +49,10 @@ async def command_start(start: str = CommandStart()) -> str:
|
||||
return start
|
||||
|
||||
|
||||
async def command_whitespace(whitespace: str = CommandWhitespace()) -> str:
|
||||
return whitespace
|
||||
|
||||
|
||||
async def shell_command_args(
|
||||
shell_command_args: dict = ShellCommandArgs(),
|
||||
) -> dict:
|
||||
@@ -63,5 +73,25 @@ async def regex_group(regex_group: Tuple = RegexGroup()) -> Tuple:
|
||||
return regex_group
|
||||
|
||||
|
||||
async def regex_matched(regex_matched: str = RegexMatched()) -> str:
|
||||
async def regex_matched(regex_matched: Match[str] = RegexMatched()) -> Match[str]:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def regex_str(regex_str: str = RegexStr()) -> str:
|
||||
return regex_str
|
||||
|
||||
|
||||
async def startswith(startswith: str = Startswith()) -> str:
|
||||
return startswith
|
||||
|
||||
|
||||
async def endswith(endswith: str = Endswith()) -> str:
|
||||
return endswith
|
||||
|
||||
|
||||
async def fullmatch(fullmatch: str = Fullmatch()) -> str:
|
||||
return fullmatch
|
||||
|
||||
|
||||
async def keyword(keyword: str = Keyword()) -> str:
|
||||
return keyword
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user