mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
1866 Commits
v2.0.0-bet
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77dc523e6 | ||
|
|
0d84bf3592 | ||
|
|
94dff49e60 | ||
|
|
5d4cf7e421 | ||
|
|
0e3e16e809 | ||
|
|
183fc8defb | ||
|
|
8712e89322 | ||
|
|
e2b49f9b65 | ||
|
|
7e11f3a3d6 | ||
|
|
71bebb6ec7 | ||
|
|
842c6ff4c6 | ||
|
|
7754f6da1d | ||
|
|
60e0752f1a | ||
|
|
ede1a20c53 | ||
|
|
04289fd50f | ||
|
|
ba3efa9e7c | ||
|
|
c5a66a6ed0 | ||
|
|
8a23b1554a | ||
|
|
d73f226cbd | ||
|
|
fd9ba678ec | ||
|
|
d29ba62ff9 | ||
|
|
00c97fd18f | ||
|
|
9531c3fa74 | ||
|
|
94293122e8 | ||
|
|
7aaa66c8ba | ||
|
|
0030bf725e | ||
|
|
22b6062900 | ||
|
|
005968ab70 | ||
|
|
dc6c194701 | ||
|
|
9b8772b590 | ||
|
|
ae8ba9f55d | ||
|
|
f4a7ce2c09 | ||
|
|
c84723668f | ||
|
|
bd3ed4207a | ||
|
|
1e8c2cfc9f | ||
|
|
5ce0238ace | ||
|
|
4e6b52b85c | ||
|
|
05fe7bb715 | ||
|
|
c555e2fac6 | ||
|
|
fd126ae154 | ||
|
|
6c7b6a9575 | ||
|
|
c4716e3e17 | ||
|
|
3601a33f20 | ||
|
|
451023518b | ||
|
|
2bd377a221 | ||
|
|
66384adad4 | ||
|
|
ec1f7ba5bc | ||
|
|
e7fc5b7b7e | ||
|
|
11477ea9d7 | ||
|
|
6adf40f45d | ||
|
|
1bdf169980 | ||
|
|
81cb356503 | ||
|
|
805778794c | ||
|
|
28cd8dd08a | ||
|
|
139b39984e | ||
|
|
f9b5fece80 | ||
|
|
8076c6bc0a | ||
|
|
44b89d13f8 | ||
|
|
fbc4225110 | ||
|
|
f07f35ccc1 | ||
|
|
111dfbf164 | ||
|
|
c713c7723b | ||
|
|
4fa2af41b0 | ||
|
|
39c09d22d1 | ||
|
|
4819b21f52 | ||
|
|
6ef6721527 | ||
|
|
14cb447874 | ||
|
|
1b2b89074d | ||
|
|
75c5678782 | ||
|
|
45ec5cdfb4 | ||
|
|
f6dd98825b | ||
|
|
f59271bd47 | ||
|
|
79f833b946 | ||
|
|
9ad562bbfd | ||
|
|
267b49247d | ||
|
|
dbda4150fb | ||
|
|
a4e17f0c49 | ||
|
|
8d8d1169d1 | ||
|
|
7bc9e61985 | ||
|
|
35cc6011b5 | ||
|
|
086af8fd22 | ||
|
|
a60d1520e6 | ||
|
|
30c22ba25a | ||
|
|
41fbaec42c | ||
|
|
562ec79e3b | ||
|
|
f620bd8eb2 | ||
|
|
13e40458d7 | ||
|
|
dc4ac6d8d7 | ||
|
|
41498bdf21 | ||
|
|
b8eae2eb82 | ||
|
|
039c2b5509 | ||
|
|
2e635370bb | ||
|
|
807a86371d | ||
|
|
c66953779c | ||
|
|
117ef18f1c | ||
|
|
520dd03d77 | ||
|
|
63f3ca2f6f | ||
|
|
2e8230e9f4 | ||
|
|
4bfea99e54 | ||
|
|
f58eba7975 | ||
|
|
53d1de4aec | ||
|
|
00f18c1bd8 | ||
|
|
ba4fbb2ec3 | ||
|
|
b3722bd637 | ||
|
|
012bd6d4fb | ||
|
|
9c4ca28d61 | ||
|
|
53bcae04ff | ||
|
|
754c54e268 | ||
|
|
f97fbc814e | ||
|
|
b8856a0577 | ||
|
|
1c0e88907b | ||
|
|
31b6df5b39 | ||
|
|
bca9e4fd08 | ||
|
|
026ceb5028 | ||
|
|
47d5a647b7 | ||
|
|
37d7230949 | ||
|
|
be458b1d5e | ||
|
|
f375a4a723 | ||
|
|
3edce9a630 | ||
|
|
c525bda1e0 | ||
|
|
417f586e0d | ||
|
|
80d7e68835 | ||
|
|
a284e6df5c | ||
|
|
7176a69f81 | ||
|
|
e3a1c02e8a | ||
|
|
5e789ae4e0 | ||
|
|
bb684e20cb | ||
|
|
e11293e46b | ||
|
|
e0d74a1657 | ||
|
|
fdd36565b1 | ||
|
|
28c53fe0d7 | ||
|
|
26539bf2b1 | ||
|
|
347889c822 | ||
|
|
91849b762c | ||
|
|
5d1319ddb9 | ||
|
|
d98228926e | ||
|
|
493997d998 | ||
|
|
3098b7c153 | ||
|
|
2b0a050226 | ||
|
|
1f3abc2bb9 | ||
|
|
dd5541e658 | ||
|
|
a76bf27f60 | ||
|
|
d70ce366cc | ||
|
|
f94b802c9b | ||
|
|
17d7bd4e31 | ||
|
|
76a40b60ff | ||
|
|
469efedab2 | ||
|
|
383699a8b4 | ||
|
|
1b9a07b923 | ||
|
|
15b76c266c | ||
|
|
dfdecaddb1 | ||
|
|
5de9de903d | ||
|
|
327f3fa441 | ||
|
|
08fde7580c | ||
|
|
4ca91ecc7e | ||
|
|
885db90bc0 | ||
|
|
c43d631eb5 | ||
|
|
cfda433d14 | ||
|
|
ea4a27bf89 | ||
|
|
23944833f2 | ||
|
|
4a40782be0 | ||
|
|
babafcaa87 | ||
|
|
9b164a6f5a | ||
|
|
4a07981972 | ||
|
|
6bb2c46f8a | ||
|
|
2054655912 | ||
|
|
062af45367 | ||
|
|
83c3ed5966 | ||
|
|
a2f2b818a7 | ||
|
|
e7941efd9a | ||
|
|
aa6faba9ae | ||
|
|
8ca72f3c64 | ||
|
|
45e10e7139 | ||
|
|
73d1b19669 | ||
|
|
ad4cf86a96 | ||
|
|
48b3e3aaf3 | ||
|
|
f2b0b1752b | ||
|
|
81dcc65f99 | ||
|
|
ac90df929e | ||
|
|
555268239f | ||
|
|
7009c8e8c1 | ||
|
|
2f3cc84f82 | ||
|
|
9444e01f0f | ||
|
|
23b7a94b9a | ||
|
|
70ece41b66 | ||
|
|
a5bb6e4220 | ||
|
|
4fc99771c5 | ||
|
|
6601def5f7 | ||
|
|
b2edea141e | ||
|
|
38886b9651 | ||
|
|
1b225cbbca | ||
|
|
b4f004c500 | ||
|
|
7a345714aa | ||
|
|
cb9fcae64c | ||
|
|
6ebeefed79 | ||
|
|
6dc87a9455 | ||
|
|
7dd7c927bf | ||
|
|
e167865686 | ||
|
|
29364679c4 | ||
|
|
ebbe8beec0 | ||
|
|
a04580e79e | ||
|
|
bfe9e7e253 | ||
|
|
720398198f | ||
|
|
5ebf349886 | ||
|
|
f8f5750c3b | ||
|
|
8d9be61406 | ||
|
|
42ea650509 | ||
|
|
a941a0f292 | ||
|
|
89f8745425 | ||
|
|
cc476528d8 | ||
|
|
64f6c2dd4c | ||
|
|
81d9531b42 | ||
|
|
3512b0ab98 | ||
|
|
ab3e916770 | ||
|
|
21376a5bfa | ||
|
|
5046b2a86e | ||
|
|
910c768910 | ||
|
|
5a526ddb40 | ||
|
|
4c5c97dca6 | ||
|
|
b3e0fb4830 | ||
|
|
258aa7d2d7 | ||
|
|
5c72fd5ba7 | ||
|
|
26e4f23a67 | ||
|
|
28fc6c35f0 | ||
|
|
3ef1d7d5d7 | ||
|
|
8474d8987e | ||
|
|
13ddfa1bdd | ||
|
|
ec8be10f26 | ||
|
|
511c521a68 | ||
|
|
0ef5940d0f | ||
|
|
eecc881cd8 | ||
|
|
770141cf0a | ||
|
|
b2b20ffc4a | ||
|
|
94a6067a4b | ||
|
|
77220d9d1f | ||
|
|
647ad9ff8f | ||
|
|
04182eefba | ||
|
|
7b4aa08c54 | ||
|
|
0033d7c686 | ||
|
|
c40b95f3e9 | ||
|
|
1fa44ca5c1 | ||
|
|
381f6633f6 | ||
|
|
d617508e32 | ||
|
|
8248e88686 | ||
|
|
25649373a6 | ||
|
|
3bee189598 | ||
|
|
c1b1742b20 | ||
|
|
3e826cab72 | ||
|
|
4ef4bb0042 | ||
|
|
25ac653623 | ||
|
|
b35bdfe6dc | ||
|
|
f06efca8cc | ||
|
|
a899523607 | ||
|
|
2c162335cb | ||
|
|
3a12984d4b | ||
|
|
7211f24a7d | ||
|
|
649624ed80 | ||
|
|
c03ff4e676 | ||
|
|
0b5a18cb63 | ||
|
|
518bf16082 | ||
|
|
b625a5d19a | ||
|
|
acca22e179 | ||
|
|
a3009d45dc | ||
|
|
fd3d1bb115 | ||
|
|
7282da8b04 | ||
|
|
7a3c7476fb | ||
|
|
f1046cfb11 | ||
|
|
8de25447b3 | ||
|
|
3cdbf35dc6 | ||
|
|
0228e255e1 | ||
|
|
353d16ebfd | ||
|
|
3d5dd5969c | ||
|
|
fe21cbfa1d | ||
|
|
c20f65636f | ||
|
|
eade8face6 | ||
|
|
ab75133e9d | ||
|
|
89596fb708 | ||
|
|
eedcf0779d | ||
|
|
05333260b7 | ||
|
|
55fd447230 | ||
|
|
263e6b25e2 | ||
|
|
e00890033e | ||
|
|
20d3d62bd5 | ||
|
|
080b876d93 | ||
|
|
27a3d1f0bb | ||
|
|
7a47985c2b | ||
|
|
8d97081948 | ||
|
|
f4ffa07c8b | ||
|
|
1b1ddc5c0f | ||
|
|
30dbd270a6 | ||
|
|
7d3c7c4933 | ||
|
|
8c8436a94f | ||
|
|
8601942ed3 | ||
|
|
4cc958ca17 | ||
|
|
472a2c7866 | ||
|
|
222609182e | ||
|
|
dccf2f3ca8 | ||
|
|
156807c365 | ||
|
|
50941f5259 | ||
|
|
2de1524a89 | ||
|
|
bdd17b62cc | ||
|
|
3a9e800a58 | ||
|
|
cb8d48c362 | ||
|
|
a5981c05d5 | ||
|
|
4cb87e596d | ||
|
|
2725a0a324 | ||
|
|
f6b0809e5f | ||
|
|
6181c1760f | ||
|
|
324277091c | ||
|
|
6eef863b70 | ||
|
|
7d52f5af4d | ||
|
|
0a70721ec0 | ||
|
|
f430f061ec | ||
|
|
572be1eb47 | ||
|
|
29cf7de1a6 | ||
|
|
c61e3cab90 | ||
|
|
77bdc5ecba | ||
|
|
16054d18c6 | ||
|
|
f0361295c3 | ||
|
|
9bd1964ae2 | ||
|
|
9141c88f77 | ||
|
|
491855876b | ||
|
|
6df28dd2a8 | ||
|
|
142d0f4d95 | ||
|
|
0127d765ae | ||
|
|
207c6b3c15 | ||
|
|
d2e699a13a | ||
|
|
ce9ba7dd9b | ||
|
|
2af23c9d89 | ||
|
|
8ee0f5efc4 | ||
|
|
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 | ||
|
|
e5e69c2726 | ||
|
|
7c7ea613e9 | ||
|
|
bb1b94e5e3 | ||
|
|
8420add975 | ||
|
|
2192e8cb6d | ||
|
|
48ccef2f06 | ||
|
|
c6bc24efc2 | ||
|
|
63f8d78d20 | ||
|
|
db36c262db | ||
|
|
732a13b692 | ||
|
|
71bf1d1147 | ||
|
|
6e98ac031c | ||
|
|
9a49354ddd | ||
|
|
455752bd92 | ||
|
|
2a51b07229 | ||
|
|
732b5b0b1b | ||
|
|
a0dcc7753c | ||
|
|
12942f2d50 | ||
|
|
192d094f54 | ||
|
|
bb02d50837 | ||
|
|
bc8c65d0d8 | ||
|
|
9447b1f462 | ||
|
|
c03b0c73cb | ||
|
|
19f4c01ad3 | ||
|
|
9bd07b9ced | ||
|
|
fe5cf5624c | ||
|
|
a14c38300e | ||
|
|
9e908d5b3f | ||
|
|
f1ab95489c | ||
|
|
3c42e26e27 | ||
|
|
c248b8c354 | ||
|
|
0ecea50778 | ||
|
|
33d4d01d51 | ||
|
|
1667440c64 | ||
|
|
141527238c | ||
|
|
e2d0453741 | ||
|
|
0849df1c76 | ||
|
|
c4d45c087a | ||
|
|
be15cfabcc | ||
|
|
dddbeb389f | ||
|
|
bdfaf4840f | ||
|
|
b37b1380a3 | ||
|
|
d8ed5c2e80 | ||
|
|
4bc391c066 | ||
|
|
5aa6138bf3 | ||
|
|
fc78b9c547 | ||
|
|
118874080d | ||
|
|
cf2137a1a9 | ||
|
|
14b145b58d | ||
|
|
1aba737cbd | ||
|
|
fe38b1f17f | ||
|
|
776651284f | ||
|
|
068cc3a7ea | ||
|
|
0e7e88cfa2 | ||
|
|
c120f9be70 | ||
|
|
563436b38e | ||
|
|
386be6cbb6 | ||
|
|
b5e29533d8 | ||
|
|
beb19adad5 | ||
|
|
e2289c78b0 | ||
|
|
d3f261eb34 | ||
|
|
d54c2e6bf4 | ||
|
|
6fdebc4912 | ||
|
|
f1ffac5ca7 | ||
|
|
12716ee79a | ||
|
|
42413281bb | ||
|
|
a67eda4c80 | ||
|
|
9dbea871b8 | ||
|
|
4181f4ca77 | ||
|
|
fe4a33d19b | ||
|
|
58d8815f39 | ||
|
|
b80083fed5 | ||
|
|
4ba17d900a | ||
|
|
f11970132c | ||
|
|
c91c9380a7 | ||
|
|
06ee47edcd | ||
|
|
a82ce00a4b | ||
|
|
e0902eeb58 | ||
|
|
b754ac2fbc | ||
|
|
1ae0a654bf | ||
|
|
b85348f648 | ||
|
|
7b06469a30 | ||
|
|
a62a49d477 | ||
|
|
e2f96a0b5c | ||
|
|
28c22b7511 | ||
|
|
c027e4d2ce | ||
|
|
e38bb2b530 | ||
|
|
19a9a3c3c5 | ||
|
|
4c8f5059db | ||
|
|
9ab1acf1e7 | ||
|
|
175acd38eb | ||
|
|
4241eb538c | ||
|
|
c55b32b9f9 | ||
|
|
2082e5f5c2 | ||
|
|
8485a356e7 | ||
|
|
f6f70fb435 | ||
|
|
c3ce4f76d1 | ||
|
|
db90a9ebab | ||
|
|
3962a98743 | ||
|
|
3ce23bc593 | ||
|
|
3238a82042 | ||
|
|
ed1f920088 | ||
|
|
c34b3439fa | ||
|
|
bd2225c43c | ||
|
|
381234725e | ||
|
|
a5ee6ac401 | ||
|
|
79fd12768d | ||
|
|
c0ab86f91b | ||
|
|
ae21bfdd0e | ||
|
|
6cfa21b15c | ||
|
|
fa3ed2b58c | ||
|
|
579839f2a4 | ||
|
|
daa95d02ac | ||
|
|
33c261c802 | ||
|
|
558e073cfa | ||
|
|
c52637a3b8 | ||
|
|
e3d1f572ed | ||
|
|
af86b96974 | ||
|
|
ddc42f7be8 | ||
|
|
41d50641ad | ||
|
|
6feed0610b | ||
|
|
fe43cc92a5 | ||
|
|
f6fb3b3970 | ||
|
|
d8ea7f1e6f | ||
|
|
dd55650f0a | ||
|
|
20f414d0de | ||
|
|
5924f1e7ac | ||
|
|
2fbd44eef9 | ||
|
|
0adf4d6934 | ||
|
|
2c91a4c7f1 | ||
|
|
625c72ab0d | ||
|
|
449a2c5f96 | ||
|
|
9e64f3f8ab | ||
|
|
9b45b77894 | ||
|
|
e890453870 | ||
|
|
abcea78fcc | ||
|
|
80594cffb6 | ||
|
|
6d4c5cbc2d | ||
|
|
d295e9ef6b | ||
|
|
2ad46bf97a | ||
|
|
70ddc634f6 | ||
|
|
540629aa7c | ||
|
|
4f4369c712 | ||
|
|
c6633bc9af | ||
|
|
1c099b4d13 | ||
|
|
abe1e29fd9 | ||
|
|
e8b9963ef3 | ||
|
|
90f7c153cb | ||
|
|
983a5930c6 | ||
|
|
ff65f10da9 | ||
|
|
1710d009bb | ||
|
|
049d988574 | ||
|
|
97fa0b4fe9 | ||
|
|
cd42385a43 | ||
|
|
56f99b7f0b | ||
|
|
91c5056c97 | ||
|
|
5e970a291f | ||
|
|
42a49a20aa | ||
|
|
a4a329cf87 | ||
|
|
dc074f35d5 | ||
|
|
591107870e | ||
|
|
94b19b4833 | ||
|
|
1a91371410 | ||
|
|
a77664297d | ||
|
|
b889d2352e | ||
|
|
17e09267e0 | ||
|
|
bbf734b2d1 | ||
|
|
7ab9e85dc0 | ||
|
|
71bfb42fe0 | ||
|
|
aeaea54ac1 | ||
|
|
49437daf10 | ||
|
|
a93401e3b4 | ||
|
|
e87861983b | ||
|
|
2d81d54d93 | ||
|
|
e145d99335 | ||
|
|
7e3a58a0e8 | ||
|
|
11b6e1ba98 | ||
|
|
34186830ab | ||
|
|
b98be416e4 | ||
|
|
aaae928026 | ||
|
|
1e43b4df10 | ||
|
|
505b4d46d0 | ||
|
|
95331bbb22 | ||
|
|
f028575f2f | ||
|
|
252e3de459 | ||
|
|
6449b1e9fd | ||
|
|
5334f11902 | ||
|
|
76ffcf14e8 | ||
|
|
c8f25db6f6 | ||
|
|
4845ca10a4 | ||
|
|
eec27a267a | ||
|
|
3870f0084d | ||
|
|
06b36ec278 | ||
|
|
dcfa25c486 | ||
|
|
14953f5161 | ||
|
|
16f69b045b | ||
|
|
533e99418c | ||
|
|
f989710cd6 | ||
|
|
e1534f2205 | ||
|
|
532aee5e71 | ||
|
|
fb047a4987 | ||
|
|
af799aa846 | ||
|
|
91f4daa722 | ||
|
|
42fa47263a | ||
|
|
39fd544651 | ||
|
|
e5bb30e2b5 | ||
|
|
a6d8f18cf0 | ||
|
|
47d843ddca | ||
|
|
74542d30e0 | ||
|
|
e12445be2f | ||
|
|
d8eb7d311b | ||
|
|
7ac14bab03 | ||
|
|
e2621b4448 | ||
|
|
2f3324ce0c | ||
|
|
494b9c625d | ||
|
|
f20cf785ce | ||
|
|
82803ff90f | ||
|
|
d38b5602a6 | ||
|
|
adb5fd8ca0 | ||
|
|
977e1de077 | ||
|
|
537866db95 | ||
|
|
9ffd78dda3 | ||
|
|
599da5158e | ||
|
|
2b64e8266c | ||
|
|
8ccf10954a | ||
|
|
09b9a626e6 | ||
|
|
1d221fddab | ||
|
|
524ed419c2 | ||
|
|
536e75f994 | ||
|
|
130c2ed5c0 | ||
|
|
3c8e705bb0 | ||
|
|
87e5e15b52 | ||
|
|
a2abc5a714 | ||
|
|
de434b3072 | ||
|
|
614f005373 | ||
|
|
36efa3f441 | ||
|
|
78a90ef7aa | ||
|
|
2e5df56d38 | ||
|
|
a230e98052 | ||
|
|
45e2e6c280 | ||
|
|
fcdb05a7e2 | ||
|
|
8c6d5a2d1f | ||
|
|
8735b61a8d | ||
|
|
02de6fd266 | ||
|
|
06f8dde33c | ||
|
|
0fe3e4fb16 | ||
|
|
56c6f6a471 | ||
|
|
58e69f7884 | ||
|
|
4ebbf7638c | ||
|
|
8d7507c8f2 | ||
|
|
c1c720756b | ||
|
|
a1be18f7f4 | ||
|
|
e2fcfa902e | ||
|
|
b54f4c8d4c | ||
|
|
4cf07ca2e0 | ||
|
|
5b3dd8f020 | ||
|
|
9bb291e95b | ||
|
|
492c0947c3 | ||
|
|
547d50ad76 | ||
|
|
f7600a8a62 | ||
|
|
9bd380a3bb | ||
|
|
8600687f7d | ||
|
|
1d98ea1961 | ||
|
|
998db949da | ||
|
|
dea1b8c6fa | ||
|
|
d348f544b1 | ||
|
|
b1559eee42 | ||
|
|
edc7183c22 | ||
|
|
ea539345cc | ||
|
|
03d60dd0be | ||
|
|
48003a779f | ||
|
|
2203b82b09 | ||
|
|
9fc2f7c02e | ||
|
|
6ee295bfd7 | ||
|
|
6b067f0865 | ||
|
|
339c25638b | ||
|
|
e6cd3e57f5 | ||
|
|
69fcda5658 | ||
|
|
88f8614cfc | ||
|
|
273b302ef2 | ||
|
|
e86bab74d3 | ||
|
|
b1df360900 | ||
|
|
2c271da965 | ||
|
|
5db9c1e232 | ||
|
|
b05ce41b1d | ||
|
|
bda27c78b9 | ||
|
|
6f0e27ee6d | ||
|
|
2c89409667 | ||
|
|
5adc5ce1cd | ||
|
|
cad2f90b8a | ||
|
|
b3d246cfb1 | ||
|
|
a8a6eb8c93 | ||
|
|
86a73011b1 | ||
|
|
9fb089bf08 | ||
|
|
72d993921f | ||
|
|
db764f7e9e | ||
|
|
1767f7a388 | ||
|
|
fc45c67d97 | ||
|
|
87885bd878 | ||
|
|
d75a04b31a | ||
|
|
12313204e1 | ||
|
|
4573235583 | ||
|
|
4293bdf21f | ||
|
|
baae3e48de | ||
|
|
24df95ae4a | ||
|
|
6b50a57348 | ||
|
|
6920ec3a11 | ||
|
|
6586f28f6a | ||
|
|
192c8da09c | ||
|
|
1fba27d9b8 | ||
|
|
0f0dc0a818 | ||
|
|
3c3a250180 | ||
|
|
03d33f3bdc | ||
|
|
1147d67f1a | ||
|
|
98e5956d44 | ||
|
|
999a6a0e10 | ||
|
|
ff2675b527 | ||
|
|
daa026cfd7 | ||
|
|
d4d3962177 | ||
|
|
1d6a333b49 | ||
|
|
9c0e05c615 | ||
|
|
258bdbe403 | ||
|
|
c48ddaf0a2 | ||
|
|
9dd989c627 | ||
|
|
3d84844a58 | ||
|
|
d63d434e0f | ||
|
|
5216a5b8f2 | ||
|
|
4107affb9b | ||
|
|
b898303e4d | ||
|
|
43aebd9c93 | ||
|
|
fcda5c37d7 | ||
|
|
1412385e51 | ||
|
|
00fab9143e | ||
|
|
5e17d4c2f9 | ||
|
|
d7de928f22 | ||
|
|
83ff7a3a6c | ||
|
|
6f9c9eb740 | ||
|
|
6272dfd46a | ||
|
|
ab78769822 | ||
|
|
004a308765 | ||
|
|
98e0ec27ee | ||
|
|
a491d842db | ||
|
|
722fc6c6e1 | ||
|
|
a5fffe2a4f | ||
|
|
5f1f84327d | ||
|
|
3d8ac3e789 | ||
|
|
c05eea2b67 | ||
|
|
a6299bec8f | ||
|
|
513c14ee78 | ||
|
|
987e44e1d0 | ||
|
|
e7937e5a06 | ||
|
|
962c71ea4e | ||
|
|
04e9a50bc1 | ||
|
|
4bd1b92e9f | ||
|
|
f737bb899c | ||
|
|
9f12404338 | ||
|
|
cee96d8ab6 | ||
|
|
14703fce8d | ||
|
|
6d0c782fcc | ||
|
|
0cceeaec0b | ||
|
|
803223f31c | ||
|
|
6ceaf51af7 | ||
|
|
8f38fc5795 | ||
|
|
257c49466f | ||
|
|
85d86dfa96 | ||
|
|
925886534c | ||
|
|
9a53b415d9 | ||
|
|
a884869ae2 | ||
|
|
04fe654d74 | ||
|
|
ff887481ee | ||
|
|
0bfe1ce433 | ||
|
|
80bf73e0e9 | ||
|
|
a8694aee23 | ||
|
|
943de58826 | ||
|
|
c0af89a6ad | ||
|
|
b076497823 | ||
|
|
a416b4315a | ||
|
|
4f91e63759 | ||
|
|
dc982fe5eb | ||
|
|
1b17039fe1 | ||
|
|
b4fc9f9539 | ||
|
|
b7762b9176 | ||
|
|
455c599b06 | ||
|
|
e9908bcbc4 | ||
|
|
fba9471fe6 | ||
|
|
58bceff175 | ||
|
|
c45843e32d | ||
|
|
aefe16f2c7 | ||
|
|
460efe436d | ||
|
|
aacfc9b90c | ||
|
|
7d190213b7 | ||
|
|
ae37f31e38 | ||
|
|
6f90dcb12f | ||
|
|
5ce72655e2 | ||
|
|
9ed5d915d5 | ||
|
|
c4d82aa21b | ||
|
|
319b3ff20e | ||
|
|
5e19ad672e | ||
|
|
2d276a6b7a | ||
|
|
e98e784fe7 | ||
|
|
f4120d91e0 | ||
|
|
2f51afc007 | ||
|
|
b9fd4b1ac8 | ||
|
|
f4395d77d7 | ||
|
|
28cfa45d95 | ||
|
|
b90054e61b | ||
|
|
1ee7f73d59 | ||
|
|
118519e15d | ||
|
|
65dc9a908b | ||
|
|
924d9b6536 | ||
|
|
fd11e2696b | ||
|
|
b8456b12ad | ||
|
|
dcaf5cedcf | ||
|
|
2cd2ea6ca4 | ||
|
|
6e8978710a | ||
|
|
7e5c179641 | ||
|
|
86aa3ef2e8 | ||
|
|
994b99e462 | ||
|
|
b0b2b1f681 | ||
|
|
4e1e0e98b4 | ||
|
|
a68a6e2659 | ||
|
|
e8bb66ca48 | ||
|
|
e720584044 | ||
|
|
ec9e8511b7 | ||
|
|
3a314fc7ae | ||
|
|
fe5e5428dd | ||
|
|
e37e6d0714 | ||
|
|
7cdcfd02ab | ||
|
|
578f0fb619 | ||
|
|
3408f27a77 | ||
|
|
0fd7c77640 | ||
|
|
613fc09382 | ||
|
|
4d5cc03454 | ||
|
|
09b438485a | ||
|
|
c7883863c7 | ||
|
|
60ccdf8a7a | ||
|
|
ea188cb948 | ||
|
|
244c7266b3 | ||
|
|
1eaacde745 | ||
|
|
c33e7eca46 | ||
|
|
bdf2f4771b | ||
|
|
2cd6867bd1 | ||
|
|
f3cc93c699 | ||
|
|
244b3c02a8 | ||
|
|
2ec5917709 | ||
|
|
a0d9c4e1a0 | ||
|
|
5abf55d095 | ||
|
|
34e2535390 | ||
|
|
5fa7806a2f | ||
|
|
1b674496ed | ||
|
|
b43dfb983d | ||
|
|
e887c39998 | ||
|
|
4c2269f789 | ||
|
|
ac0dd63ca4 | ||
|
|
a8a68d89a2 | ||
|
|
942bb8761e | ||
|
|
ad712c59b3 | ||
|
|
1271a757c9 | ||
|
|
ea8a700f86 | ||
|
|
fd8fd233b6 | ||
|
|
d06e06eba2 | ||
|
|
392def7936 | ||
|
|
c4d08d13cb | ||
|
|
ffab125f02 | ||
|
|
0b150d35f7 | ||
|
|
15d68706b3 | ||
|
|
2924c74333 | ||
|
|
afea004421 | ||
|
|
1b08e0d822 | ||
|
|
1da7da9fc1 | ||
|
|
1b6b9d61fe | ||
|
|
b222863f63 | ||
|
|
e1dffa99a0 | ||
|
|
0885474b94 | ||
|
|
482fba234b | ||
|
|
770e808f1d | ||
|
|
fdf05173be | ||
|
|
5c73c80c65 | ||
|
|
bda2991c30 | ||
|
|
efbfea3d94 | ||
|
|
956ee7e321 | ||
|
|
ba8dfb6d94 | ||
|
|
1a202e918d | ||
|
|
2e78de3d5d | ||
|
|
f53e374521 | ||
|
|
b773083b4e | ||
|
|
ebd45dc5c1 | ||
|
|
2f61c6a6f9 | ||
|
|
22597959cf | ||
|
|
6b930cd7be | ||
|
|
c8369f599f | ||
|
|
c09ecb4a88 | ||
|
|
74c3961998 | ||
|
|
e7b2dde44a | ||
|
|
77faa027bd | ||
|
|
40f7c4013b | ||
|
|
ba6a440bbd | ||
|
|
d0d6e0fc25 | ||
|
|
26934124a1 | ||
|
|
05f4a8b363 | ||
|
|
cb92bb7007 | ||
|
|
bc2abc9c51 | ||
|
|
ebd7e4df19 | ||
|
|
48b507e272 | ||
|
|
6a17ce5fda | ||
|
|
44ca11cceb | ||
|
|
12d28eb77d | ||
|
|
0513b5884d | ||
|
|
97563a0090 | ||
|
|
596f763c46 | ||
|
|
5f3902fe61 | ||
|
|
ceb7589b27 | ||
|
|
fa8a3987e7 | ||
|
|
8f51aff483 | ||
|
|
c1084cc437 | ||
|
|
46757a315b | ||
|
|
4316990aab | ||
|
|
7d81e079de | ||
|
|
12d9a68351 | ||
|
|
c4e204001e | ||
|
|
f9674da6ea | ||
|
|
794a640681 | ||
|
|
1bdb27d109 | ||
|
|
aef585c60c | ||
|
|
215cfc90f4 | ||
|
|
b671a9658b | ||
|
|
017049f321 | ||
|
|
10733e7520 | ||
|
|
b343fb8e6f | ||
|
|
f6308916ad | ||
|
|
b3d0e7c548 | ||
|
|
67f5b87492 | ||
|
|
30a14eb99c | ||
|
|
92714de522 | ||
|
|
98ef09585a | ||
|
|
13e021fa1e | ||
|
|
82224da9ab | ||
|
|
2877c39288 | ||
|
|
af0b0ae823 | ||
|
|
4701537a48 | ||
|
|
608cf859c8 | ||
|
|
27b1ded9a1 | ||
|
|
60800b1d9e | ||
|
|
c675addeef | ||
|
|
a07919ad5c | ||
|
|
ce0e230887 | ||
|
|
b037be4485 | ||
|
|
3b4c4d3081 | ||
|
|
1221baaa94 | ||
|
|
39822378a7 | ||
|
|
3041650b4b | ||
|
|
3a9a5a9ce9 | ||
|
|
34c086a046 | ||
|
|
cf819af179 | ||
|
|
9b6d8b6efa | ||
|
|
478dff512f | ||
|
|
cc1dc10b5d | ||
|
|
b7a0ba11d5 | ||
|
|
93aec6d3f6 | ||
|
|
9c729d194b | ||
|
|
dcfba4e94a | ||
|
|
55b1f962ac | ||
|
|
6c0ab74956 | ||
|
|
9cbac0b4ca | ||
|
|
8d0951a816 | ||
|
|
8a896b42a3 | ||
|
|
d26fda7320 | ||
|
|
9af7079fdd | ||
|
|
1d49defdfa | ||
|
|
0e31a5fbf4 | ||
|
|
ad5a011b45 | ||
|
|
341ee94848 | ||
|
|
a47b8c68c0 | ||
|
|
cc343c981f | ||
|
|
10395d6bce | ||
|
|
6713f4d9b0 | ||
|
|
8494768a89 | ||
|
|
88bd49de45 | ||
|
|
b52ccc3690 | ||
|
|
a8a1b4d65d | ||
|
|
98deadb4d6 | ||
|
|
ec06010298 | ||
|
|
cd69b22d43 | ||
|
|
f298930f9d | ||
|
|
0e97022d3b | ||
|
|
6d21cbed55 | ||
|
|
edb20a4786 | ||
|
|
187532930b | ||
|
|
456d333568 | ||
|
|
a5f32febbd | ||
|
|
4ba1a09fb7 | ||
|
|
0099364838 | ||
|
|
0e11959347 | ||
|
|
705a6f7fbf | ||
|
|
eeb54c8f8a | ||
|
|
c9056c6fd0 | ||
|
|
e29db806cc | ||
|
|
f837e5f3fa | ||
|
|
d82f0c6310 | ||
|
|
18082d0dc7 | ||
|
|
6ca54574ce | ||
|
|
85fc2422d1 | ||
|
|
bb7313f12c | ||
|
|
742d1f1f02 | ||
|
|
1b035ed19b | ||
|
|
ea65ee5b4f | ||
|
|
6572622e8a | ||
|
|
00de3bcc54 | ||
|
|
2ccdc218e0 | ||
|
|
c11c62f622 | ||
|
|
59c77d312a | ||
|
|
a14cfc8d77 | ||
|
|
d6f5216d29 | ||
|
|
d549087db2 | ||
|
|
3fa8bd2cd0 | ||
|
|
8674d18543 | ||
|
|
096f0d72f1 | ||
|
|
f0e935d4ff | ||
|
|
490a8146bc | ||
|
|
a74c6e220b | ||
|
|
0e8ef3def2 | ||
|
|
ba4e90b14f | ||
|
|
d02d8f06b2 |
56
.devcontainer/devcontainer.json
Normal file
56
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "Default Linux Universal",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||||
|
},
|
||||||
|
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"python.analysis.diagnosticMode": "workspace",
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"ruff.organizeImports": false,
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.ruff": true,
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/__pycache__": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/target/**": true,
|
||||||
|
"**/__pycache__": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"ms-python.isort",
|
||||||
|
"ms-python.black-formatter",
|
||||||
|
"charliermarsh.ruff",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.yarn
|
||||||
|
.history
|
||||||
|
build
|
||||||
|
lib
|
||||||
85
.eslintrc.js
Normal file
85
.eslintrc.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ["./tsconfig.json", "./website/tsconfig.json"],
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
JSX: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:regexp/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
node: {
|
||||||
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
|
},
|
||||||
|
typescript: true,
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.ts", "*.tsx"],
|
||||||
|
rules: {
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.js", "*.cjs"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
quotes: ["error", "double", { avoidEscape: true }],
|
||||||
|
semi: ["error", "always"],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
],
|
||||||
|
pathGroups: [
|
||||||
|
{ pattern: "react", group: "builtin", position: "before" },
|
||||||
|
{ pattern: "fs-extra", group: "builtin" },
|
||||||
|
{ pattern: "lodash", group: "external", position: "before" },
|
||||||
|
{ pattern: "clsx", group: "external", position: "before" },
|
||||||
|
{ pattern: "@theme/**", group: "internal" },
|
||||||
|
{ pattern: "@site/**", group: "internal" },
|
||||||
|
{ pattern: "@theme-init/**", group: "internal" },
|
||||||
|
{ pattern: "@theme-original/**", group: "internal" },
|
||||||
|
],
|
||||||
|
pathGroupsExcludedImportTypes: [],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: {
|
||||||
|
order: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
website/versioned_*/** linguist-documentation
|
||||||
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
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Question
|
- name: NoneBot 论坛
|
||||||
url: https://discussions.nonebot.dev/
|
url: https://discussions.nonebot.dev/
|
||||||
about: Ask questions about nonebot
|
about: 前往 NoneBot 论坛提问
|
||||||
- name: Plugin Publish
|
|
||||||
url: https://v2.nonebot.dev/store.html
|
|
||||||
about: Publish your plugin to nonebot homepage and nb-cli
|
|
||||||
- name: Adapter Publish
|
|
||||||
url: https://v2.nonebot.dev/store.html
|
|
||||||
about: Publish your adapter to nonebot homepage and nb-cli
|
|
||||||
- name: Bot Publish
|
|
||||||
url: https://v2.nonebot.dev/store.html
|
|
||||||
about: Publish your bot to nonebot homepage and nb-cli
|
|
||||||
|
|||||||
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
|
||||||
43
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/plugin_publish.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: 发布插件
|
||||||
|
title: "Plugin: {name}"
|
||||||
|
description: 发布插件到 NoneBot 官方商店
|
||||||
|
labels: ["Plugin"]
|
||||||
|
body:
|
||||||
|
- 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: tags
|
||||||
|
attributes:
|
||||||
|
label: 标签
|
||||||
|
description: 标签
|
||||||
|
placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]'
|
||||||
|
value: "[]"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: 插件配置项
|
||||||
|
description: 插件配置项
|
||||||
|
render: dotenv
|
||||||
|
placeholder: |
|
||||||
|
# e.g.
|
||||||
|
# KEY=VALUE
|
||||||
|
# KEY2=VALUE2
|
||||||
14
.github/actions/build-api-doc/action.yml
vendored
Normal file
14
.github/actions/build-api-doc/action.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Build API Doc
|
||||||
|
description: Build API Doc
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
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
|
||||||
13
.github/actions/setup-node/action.yml
vendored
Normal file
13
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Setup Node
|
||||||
|
description: Setup Node
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- run: yarn install --frozen-lockfile
|
||||||
|
shell: bash
|
||||||
24
.github/actions/setup-python/action.yml
vendored
Normal file
24
.github/actions/setup-python/action.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Setup Python
|
||||||
|
description: Setup Python
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
python-version:
|
||||||
|
description: Python version
|
||||||
|
required: false
|
||||||
|
default: "3.10"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Install poetry
|
||||||
|
run: pipx install poetry
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
architecture: "x64"
|
||||||
|
cache: "poetry"
|
||||||
|
|
||||||
|
- run: poetry install -E all
|
||||||
|
shell: bash
|
||||||
37
.github/dependabot.yml
vendored
Normal file
37
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/build-api-doc"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/setup-node"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/.github/actions/setup-python"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
61
.github/release-drafter.yml
vendored
61
.github/release-drafter.yml
vendored
@@ -1,42 +1,43 @@
|
|||||||
name-template: 'Release v$RESOLVED_VERSION 🌈'
|
template: $CHANGES
|
||||||
tag-template: 'v$RESOLVED_VERSION'
|
category-template: "### $TITLE"
|
||||||
|
name-template: "Release v$RESOLVED_VERSION 🌈"
|
||||||
|
tag-template: "v$RESOLVED_VERSION"
|
||||||
|
change-template: "- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))"
|
||||||
|
change-title-escapes: '\<&'
|
||||||
|
exclude-labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "skip-changelog"
|
||||||
categories:
|
categories:
|
||||||
- title: '💥 Breaking Changes'
|
- title: "💥 破坏性变更"
|
||||||
labels:
|
labels:
|
||||||
- 'Breaking'
|
- "Breaking"
|
||||||
- title: '🚀 Features'
|
- title: "🚀 新功能"
|
||||||
labels:
|
labels:
|
||||||
- 'feature'
|
- "feature"
|
||||||
- 'enhancement'
|
- "enhancement"
|
||||||
- title: '🐛 Bug Fixes'
|
- title: "🐛 Bug 修复"
|
||||||
labels:
|
labels:
|
||||||
- 'fix'
|
- "fix"
|
||||||
- 'bugfix'
|
- "bugfix"
|
||||||
- 'bug'
|
- "bug"
|
||||||
- title: '🍻 Plugin Publish'
|
- title: "📝 文档"
|
||||||
label: 'Plugin'
|
labels:
|
||||||
- title: '🍻 Bot Publish'
|
- "documentation"
|
||||||
label: 'Bot'
|
- title: "💫 杂项"
|
||||||
- title: '🍻 Adapter Publish'
|
- title: "🍻 插件发布"
|
||||||
label: 'Adapter'
|
label: "Plugin"
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
- title: "🍻 机器人发布"
|
||||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
label: "Bot"
|
||||||
|
- title: "🍻 适配器发布"
|
||||||
|
label: "Adapter"
|
||||||
version-resolver:
|
version-resolver:
|
||||||
major:
|
major:
|
||||||
labels:
|
labels:
|
||||||
- 'major'
|
- "major"
|
||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- 'minor'
|
- "minor"
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
- 'patch'
|
- "patch"
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
See: https://v2.nonebot.dev
|
|
||||||
|
|
||||||
## 💫 Changes
|
|
||||||
|
|
||||||
$CHANGES
|
|
||||||
|
|||||||
46
.github/workflows/codecov.yml
vendored
46
.github/workflows/codecov.yml
vendored
@@ -2,38 +2,46 @@ name: Code Coverage
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "nonebot/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "tests/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
name: Test Coverage
|
||||||
name: Run Pytest and Upload Coverage
|
runs-on: ${{ matrix.os }}
|
||||||
|
concurrency:
|
||||||
|
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
fail-fast: false
|
||||||
|
env:
|
||||||
|
OS: ${{ matrix.os }}
|
||||||
|
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python environment
|
- name: Setup Python environment
|
||||||
uses: actions/setup-python@v2
|
uses: ./.github/actions/setup-python
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: ${{ matrix.python-version }}
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- uses: Gr1N/setup-poetry@v7
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: poetry install -E all
|
|
||||||
|
|
||||||
- name: Run Pytest
|
- name: Run Pytest
|
||||||
run: |
|
run: |
|
||||||
cd tests/
|
cd tests/
|
||||||
poetry run pytest --cov-report xml
|
poetry run pytest -n auto --cov-report xml
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
env_vars: OS,PYTHON_VERSION
|
||||||
files: ./tests/coverage.xml
|
files: ./tests/coverage.xml
|
||||||
flags: unittests
|
flags: unittests
|
||||||
|
|||||||
100
.github/workflows/noneflow.yml
vendored
Normal file
100
.github/workflows/noneflow.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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: false
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
metadata: ${{ steps.plugin-test.outputs.METADATA }}
|
||||||
|
steps:
|
||||||
|
- name: Install Poetry
|
||||||
|
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||||
|
run: pipx install poetry
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Test Plugin
|
||||||
|
id: plugin-test
|
||||||
|
run: |
|
||||||
|
curl -sSL https://github.com/nonebot/noneflow/releases/latest/download/plugin_test.py | python -
|
||||||
|
noneflow:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: noneflow
|
||||||
|
needs: plugin_test
|
||||||
|
steps:
|
||||||
|
- name: Generate token
|
||||||
|
id: generate-token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.APP_ID }}
|
||||||
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Cache pre-commit hooks
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: .cache/.pre-commit
|
||||||
|
key: noneflow-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
|
||||||
|
- name: NoneFlow
|
||||||
|
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 }}
|
||||||
|
PLUGIN_TEST_METADATA: ${{ needs.plugin_test.outputs.metadata }}
|
||||||
|
APP_ID: ${{ secrets.APP_ID }}
|
||||||
|
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
||||||
|
PRE_COMMIT_HOME: /github/workspace/.cache/.pre-commit
|
||||||
|
|
||||||
|
- name: Fix permission
|
||||||
|
run: sudo chown -R $(whoami):$(id -ng) .cache/.pre-commit
|
||||||
30
.github/workflows/publish-bot.yml
vendored
30
.github/workflows/publish-bot.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: NoneBot2 Publish Bot
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
issues:
|
|
||||||
types: [opened, reopened, edited]
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish_bot:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: nonebot2 publish bot
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: NoneBot2 Publish Bot
|
|
||||||
uses: 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"
|
|
||||||
}
|
|
||||||
26
.github/workflows/pyright.yml
vendored
Normal file
26
.github/workflows/pyright.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Pyright Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "nonebot/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "tests/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pyright:
|
||||||
|
name: Pyright Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python environment
|
||||||
|
uses: ./.github/actions/setup-python
|
||||||
|
|
||||||
|
- run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Run Pyright
|
||||||
|
uses: jakebailey/pyright-action@v1
|
||||||
133
.github/workflows/release-drafter.yml
vendored
Normal file
133
.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-release-draft:
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pull-request-changelog
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- name: Generate token
|
||||||
|
id: generate-token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.APP_ID }}
|
||||||
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
id: release-drafter
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Update Changelog
|
||||||
|
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||||
|
with:
|
||||||
|
changelog_file: website/src/pages/changelog.md
|
||||||
|
latest_changes_position: '# 更新日志\n\n'
|
||||||
|
latest_changes_title: "## 最近更新"
|
||||||
|
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
|
||||||
|
changelog_body: ${{ steps.release-drafter.outputs.body }}
|
||||||
|
commit_and_push: false
|
||||||
|
|
||||||
|
- name: Commit and Push
|
||||||
|
run: |
|
||||||
|
yarn prettier
|
||||||
|
git config user.name 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
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Generate token
|
||||||
|
id: generate-token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.APP_ID }}
|
||||||
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python Environment
|
||||||
|
uses: ./.github/actions/setup-python
|
||||||
|
|
||||||
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
|
- name: Build API Doc
|
||||||
|
uses: ./.github/actions/build-api-doc
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
|
||||||
|
echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check Version
|
||||||
|
if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈
|
||||||
|
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||||
|
publish: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
run: |
|
||||||
|
poetry build
|
||||||
|
|
||||||
|
- name: Publish package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
- name: Publish package to GitHub
|
||||||
|
run: |
|
||||||
|
gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Build and Publish Doc Package
|
||||||
|
run: |
|
||||||
|
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
|
||||||
|
cd packages/nonebot-plugin-docs/
|
||||||
|
poetry version ${{ steps.version.outputs.VERSION }}
|
||||||
|
poetry build
|
||||||
|
|
||||||
|
- name: Publish Doc Package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
packages-dir: packages/nonebot-plugin-docs/
|
||||||
|
|
||||||
|
- name: Publish Doc Package to GitHub
|
||||||
|
run: |
|
||||||
|
cd packages/nonebot-plugin-docs/
|
||||||
|
gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
36
.github/workflows/release-github.yml
vendored
36
.github/workflows/release-github.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: Release Drafter
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-release-draft:
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
release:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
|
||||||
with:
|
|
||||||
name: Release ${{ env.TAG_NAME }} 🌈
|
|
||||||
tag: ${{ env.TAG_NAME }}
|
|
||||||
publish: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
58
.github/workflows/release-plugin-docs.yml
vendored
58
.github/workflows/release-plugin-docs.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
name: Release Nonebot Plugin Docs
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.9"
|
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- uses: Gr1N/setup-poetry@v7
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Cache Packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install and build
|
|
||||||
run: |
|
|
||||||
poetry install -E all
|
|
||||||
poetry run sphinx-build -M markdown ./docs_build ./build
|
|
||||||
cp -r ./build/markdown/* ./website/docs/api/
|
|
||||||
yarn install
|
|
||||||
yarn prettier
|
|
||||||
yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist
|
|
||||||
|
|
||||||
- name: Publish Package
|
|
||||||
run: |
|
|
||||||
export NONEBOT_VERSION=`poetry version -s`
|
|
||||||
cd packages/nonebot-plugin-docs/
|
|
||||||
poetry version $NONEBOT_VERSION
|
|
||||||
poetry build
|
|
||||||
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
|
|
||||||
73
.github/workflows/release.yml
vendored
73
.github/workflows/release.yml
vendored
@@ -6,64 +6,47 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Generate token
|
||||||
|
id: generate-token
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
ref: master
|
app_id: ${{ secrets.APP_ID }}
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- name: Set up Python
|
- uses: actions/checkout@v4
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- uses: Gr1N/setup-poetry@v7
|
- name: Setup Python Environment
|
||||||
|
uses: ./.github/actions/setup-python
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
|
- name: Build API Doc
|
||||||
|
uses: ./.github/actions/build-api-doc
|
||||||
|
|
||||||
|
- run: echo "TAG_NAME=v$(poetry version -s)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Archive Changelog
|
||||||
|
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
changelog_file: website/src/pages/changelog.md
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
archive_regex: '(?<=## )最近更新(?=\n)'
|
||||||
|
archive_title: ${{ env.TAG_NAME }}
|
||||||
- name: Setup Node
|
commit_and_push: false
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Cache Packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Set up dependencies
|
|
||||||
run: |
|
|
||||||
poetry install -E all
|
|
||||||
|
|
||||||
- name: Build Doc
|
|
||||||
run: poetry run sphinx-build -M markdown ./docs_build ./build
|
|
||||||
|
|
||||||
- name: Copy Files
|
|
||||||
run: cp -r ./build/markdown/* ./website/docs/api/
|
|
||||||
|
|
||||||
- name: Archive Files
|
- name: Archive Files
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
|
||||||
yarn prettier
|
|
||||||
yarn archive $(poetry version -s)
|
yarn archive $(poetry version -s)
|
||||||
|
yarn prettier
|
||||||
|
|
||||||
- name: Push Tag and Release to PyPI
|
- name: Push Tag
|
||||||
run: |
|
run: |
|
||||||
git config user.name github-actions
|
git config user.name noneflow[bot]
|
||||||
git config user.email github-actions@github.com
|
git config user.email 129742071+noneflow[bot]@users.noreply.github.com
|
||||||
git add .
|
git add .
|
||||||
git commit -m ":bookmark: Release $(poetry version -s)"
|
git commit -m ":bookmark: Release $(poetry version -s)"
|
||||||
git tag v$(poetry version -s)
|
git tag ${{ env.TAG_NAME }}
|
||||||
git push && git push --tags
|
git push && git push --tags
|
||||||
poetry build
|
|
||||||
poetry publish -u ${{secrets.PYPI_USERNAME}} -p ${{secrets.PYPI_PASSWORD}}
|
|
||||||
|
|||||||
21
.github/workflows/ruff.yml
vendored
Normal file
21
.github/workflows/ruff.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Ruff Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "nonebot/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "tests/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ruff:
|
||||||
|
name: Ruff Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Ruff Lint
|
||||||
|
uses: chartboost/ruff-action@v1
|
||||||
79
.github/workflows/website-deploy.yml
vendored
79
.github/workflows/website-deploy.yml
vendored
@@ -1,83 +1,46 @@
|
|||||||
name: Build Upload Site
|
name: Site Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request_target:
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: website-deploy-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
if: github.event_name == 'push'
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
if: github.event_name != 'push'
|
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Setup Python Environment
|
||||||
uses: actions/setup-python@v2
|
uses: ./.github/actions/setup-python
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- uses: Gr1N/setup-poetry@v7
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- name: Build API Doc
|
||||||
with:
|
uses: ./.github/actions/build-api-doc
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Build Doc
|
||||||
uses: actions/setup-node@v2
|
run: yarn build
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Cache Packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install and build
|
|
||||||
run: |
|
|
||||||
poetry install -E all
|
|
||||||
poetry run sphinx-build -M markdown ./docs_build ./build
|
|
||||||
cp -r ./build/markdown/* ./website/docs/api/
|
|
||||||
yarn install
|
|
||||||
yarn prettier
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
- name: Get Branch Name
|
- name: Get Branch Name
|
||||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Get Deploy Name
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
echo "DEPLOY_NAME=${{ env.BRANCH_NAME }}" >> $GITHUB_ENV
|
|
||||||
echo "PRODUCTION=${{ env.BRANCH_NAME == 'master' }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get Deploy Name
|
|
||||||
if: github.event_name != 'push'
|
|
||||||
run: |
|
|
||||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
|
||||||
echo "PRODUCTION=false" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
uses: nwtgck/actions-netlify@v1
|
uses: nwtgck/actions-netlify@v2
|
||||||
with:
|
with:
|
||||||
publish-dir: './website/build'
|
publish-dir: "./website/build"
|
||||||
production-deploy: ${{ env.PRODUCTION }}
|
production-deploy: true
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
deploy-message: 'Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}'
|
deploy-message: "Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}"
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
alias: ${{ env.DEPLOY_NAME }}
|
alias: ${{ env.BRANCH_NAME }}
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
||||||
|
|||||||
46
.github/workflows/website-preview.yml
vendored
Normal file
46
.github/workflows/website-preview.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Site Deploy(Preview)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
preview:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: pull-request-preview-${{ github.event.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python Environment
|
||||||
|
uses: ./.github/actions/setup-python
|
||||||
|
|
||||||
|
- name: Setup Node Environment
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
|
||||||
|
- name: Build API Doc
|
||||||
|
uses: ./.github/actions/build-api-doc
|
||||||
|
|
||||||
|
- name: Build Doc
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Get Deploy Name
|
||||||
|
run: |
|
||||||
|
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
uses: nwtgck/actions-netlify@v2
|
||||||
|
with:
|
||||||
|
publish-dir: "./website/build"
|
||||||
|
production-deploy: false
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
|
||||||
|
enable-commit-comment: false
|
||||||
|
alias: ${{ env.DEPLOY_NAME }}
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -139,7 +139,7 @@ fabric.properties
|
|||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
# Icon
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|||||||
4
.markdownlint.yaml
Normal file
4
.markdownlint.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
MD013: false
|
||||||
|
MD024: # 重复标题
|
||||||
|
siblings_only: true
|
||||||
|
MD033: false # 允许 html
|
||||||
39
.pre-commit-config.yaml
Normal file
39
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
default_install_hook_types: [pre-commit, prepare-commit-msg]
|
||||||
|
ci:
|
||||||
|
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
|
||||||
|
autofix_prs: true
|
||||||
|
autoupdate_branch: master
|
||||||
|
autoupdate_schedule: monthly
|
||||||
|
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.0.291
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
stages: [commit]
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
stages: [commit]
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 23.9.1
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
stages: [commit]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v3.0.3
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||||
|
stages: [commit]
|
||||||
|
|
||||||
|
- repo: https://github.com/nonebot/nonemoji
|
||||||
|
rev: v0.1.4
|
||||||
|
hooks:
|
||||||
|
- id: nonemoji
|
||||||
|
stages: [prepare-commit-msg]
|
||||||
31
.stylelintrc.js
Normal file
31
.stylelintrc.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.css"],
|
||||||
|
rules: {
|
||||||
|
"function-no-unknown": [true, { ignoreFunctions: ["theme"] }],
|
||||||
|
"selector-class-pattern": [
|
||||||
|
"^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
|
||||||
|
{
|
||||||
|
resolveNestedSelectors: true,
|
||||||
|
message: (selector) =>
|
||||||
|
`Expected class selector "${selector}" to be kebab-case`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.module.css"],
|
||||||
|
rules: {
|
||||||
|
"selector-class-pattern": [
|
||||||
|
"^[a-z][a-zA-Z0-9]+$",
|
||||||
|
{
|
||||||
|
message: (selector) =>
|
||||||
|
`Expected class selector "${selector}" to be lowerCamelCase`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Changelog
|
# 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>
|
||||||
|
|||||||
86
CODE_OF_CONDUCT.md
Normal file
86
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# NoneBot2 贡献者公约
|
||||||
|
|
||||||
|
## 我们的承诺
|
||||||
|
|
||||||
|
身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。
|
||||||
|
|
||||||
|
我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。
|
||||||
|
|
||||||
|
## 我们的准则
|
||||||
|
|
||||||
|
有助于促进本社区积极环境的行为包括但不限于:
|
||||||
|
|
||||||
|
- 与人为善、推己及人
|
||||||
|
- 尊重不同的主张、观点和经历
|
||||||
|
- 积极提出、耐心接受有益批评
|
||||||
|
- 面对过失,承担责任、认真道歉、从中学习
|
||||||
|
- 关注社区共同诉求,而非一己私利
|
||||||
|
|
||||||
|
不当行为包括但不限于:
|
||||||
|
|
||||||
|
- 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引
|
||||||
|
- 挑衅行为、侮辱或贬损的言论、人身及政治攻击
|
||||||
|
- 公开或私下骚扰
|
||||||
|
- 未获明确授权擅自发布他人的资料,如地址、电子邮箱等
|
||||||
|
- 其他有理由认定为违反职业操守的不当行为
|
||||||
|
|
||||||
|
## 落实之义务
|
||||||
|
|
||||||
|
社区负责人有责任诠释什么是“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。
|
||||||
|
|
||||||
|
社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论(comment)、提交(commit)、代码、维基(wiki)编辑、问题(issue)等贡献。如有必要,需告知采取措施的理由。
|
||||||
|
|
||||||
|
## 适用范围
|
||||||
|
|
||||||
|
此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。
|
||||||
|
|
||||||
|
代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。
|
||||||
|
|
||||||
|
## 贯彻落实
|
||||||
|
|
||||||
|
如遇滥用、骚扰等不当行为,请通过 contact@nonebot.dev 向我们举报。我们将迅速审议并调查全部投诉。
|
||||||
|
|
||||||
|
社区全体负责人有义务保密举报者信息。
|
||||||
|
|
||||||
|
## 指导方针
|
||||||
|
|
||||||
|
社区负责人将依据下列方案判断并处置违纪行为:
|
||||||
|
|
||||||
|
### 一、督促
|
||||||
|
|
||||||
|
**社区影响**:用语不当、举止不符合道德或不受社区欢迎。
|
||||||
|
|
||||||
|
**处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或要求公开道歉。
|
||||||
|
|
||||||
|
### 二、警告
|
||||||
|
|
||||||
|
**社区影响**:一起或多起事件中的违纪行为。
|
||||||
|
|
||||||
|
**处理意见**:警告继续违纪的后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。
|
||||||
|
|
||||||
|
### 三、封禁
|
||||||
|
|
||||||
|
**社区影响**:严重违纪行为,包括屡教不改。
|
||||||
|
|
||||||
|
**处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区管理者往来。如有违反,可导致开除。
|
||||||
|
|
||||||
|
### 四、开除
|
||||||
|
|
||||||
|
**社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。
|
||||||
|
|
||||||
|
**处理意见**:无限期禁止违纪者与项目社区的一切公开往来。
|
||||||
|
|
||||||
|
## 来源
|
||||||
|
|
||||||
|
本行为标准改编自[参与者公约][homepage]2.0 版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]
|
||||||
|
|
||||||
|
指导方针借鉴自[Mozilla 纪检分级][mozilla coc]。
|
||||||
|
|
||||||
|
此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][faq]。
|
||||||
|
另有诸译本:[https://www.contributor-covenant.org/translations][translations]。
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[mozilla coc]: https://github.com/mozilla/diversity
|
||||||
|
[faq]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
91
CONTRIBUTING.md
Normal file
91
CONTRIBUTING.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# NoneBot2 贡献指南
|
||||||
|
|
||||||
|
首先,感谢你愿意为 NoneBot2 贡献自己的一份力量!
|
||||||
|
|
||||||
|
本指南旨在引导你更规范地向 NoneBot2 提交贡献,请务必认真阅读。
|
||||||
|
|
||||||
|
## 提交 Issue
|
||||||
|
|
||||||
|
在提交 Issue 前,我们建议你先查看 [FAQ](https://github.com/nonebot/discussions/discussions/13) 与 [已有的 Issues](https://github.com/nonebot/nonebot2/issues),以防重复提交。
|
||||||
|
|
||||||
|
### 报告问题、故障与漏洞
|
||||||
|
|
||||||
|
如果你在使用过程中发现问题并确信是由 NoneBot2 引起的,欢迎提交 Issue。
|
||||||
|
|
||||||
|
### 建议功能
|
||||||
|
|
||||||
|
为了让开发者更好地理解你的意图,请认真描述你所需要的特性,可能的话可以提出你认为可行的解决方案。
|
||||||
|
|
||||||
|
## Pull Request
|
||||||
|
|
||||||
|
NoneBot 使用 [poetry](https://python-poetry.org/) 管理项目依赖,由于 pre-commit 也经其管理,所以在此一并说明。
|
||||||
|
|
||||||
|
下面的命令能在已安装 poetry 和 yarn 的情况下帮你快速配置开发环境。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 python 依赖
|
||||||
|
poetry install
|
||||||
|
# 安装 pre-commit git hook
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 GitHub Codespaces(Dev Container)
|
||||||
|
|
||||||
|
[](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)
|
||||||
|
|
||||||
|
### Commit 规范
|
||||||
|
|
||||||
|
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。
|
||||||
|
|
||||||
|
NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。
|
||||||
|
|
||||||
|
或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commit,nonemoji 已默认作为项目开发依赖安装。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作流概述
|
||||||
|
|
||||||
|
`master` 分支为 NoneBot 的开发分支,在任何情况下都请不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文,以便自动生成更新日志。
|
||||||
|
|
||||||
|
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。
|
||||||
|
|
||||||
|
### 撰写文档
|
||||||
|
|
||||||
|
NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。
|
||||||
|
|
||||||
|
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server,如下所示:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||||
|
|
||||||
|
以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||||
|
|
||||||
|
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
|
||||||
|
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。`
|
||||||
|
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
|
||||||
|
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
|
||||||
|
5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。
|
||||||
|
|
||||||
|
以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。
|
||||||
|
|
||||||
|
如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。
|
||||||
|
|
||||||
|
### 参与开发
|
||||||
|
|
||||||
|
NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
|
||||||
|
|
||||||
|
## 为社区做贡献
|
||||||
|
|
||||||
|
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://nonebot.dev/docs/developer/plugin-publishing) 一节。
|
||||||
|
|
||||||
|
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
|
||||||
|
|
||||||
|
虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。
|
||||||
|
|
||||||
|
同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。
|
||||||
167
README.md
167
README.md
@@ -1,6 +1,6 @@
|
|||||||
<!-- markdownlint-disable MD033 MD041 -->
|
<!-- markdownlint-disable MD033 MD041 -->
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -19,34 +19,62 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<img src="https://img.shields.io/github/license/nonebot/nonebot2" alt="license">
|
<img src="https://img.shields.io/github/license/nonebot/nonebot2" alt="license">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||||
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
|
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python">
|
<img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
|
||||||
|
<a href="https://github.com/psf/black">
|
||||||
|
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Microsoft/pyright">
|
||||||
|
<img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/astral-sh/ruff">
|
||||||
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
<a href="https://codecov.io/gh/nonebot/nonebot2">
|
<a href="https://codecov.io/gh/nonebot/nonebot2">
|
||||||
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4"/>
|
<img src="https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4" alt="codecov"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml">
|
||||||
|
<img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
|
||||||
|
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml">
|
||||||
|
<img src="https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml/badge.svg?branch=master&event=push" alt="pyright">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml">
|
||||||
|
<img src="https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml/badge.svg?branch=master&event=push" alt="ruff">
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<a href="https://onebot.dev/">
|
<a href="https://onebot.dev/">
|
||||||
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
|
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||||
</a>
|
</a>
|
||||||
<a href="http://github.com/mamoe/mirai">
|
<a href="https://onebot.dev/">
|
||||||
<img src="https://img.shields.io/badge/mirai-HTTP-lightgrey?style=social">
|
<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="ding">
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://core.telegram.org/bots/api">
|
<a href="https://core.telegram.org/bots/api">
|
||||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram">
|
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://open.feishu.cn/document/home/index">
|
<a href="https://open.feishu.cn/document/home/index">
|
||||||
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAk1BMVEX///8zMzNJSUlSUlJcXFxtbW0zMzNLS0szMzMzMzNBQUGVlZUzMzM1NTU0NDQzMzMzMzM0NDQ0NDQ0NDQ3NzdDQ0M0NDQ2NjY4ODg9PT0zMzM0NDQ5OTk7OzszMzM0NDQ3NzczMzM0NDQ0NDQ0NDQ0NDQ3Nzc2NjY4ODg2NjY7Ozs0NDQ6Ojo6Ojo3Nzc4ODgzMzNGdMWJAAAAMHRSTlMD6h0TDgr8GNf0KQbvhLT45KKTmm4jwHVJLdLFQzbcjFTgzsq7rl58T2kyqD46Y1riMDRhAAAFr0lEQVR42uzZWXKiUACF4YMyqKAQhyjOc7STmLP/1bVlLukESIJ3sLGKbwFU/Q8HuIBKpVKpVCqVSqVSqVQqlUqlUvmNM10Mcfda/U6TPdw3e9lb8ayLO+bPniYu+amjNcPd8U7PFhML0RE5uCvnaY/5SVt0WFvckcu0vxjiYmDxbu5cl2mn9UVHRMa4B2LaP3RYKD1vL6adccRFLSLL/izxxbRz7UXHimdLlFdq2mlvnztYRznZh96cP3G/dkxQRrOnR5c/c5eiQ+S2UTbe/sHir9zD1w6+okz8aXvMItyRqE46ApSHmHYRYdLRoPCMcrAP3TkLC6fpDp5QAn/EtItqij3UG/zgQZH5aWc7ZqJjzA9jKFGf9ppXC3I6uMB/Mzh2mpQQ/Mnp4BSy1Kctx4pFx5qfhA4kqE87pCyrldfBDm6sLqat2mGnttXHDfkvYtryooHo2PCrFm5lcNw1qWr1XUeEm7BH3QYVRJNGcOmoietNmNKDWeKFnCo6b3Wc1drW/NsOLpFRqmmT4xgfPFw42Q7XhkFi2kq2DtKcR2Y8wpTacRdQ3aZYB59ggiOmrS6sFevgDNr9GW6pzRAZdsQsC3rV3x4i6uQha8+sB2h9am9c6rVBDj9ixr5k007rIs+CGV65pl3wXjRi2hrKYjFtM/rI02JaW3XaPYtGtZHHY9qL0lN7QuO2yBMzpenLTvtkZNos+AY1ZcpObtoLtWmrj6TNlCOuJqZ9M3PkaDBlIBHCmwpHyHpjSgMS2ryhcIqsmsWULmR0eTPhK7LsMdMOKHdJM+nw8E+8ZoYDOT3eRDDDuz6HNt7VeszaQtYDJch+38WRZ51TDO+0Y54hylzy0XHib2JI83c0zIoLd1hAeUus1jenQe2HQ79Dg6wB3i1d/uoNpS2JrulgHWqcRxqySjoObrFjfUlLVrVrOtiGMmdCA+ZJx8hlEa9QZ2+oXcNLOkIWEUAHe22sYxqykGdoUV//5w6eoKlkTI3Gdbx7CVmQB10lDWqzSTpemyyoAW28ubYO++oOLqBPbUUtJknHrMnCRihdyaOTdAQsLHSgtSTS2BEHLK4DvQYWFW2lOtiHZi3Fko6fXCjgNVooV0nHl7tMBP1aAaXtJDvYgwGxdMmzLzu1JUyYNSU7IAwiiZ8OJrxKlTzI38QnMORFoqSn8Fh9gikvIa/UVejgDMZMQ9mOOa8WwKCRyysslF6hn2HSwZX4+ew1KGEPCSZKhoqHMw9mLd1rO8aUMYZpexbRV/2AsYBxy7/t3NtuglAQheFR6wEPVEQtaq1WxQNqnfd/urY08QJFYHZS15D9vcHckMzOz/QWA9/3jqHrbmbr1bT10a90ncQcoiclgKY/Vq81q6P2JJqfI+NHPqdDSMRzsEtIXmYGcQcQk2fwKgHxTCIVJGMWwTu6sWGxPSFx+QpkOfz3QcYEJWQhtGsbR5aKCIrHInjXNsSDeITFZ6ELYZEMAnltY8AyawKz4KJAr21IBzkRmB6LOIRGOEhIaHYsciA0uxIshwa/DLQIzrAEy2HswBIBwck9yNOvbWT4YgHEU4zbEiyHsQsXhnmKccmxp2cbxvb8CyDbMBXwD4hsw1BQguUw9s4Mk20YOTFQtmHiDJVtGJhjZRtyEVi2ITbhnLBOMd5qOvqXwz9RFy3bkJpU0LINeTCsJdvIztHVZhsJo77SbOPG6FNltpFQqMxsE7hmS+9ymJxE7XKYUGupyzZS1Kbaso00tbWybONBTadyObyjPlaVbTycRFO28Uh9oyjbEJ/E2JImnVDXy1y6zpHvW5E2npJsI5unI9vIwVe3HKYZaMg2clkoyDby6Wl5mcv0Bp9t5DVEzzZyG4JnG/kdsLONArbQ2UYRlwZwtlHIsoGbbRSdRNtymGbf0LYcpgleQbMNwdUCbcthmrP2j++VjqdSy7Isy7Isy4LxDTcBnqEPd5jdAAAAAElFTkSuQmCC" alt="feishu">
|
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
|
||||||
|
</a>
|
||||||
|
<a href="https://docs.github.com/en/developers/apps">
|
||||||
|
<img src="https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github" alt="github"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bot.q.qq.com/wiki/">
|
<a href="https://bot.q.qq.com/wiki/">
|
||||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC+lBMVEUAAAApRHRAbvYyVI4dMlsNGjcvTH5anN0/a5xdo+Vdpvpms/dMfvdcoPZjr/hJfu5krvA9Z+8/a+M9adxQjNdYmdVAbdU4XtRRjcxFd8wyVbM+aZowUJwpRJlZneRIevU5YORWl+1fqOZVle9ZnOpEdOk/aelepus/a+NQiuVKgeQ6YeRQi905X91EdtxAbthZnNlRjdk5YdpQjuhLgehhquc7Y+c6Y8k3XctWlcs0V8tJfMMzVsM9aMJVksdHebVKgLQyVJknP5REdKD///9irv9Ccf5EdP9GfP9jsf7+/f5SkP9mtf9GeP/+/vxJfv5Ukv9Ng/9boP9Gef1ls/9Ulf9Wl//+//5KgP9Nhv9dpP9Oiv9Tk/5Abv9Lg/9Fdv9NiP7///5Qj/7+/fxfpv9Nh/xhq/9QjP9Wm/79//1co/9ir/5fqf5bov9Qi//8/f1XmP1Wl/1ksv/7/v38/vv9/vlgqv9Znf9IfPzZ5fdeqP9nt/5anv5Xmf9Uk/1anvxpuf73/P34+/tot/9hrf1anvpMhvpSjv1pt/9ZnP1aofxTkfxNivza5/dOhPf+//1Ri/z+//vz+fvw9/tIf/v6/fpdofrI2PlYmfnF3Pe0y/Pm7/jW4/e0zvRapPxXlfpTkvpGevri7ffS3vVMiP5XnvxXnPtQivn3/Ph0r/dgpPZ1qfXi6/S9z/NYmf/z+v5Yn/7L2vlfmflUk/hXi/dRgvSw0fGnu+1dqP3t9Pthq/vA1vjc6vfU4vd4sfdepPdsn/dam/fU5fawzfRjjfRhi/NJhP0/a/y80fhTjfhrrfeXvvZuk/WyyPR9rvSqz/OcwfOmv/OSuPO0yfKguPCWr/BPi+9MheRKgv1gsPzq9PpgpvpVjvrN2/jG1vdonvbA0PSrx/OtxPCkxPDp8PvN4ftnuPuCufloqPiex/a3z/WPuvWQqvR2mfRplvReivSOqvOKufKGufKCrPKBoPJzlfFjr+88Ze6ftux8nOxxq/SGr/CFr/Cvx+3R19+QAAAARHRSTlMAEP4ZEwUEvC3S/v7+/Pv08PDXvJqampqSgEtJLS3f/Pz4+O/n5+bl4tTU1Lq6ure2trGdnZ2dlI6Afl5eXldMS0lJLRAR2gcAAAewSURBVFjDlZcFfBJRHMdvdnd3d3cn1qk3h4Et7EQ965h3KKgTQUGdOgYMHII6a5vO2d3d3d3d3X4+/t974A51xvfDu7vfP348HsdxR6VAulaN6lUu8SHVgQOpPpSoXK9Rq3TUf5CmSdWcB/oFcCBn1SZp/rG9aP6c/X5LzvxF/6W9Vqq2KZKq1t8sgvKT9pQt8gf9qb9QOVLWjux+K8oVSrE9bYNU7f6BVA3SpjD93O3+kdy//Ripcw34QefOnaViwM8iV+rf9Ffo7Kdr166du87vSgQGZGcIJocq/OIQlAv1EbAHDL/0hXzOhFxBP61f7vbJdJ0PFfN1RBB0MIPAUO7Alazvz+l0WY4+Pbthw4az66QG63Do6dEsuh+W9QO+//moVRet071L8phomUzBGPcGBwe3hxdmn5FVyGS0yZP0TodpHzy/kGQBygYjonUvbtpFraDwGUjYZ2JRVNSKK26+0EXjWNnkZciH26OjPy3gOZ5nVVCq+p0BzfK8wC/4FB2MLfL5+4sU7wkER7/dhN6aYxia0zKmvT0l7DUxHE1zHMcoVLJNb6ODUbB4EYpQHddcPHpXK5OpWNHp8CxbtmzmPqnBvpnLAIeTQdPT3j16EUer+yaQpT8i8pqAVi9h4cu1kZGR69bt7y9h/7p1EFz7cmEC+iTiwkgczUKmkC8ECkIs54wiI4j2j5EXQwj9QyCBNgCRIZbIj3ZRYET+nCVkP4TwKqTJHoJTXlbFscZzlv6WXr1CkpGKXhb0PozVynoj1SiTHV3lmvYC1JZnCQzNOG9a1BY1aAlKqbBYLEm8jWYSnpGypmBQTQ0sUp9iVCqr/bl60SK1UqlchIIzYCABB/BCIRR/breqVMwp9SIlUA2u39mVCPUWBlZQHz8DiT591t5euPD2WiUR6rXfiAKhnBHvhXVkrs9AGWX2dFTLGX0AZfxCTiaTHYwChTjl1LL8HZ+YccvJa52nfCL+IHyT3Lb4kVi1pAqOBPqMjPfSYHBmZBQSI6Nm0TTDXicCFCsy9HWfiDqGDPTxIzEFqTqDMVF6BTIYHEXUKhDMrMGEKddstIr2qajQYzT6sIeIrENVCsVc1VuRQeiU0FAYU4gBEkhfY5CBT4TCDJABkZWoCx0xxGB26BSspqyiFSowwAKUSNvoWUQQA1p/NRTLC9R5El+jZ5BBR9Qzffr0VbTICrNiQSC9yskKrN9g+hkoVOljp2N5njrUBdExlhj07UI4qGVZ7ePpREw/KLJa5xxfqu9sBRjMjCXJQ5SvaI3ehg0mELlz94oVu3ei3IQJE2J3eu0rdt/AAvAZoEMwp2L7Iias0aOlAYO+hHmrV89De5KVqAndicEaUhlLremOCJuHDeZ0D+v+V4jBPFK5hjrfA+E3CAvr8RfCwvwGWJ6nLmgQYTF6Gr4cZACCELN6Z+Kbr1/eJO5cHaOBBAyNpkeYZg86kWbGgAAuUBXDw8OXh4fH6BlkoNGgY43mxumkbR6HySiIomBKcHi2JZ2+odHELIdSzR5YbevMmHBMRar2EEyMXgXxOcvD4Viza88mnuNYhmNpDMMwIr9pzy4NJMOXIwMGDHBfbaog3k+LQzNgHscNmTZkSNwut0mgaQH6OK0NtmDG8yb3rrghY8YMiZuD5jozbhoSQwpShaeNAaYt3obWYIt58TQkzKe3JYisKCoUNKegwUFI8J7257agSi8SIAtT6UqPRxjc+De2axocLzYbzObjSVtX8Dyj1TICv2Jr0nEzsHLMmPFxZvS75d2GMSDGl4bbxypyuXy8wXBLUMloe6JBvtJgGB8BjitdiUfmuO/dc885kuhaaRg/fiVUwUi00zKVcAuqoLEKRVGN5UCE4bhJRdPiFrPZJZcQESEPwOUybxFpWmU6bsCZxuiyXnKEfIQ8wjWTZxSs6WRExNwRfiCByn4IOSRPwr+kit/qikChkvjmNc/Qob2HznWd5G1WgXbc2TF36A9GkI0fqLrj4AWrjT/pmju0d++heShEhqy9EUt3CwKso9F75OHSHTuWLp3bW8LcpSj28IjXCCsoCLuv4GjWDBSm5sCBSG6/zMNJDldjh2fBggVbEwdicOXAz1sh5HHwrBbON3bTdpKoCc1kCrg00xMHzICzwanD0VrTkoESlpi06L/dZuMUMoXjSaaNKIgmQMgzFti48corj1aEk88KJwpjXDJWwhIjSysUVi0HJ4bn1ZWNG1EwT/ItTpnhhCVuO8sKCjDgjNuHSwAD9M8lsJzdvcQXKyO502ueaTQwaNDmza/dl50+g9EStsMMwMB52f168+bho4dDKFNzSkKBQX4mH37w6MT69etPHCaacBiHHj04PHnQoNGkrkDgjWb6NslMJrSRhjZPHhQYSp+WCiCoWHJu2LBhl2BIDSASGMoY9MvNdjFfKUA2RCRHpQbFfne7nrHDqA4dOozCG/Qie6mAQURG6P+VoPS4DED7wONAkT4ohUeeAtm6/QPZCqSlUqJF+U6dOnVDA23Q/ldRvsUfH/vyZuv0R7LlDaL+TOsamSdNmjTuBwEic43W1N/JkDfH1EmIqT78xznyZvjXh+9m6XNM/Ikc6Zulof6DdIUb1s1Y6n3m+/czvy+VsW7Dwik9/n8HzjZEy9x05tIAAAAASUVORK5CYII=" alt="QQ频道">
|
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
|
||||||
|
</a>
|
||||||
|
<!-- <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||||
|
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk"> -->
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
|
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
|
||||||
<img src="https://img.shields.io/badge/qq%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat">
|
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat Group">
|
||||||
|
</a>
|
||||||
|
<a href="https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka">
|
||||||
|
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-5492ff?style=flat-square" alt="QQ Channel">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://t.me/botuniverse">
|
<a href="https://t.me/botuniverse">
|
||||||
<img src="https://img.shields.io/badge/telegram-botuniverse-blue?style=flat-square" alt="Telegram Channel">
|
<img src="https://img.shields.io/badge/telegram-botuniverse-blue?style=flat-square" alt="Telegram Channel">
|
||||||
@@ -57,16 +85,18 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://v2.nonebot.dev/">文档</a>
|
<a href="https://nonebot.dev/">文档</a>
|
||||||
·
|
·
|
||||||
<a href="https://v2.nonebot.dev/guide/installation.html">安装</a>
|
<a href="https://nonebot.dev/docs/quick-start">快速上手</a>
|
||||||
·
|
|
||||||
<a href="https://v2.nonebot.dev/guide/getting-started.html">开始使用</a>
|
|
||||||
·
|
·
|
||||||
<a href="#插件">文档打不开?</a>
|
<a href="#插件">文档打不开?</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- TODO: asciinema for install -->
|
<p align="center">
|
||||||
|
<a href="https://asciinema.org/a/569440">
|
||||||
|
<img src="https://nonebot.dev/img/setup.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
@@ -76,23 +106,40 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
|||||||
|
|
||||||
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
|
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
|
||||||
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
|
- 易于开发:配合 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 等)
|
|
||||||
- [Mirai-API-HTTP 协议](https://github.com/project-mirai/mirai-api-http)
|
|
||||||
- [钉钉](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)) | ✅ | 控制台交互 |
|
||||||
|
| Red ([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQ 协议 |
|
||||||
|
| Discord ([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 |
|
||||||
|
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||||
|
| 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 协议,由社区贡献 |
|
||||||
|
| Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa),[协议](https://webstatic.mihoyo.com/vila/bot/doc/)) | ↗️ | 米游社大别野 Bot 协议,由社区贡献 |
|
||||||
|
|
||||||
|
- 坚实后盾:支持多种 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
|
## 什么不是 NoneBot2
|
||||||
|
|
||||||
@@ -104,16 +151,21 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
|||||||
|
|
||||||
## 即刻开始
|
## 即刻开始
|
||||||
|
|
||||||
~~完整~~文档可以在 [这里](https://v2.nonebot.dev/) 查看。
|
~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。
|
||||||
|
|
||||||
懒得看文档?下面是快速安装指南:
|
懒得看文档?下面是快速安装指南:
|
||||||
|
|
||||||
1. (**强烈建议**)使用你喜欢的 Python 环境管理工具创建新的虚拟环境。
|
1. 安装 [pipx](https://pypa.github.io/pipx/)
|
||||||
|
|
||||||
2. 使用 `pip` (或其他) 安装 NoneBot 脚手架。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install nb-cli
|
python -m pip install --user pipx
|
||||||
|
python -m pipx ensurepath
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装脚手架
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install nb-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 使用脚手架创建项目
|
3. 使用脚手架创建项目
|
||||||
@@ -122,6 +174,12 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
|||||||
nb create
|
nb create
|
||||||
```
|
```
|
||||||
|
|
||||||
|
4. 运行项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nb run
|
||||||
|
```
|
||||||
|
|
||||||
## 社区资源
|
## 社区资源
|
||||||
|
|
||||||
### 常见问题
|
### 常见问题
|
||||||
@@ -148,33 +206,40 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
|||||||
或者尝试以下镜像:
|
或者尝试以下镜像:
|
||||||
|
|
||||||
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
- [文档镜像(中国境内)](https://nb2.baka.icu)
|
||||||
- [文档镜像(Vercel)](https://nonebot2-vercel-mirror.vercel.app)
|
|
||||||
|
|
||||||
- 其他插件请查看 [商店](https://v2.nonebot.dev/store.html)
|
- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
`NoneBot` 采用 `MIT` 许可证进行开源
|
`NoneBot` 采用 `MIT` 许可证进行开源
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
```text
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
```
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
如果你在使用过程中发现任何问题,可以 [提交 Issue](https://github.com/nonebot/nonebot2/issues/new) 或自行 Fork 修改后提交 Pull Request。
|
请参考 [贡献指南](./CONTRIBUTING.md)
|
||||||
|
|
||||||
如果你要提交 Pull Request,请确保你的代码风格和项目已有的代码保持一致,遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/),变量命名清晰,有适当的注释与测试代码,**并且请以 `dev` 分支作为 Pull Request 目标分支**。
|
## 鸣谢
|
||||||
|
|
||||||
<!--TODO: Add a CONTRIBUTING.md-->
|
### 赞助者
|
||||||
|
|
||||||
### 鸣谢
|
感谢以下赞助者对 NoneBot 项目提供的资金支持:
|
||||||
|
|
||||||
|
<a href="https://assets.nonebot.dev/sponsors.svg">
|
||||||
|
<img src='https://assets.nonebot.dev/sponsors.svg'/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 开发者
|
||||||
|
|
||||||
感谢以下开发者对 NoneBot2 作出的贡献:
|
感谢以下开发者对 NoneBot2 作出的贡献:
|
||||||
|
|
||||||
<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
|
<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2" />
|
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
|
||||||
# from the environment for the first two.
|
|
||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 1
|
|
||||||
id: index
|
|
||||||
slug: /api
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot 模块
|
|
||||||
===============
|
|
||||||
|
|
||||||
.. automodule:: nonebot
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
id: index
|
|
||||||
slug: /api/adapters/
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.adapters 模块
|
|
||||||
=====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters._adapter
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters._bot
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters._message
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters._event
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: nonebot.adapters._template
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# This file only contains a selection of the most common options. For a full
|
|
||||||
# list see the documentation:
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
||||||
|
|
||||||
# -- Path setup --------------------------------------------------------------
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
#
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
|
|
||||||
project = 'nonebot'
|
|
||||||
copyright = '2020, richardchien'
|
|
||||||
author = 'richardchien'
|
|
||||||
|
|
||||||
# The short X.Y version
|
|
||||||
version = '2.0.0'
|
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
|
||||||
release = '2.0.0'
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'sphinx.ext.viewcode',
|
|
||||||
'sphinx.ext.todo',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#
|
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
|
||||||
# Usually you set "language" from the command line for these cases.
|
|
||||||
language = 'zh_CN'
|
|
||||||
|
|
||||||
master_doc = "README"
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
html_theme = 'alabaster'
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# html_baseurl = '/api/'
|
|
||||||
|
|
||||||
# -- Extension configuration -------------------------------------------------
|
|
||||||
|
|
||||||
# -- Options for autodoc extension ----------------------------------------------
|
|
||||||
autodoc_default_options = {'member-order': 'bysource'}
|
|
||||||
autodoc_inherit_docstrings = False
|
|
||||||
autodoc_typehints = 'none'
|
|
||||||
|
|
||||||
# -- Options for todo extension ----------------------------------------------
|
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
|
||||||
todo_include_todos = True
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 2
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.config 模块
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.config
|
|
||||||
:members: Env, Config
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 8
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.dependencies 模块
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.dependencies
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
id: index
|
|
||||||
slug: /api/drivers/
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.drivers 模块
|
|
||||||
=====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
NoneBot.drivers.aiohttp 模块
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers.aiohttp
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
NoneBot.drivers.fastapi 模块
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers.fastapi
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
NoneBot.drivers.httpx 模块
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers.httpx
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
NoneBot.drivers.quart 模块
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers.quart
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
NoneBot.drivers.websockets 模块
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.drivers.websockets
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 12
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.exception 模块
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.exception
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 9
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.log 模块
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.log
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set SOURCEDIR=.
|
|
||||||
set BUILDDIR=_build
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
|
||||||
if errorlevel 9009 (
|
|
||||||
echo.
|
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
||||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
||||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
||||||
echo.may add the Sphinx directory to PATH.
|
|
||||||
echo.
|
|
||||||
echo.If you don't have Sphinx installed, grab it from
|
|
||||||
echo.http://sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:help
|
|
||||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
||||||
|
|
||||||
:end
|
|
||||||
popd
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 5
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.matcher 模块
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.matcher
|
|
||||||
:members:
|
|
||||||
:private-members:
|
|
||||||
:special-members: __init__
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 4
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.message 模块
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.message
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 7
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.permission 模块
|
|
||||||
=======================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.permission
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members:
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 3
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.plugin 模块
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.plugin
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members: __init__
|
|
||||||
|
|
||||||
.. automodule:: nonebot.plugin.plugin
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members: __init__
|
|
||||||
|
|
||||||
.. automodule:: nonebot.plugin.on
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members: __init__
|
|
||||||
|
|
||||||
.. automodule:: nonebot.plugin.load
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members: __init__
|
|
||||||
|
|
||||||
.. automodule:: nonebot.plugin.export
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
:special-members: __init__
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 6
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.rule 模块
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.rule
|
|
||||||
:members:
|
|
||||||
:special-members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 11
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.typing 模块
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.typing
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
\-\-\-
|
|
||||||
sidebar_position: 10
|
|
||||||
\-\-\-
|
|
||||||
|
|
||||||
NoneBot.utils 模块
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: nonebot.utils
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
@@ -1,164 +1,223 @@
|
|||||||
"""
|
"""本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。
|
||||||
快捷导入
|
|
||||||
========
|
|
||||||
|
|
||||||
为方便使用,``nonebot`` 模块从子模块导入了部分内容
|
## 快捷导入
|
||||||
|
|
||||||
- ``on_message`` => ``nonebot.plugin.on_message``
|
为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入:
|
||||||
- ``on_notice`` => ``nonebot.plugin.on_notice``
|
|
||||||
- ``on_request`` => ``nonebot.plugin.on_request``
|
- `on` => {ref}``on` <nonebot.plugin.on.on>`
|
||||||
- ``on_metaevent`` => ``nonebot.plugin.on_metaevent``
|
- `on_metaevent` => {ref}``on_metaevent` <nonebot.plugin.on.on_metaevent>`
|
||||||
- ``on_startswith`` => ``nonebot.plugin.on_startswith``
|
- `on_message` => {ref}``on_message` <nonebot.plugin.on.on_message>`
|
||||||
- ``on_endswith`` => ``nonebot.plugin.on_endswith``
|
- `on_notice` => {ref}``on_notice` <nonebot.plugin.on.on_notice>`
|
||||||
- ``on_keyword`` => ``nonebot.plugin.on_keyword``
|
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
|
||||||
- ``on_command`` => ``nonebot.plugin.on_command``
|
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
|
||||||
- ``on_shell_command`` => ``nonebot.plugin.on_shell_command``
|
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
|
||||||
- ``on_regex`` => ``nonebot.plugin.on_regex``
|
- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`
|
||||||
- ``CommandGroup`` => ``nonebot.plugin.CommandGroup``
|
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
|
||||||
- ``Matchergroup`` => ``nonebot.plugin.MatcherGroup``
|
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||||
- ``load_plugin`` => ``nonebot.plugin.load_plugin``
|
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||||
- ``load_plugins`` => ``nonebot.plugin.load_plugins``
|
- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`
|
||||||
- ``load_all_plugins`` => ``nonebot.plugin.load_all_plugins``
|
- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`
|
||||||
- ``load_from_json`` => ``nonebot.plugin.load_from_json``
|
- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`
|
||||||
- ``load_from_toml`` => ``nonebot.plugin.load_from_toml``
|
- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`
|
||||||
- ``load_builtin_plugins`` => ``nonebot.plugin.load_builtin_plugins``
|
- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`
|
||||||
- ``get_plugin`` => ``nonebot.plugin.get_plugin``
|
- `load_plugins` => {ref}``load_plugins` <nonebot.plugin.load.load_plugins>`
|
||||||
- ``get_loaded_plugins`` => ``nonebot.plugin.get_loaded_plugins``
|
- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`
|
||||||
- ``export`` => ``nonebot.plugin.export``
|
- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`
|
||||||
- ``require`` => ``nonebot.plugin.require``
|
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
|
||||||
|
- `load_builtin_plugin` =>
|
||||||
|
{ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
|
||||||
|
- `load_builtin_plugins` =>
|
||||||
|
{ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||||
|
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
|
||||||
|
- `get_plugin_by_module_name` =>
|
||||||
|
{ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
|
||||||
|
- `get_loaded_plugins` =>
|
||||||
|
{ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
||||||
|
- `get_available_plugin_names` =>
|
||||||
|
{ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
||||||
|
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 0
|
||||||
|
description: nonebot 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib
|
import os
|
||||||
from typing import Any, Dict, Type, Optional
|
from importlib.metadata import version
|
||||||
|
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
|
||||||
|
|
||||||
import pkg_resources
|
import loguru
|
||||||
|
from pydantic.env_settings import DotenvType
|
||||||
|
|
||||||
from nonebot.adapters import Bot
|
|
||||||
from nonebot.utils import escape_tag
|
|
||||||
from nonebot.config import Env, Config
|
from nonebot.config import Env, Config
|
||||||
from nonebot.log import logger, default_filter
|
from nonebot.log import logger as logger
|
||||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
from nonebot.adapters import Bot, Adapter
|
||||||
|
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||||
|
from nonebot.drivers import Driver, ASGIMixin, combine_driver
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
|
__version__ = version("nonebot2")
|
||||||
__version__ = _dist.version
|
except Exception: # pragma: no cover
|
||||||
VERSION = _dist.parsed_version
|
|
||||||
except pkg_resources.DistributionNotFound: # pragma: no cover
|
|
||||||
__version__ = None
|
__version__ = None
|
||||||
VERSION = None
|
|
||||||
|
A = TypeVar("A", bound=Adapter)
|
||||||
|
|
||||||
_driver: Optional[Driver] = None
|
_driver: Optional[Driver] = None
|
||||||
|
|
||||||
|
|
||||||
def get_driver() -> Driver:
|
def get_driver() -> Driver:
|
||||||
"""
|
"""获取全局 {ref}`nonebot.drivers.Driver` 实例。
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取全局 Driver 对象。可用于在计划任务的回调中获取当前 Driver 对象。
|
可用于在计划任务的回调等情形中获取当前 {ref}`nonebot.drivers.Driver` 实例。
|
||||||
|
|
||||||
:返回:
|
返回:
|
||||||
|
全局 {ref}`nonebot.drivers.Driver` 对象
|
||||||
|
|
||||||
* ``Driver``: 全局 Driver 对象
|
异常:
|
||||||
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
:异常:
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
driver = nonebot.get_driver()
|
driver = nonebot.get_driver()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
if _driver is None:
|
if _driver is None:
|
||||||
raise ValueError("NoneBot has not been initialized.")
|
raise ValueError("NoneBot has not been initialized.")
|
||||||
return _driver
|
return _driver
|
||||||
|
|
||||||
|
|
||||||
def get_app() -> Any:
|
@overload
|
||||||
|
def get_adapter(name: str) -> Adapter:
|
||||||
"""
|
"""
|
||||||
:说明:
|
参数:
|
||||||
|
name: 适配器名称
|
||||||
|
|
||||||
获取全局 Driver 对应 Server App 对象。
|
返回:
|
||||||
|
指定名称的 {ref}`nonebot.adapters.Adapter` 对象
|
||||||
|
"""
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``Any``: Server App 对象
|
@overload
|
||||||
|
def get_adapter(name: Type[A]) -> A:
|
||||||
|
"""
|
||||||
|
参数:
|
||||||
|
name: 适配器类型
|
||||||
|
|
||||||
:异常:
|
返回:
|
||||||
|
指定类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||||
|
"""
|
||||||
|
|
||||||
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
|
|
||||||
|
|
||||||
:用法:
|
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
||||||
|
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||||
|
|
||||||
.. code-block:: python
|
异常:
|
||||||
|
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.ASGIMixin` 对应的 Server App 对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
Server App 对象
|
||||||
|
|
||||||
|
异常:
|
||||||
|
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||||
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
app = nonebot.get_app()
|
app = nonebot.get_app()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(
|
assert isinstance(driver, ASGIMixin), "app object is only available for asgi driver"
|
||||||
driver, ReverseDriver
|
|
||||||
), "app object is only available for reverse driver"
|
|
||||||
return driver.server_app
|
return driver.server_app
|
||||||
|
|
||||||
|
|
||||||
def get_asgi() -> Any:
|
def get_asgi() -> Any:
|
||||||
"""
|
"""获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的
|
||||||
:说明:
|
[ASGI](https://asgi.readthedocs.io/) 对象。
|
||||||
|
|
||||||
获取全局 Driver 对应 Asgi 对象。
|
返回:
|
||||||
|
ASGI 对象
|
||||||
|
|
||||||
:返回:
|
异常:
|
||||||
|
AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型
|
||||||
* ``Any``: Asgi 对象
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
:异常:
|
|
||||||
|
|
||||||
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
asgi = nonebot.get_asgi()
|
asgi = nonebot.get_asgi()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
driver, ReverseDriver
|
driver, ASGIMixin
|
||||||
), "asgi object is only available for reverse driver"
|
), "asgi object is only available for asgi driver"
|
||||||
return driver.asgi
|
return driver.asgi
|
||||||
|
|
||||||
|
|
||||||
def get_bot(self_id: Optional[str] = None) -> Bot:
|
def get_bot(self_id: Optional[str] = None) -> Bot:
|
||||||
"""
|
"""获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
||||||
:说明:
|
|
||||||
|
|
||||||
当提供 self_id 时,此函数是 get_bots()[self_id] 的简写;当不提供时,返回一个 Bot。
|
当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写;
|
||||||
|
当不提供时,返回一个 {ref}`nonebot.adapters.Bot`。
|
||||||
|
|
||||||
:参数:
|
参数:
|
||||||
|
self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的
|
||||||
|
{ref}`nonebot.adapters.Bot.self_id` 属性
|
||||||
|
|
||||||
* ``self_id: Optional[str]``: 用来识别 Bot 的 ID
|
返回:
|
||||||
|
{ref}`nonebot.adapters.Bot` 对象
|
||||||
|
|
||||||
:返回:
|
异常:
|
||||||
|
KeyError: 对应 self_id 的 Bot 不存在
|
||||||
|
ValueError: 没有传入 self_id 且没有 Bot 可用
|
||||||
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
* ``Bot``: Bot 对象
|
用法:
|
||||||
|
```python
|
||||||
:异常:
|
assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"]
|
||||||
|
|
||||||
* ``KeyError``: 对应 ID 的 Bot 不存在
|
|
||||||
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
|
|
||||||
* ``ValueError``: 没有传入 ID 且没有 Bot 可用
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
assert nonebot.get_bot('12345') == nonebot.get_bots()['12345']
|
|
||||||
|
|
||||||
another_unspecified_bot = nonebot.get_bot()
|
another_unspecified_bot = nonebot.get_bot()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
bots = get_bots()
|
bots = get_bots()
|
||||||
if self_id is not None:
|
if self_id is not None:
|
||||||
@@ -171,98 +230,80 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
|
|||||||
|
|
||||||
|
|
||||||
def get_bots() -> Dict[str, Bot]:
|
def get_bots() -> Dict[str, Bot]:
|
||||||
"""
|
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取所有通过 ws 连接 NoneBot 的 Bot 对象。
|
返回:
|
||||||
|
一个以 {ref}`nonebot.adapters.Bot.self_id` 为键
|
||||||
|
{ref}`nonebot.adapters.Bot` 对象为值的字典
|
||||||
|
|
||||||
:返回:
|
异常:
|
||||||
|
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化
|
||||||
* ``Dict[str, Bot]``: 一个以字符串 ID 为键,Bot 对象为值的字典
|
({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||||
|
|
||||||
:异常:
|
|
||||||
|
|
||||||
* ``ValueError``: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
bots = nonebot.get_bots()
|
bots = nonebot.get_bots()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
driver = get_driver()
|
return get_driver().bots
|
||||||
return driver.bots
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_dot_notation(
|
|
||||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
|
||||||
) -> Any:
|
|
||||||
modulename, _, cls = obj_str.partition(":")
|
|
||||||
if default_prefix is not None and modulename.startswith("~"):
|
|
||||||
modulename = default_prefix + modulename[1:]
|
|
||||||
module = importlib.import_module(modulename)
|
|
||||||
if not cls:
|
|
||||||
return getattr(module, default_attr)
|
|
||||||
instance = module
|
|
||||||
for attr_str in cls.split("."):
|
|
||||||
instance = getattr(instance, attr_str)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
||||||
drivers = obj_str.split("+")
|
drivers = obj_str.split("+")
|
||||||
DriverClass = _resolve_dot_notation(
|
DriverClass = resolve_dot_notation(
|
||||||
drivers[0], "Driver", default_prefix="nonebot.drivers."
|
drivers[0], "Driver", default_prefix="nonebot.drivers."
|
||||||
)
|
)
|
||||||
if len(drivers) == 1:
|
if len(drivers) == 1:
|
||||||
logger.trace(f"Detected driver {DriverClass} with no mixins.")
|
logger.trace(f"Detected driver {DriverClass} with no mixins.")
|
||||||
return DriverClass
|
return DriverClass
|
||||||
mixins = [
|
mixins = [
|
||||||
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||||
for mixin in drivers[1:]
|
for mixin in drivers[1:]
|
||||||
]
|
]
|
||||||
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
|
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
|
||||||
return combine_driver(DriverClass, *mixins)
|
return combine_driver(DriverClass, *mixins)
|
||||||
|
|
||||||
|
|
||||||
def init(*, _env_file: Optional[str] = None, **kwargs):
|
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])
|
||||||
|
)
|
||||||
|
|
||||||
初始化 NoneBot 以及 全局 Driver 对象。
|
|
||||||
|
|
||||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None:
|
||||||
|
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||||
|
|
||||||
你也可以传入自定义的 _env_file 来指定 NoneBot 从该文件读取配置。
|
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||||
|
|
||||||
:参数:
|
也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。
|
||||||
|
|
||||||
* ``_env_file: Optional[str]``: 配置文件名,默认从 .env.{env_name} 中读取配置
|
参数:
|
||||||
* ``**kwargs``: 任意变量,将会存储到 Config 对象里
|
_env_file: 配置文件名,默认从 `.env.{env_name}` 中读取配置
|
||||||
|
kwargs: 任意变量,将会存储到 {ref}`nonebot.drivers.Driver.config` 对象里
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``None``
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
nonebot.init(database=Database(...))
|
nonebot.init(database=Database(...))
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
global _driver
|
global _driver
|
||||||
if not _driver:
|
if not _driver:
|
||||||
logger.success("NoneBot is initializing...")
|
logger.success("NoneBot is initializing...")
|
||||||
env = Env()
|
env = Env()
|
||||||
|
_env_file = _env_file or f".env.{env.environment}"
|
||||||
config = Config(
|
config = Config(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
_common_config=env.dict(),
|
_env_file=(".env", _env_file)
|
||||||
_env_file=_env_file or f".env.{env.environment}",
|
if isinstance(_env_file, (str, os.PathLike))
|
||||||
|
else _env_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
default_filter.level = config.log_level
|
logger.configure(
|
||||||
|
extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher
|
||||||
|
)
|
||||||
logger.opt(colors=True).info(
|
logger.opt(colors=True).info(
|
||||||
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>"
|
||||||
)
|
)
|
||||||
@@ -270,38 +311,28 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
|
|||||||
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
|
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}"
|
||||||
)
|
)
|
||||||
|
|
||||||
DriverClass: Type[Driver] = _resolve_combine_expr(config.driver)
|
DriverClass = _resolve_combine_expr(config.driver)
|
||||||
_driver = DriverClass(env, config)
|
_driver = DriverClass(env, config)
|
||||||
|
|
||||||
|
|
||||||
def run(*args: Any, **kwargs: Any) -> None:
|
def run(*args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""启动 NoneBot,即运行全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||||
:说明:
|
|
||||||
|
|
||||||
启动 NoneBot,即运行全局 Driver 对象。
|
参数:
|
||||||
|
args: 传入 {ref}`nonebot.drivers.Driver.run` 的位置参数
|
||||||
:参数:
|
kwargs: 传入 {ref}`nonebot.drivers.Driver.run` 的命名参数
|
||||||
|
|
||||||
* ``*args``: 传入 Driver.run 的位置参数
|
|
||||||
* ``**kwargs``: 传入 Driver.run 的命名参数
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``None``
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
nonebot.run(host="127.0.0.1", port=8080)
|
nonebot.run(host="127.0.0.1", port=8080)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
logger.success("Running NoneBot...")
|
logger.success("Running NoneBot...")
|
||||||
get_driver().run(*args, **kwargs)
|
get_driver().run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
import nonebot.params as params
|
from nonebot.plugin import on as on
|
||||||
from nonebot.plugin import export as export
|
from nonebot.plugin import on_type as on_type
|
||||||
from nonebot.plugin import require as require
|
from nonebot.plugin import require as require
|
||||||
from nonebot.plugin import on_regex as on_regex
|
from nonebot.plugin import on_regex as on_regex
|
||||||
from nonebot.plugin import on_notice as on_notice
|
from nonebot.plugin import on_notice as on_notice
|
||||||
@@ -315,6 +346,7 @@ from nonebot.plugin import on_endswith as on_endswith
|
|||||||
from nonebot.plugin import CommandGroup as CommandGroup
|
from nonebot.plugin import CommandGroup as CommandGroup
|
||||||
from nonebot.plugin import MatcherGroup as MatcherGroup
|
from nonebot.plugin import MatcherGroup as MatcherGroup
|
||||||
from nonebot.plugin import load_plugins as load_plugins
|
from nonebot.plugin import load_plugins as load_plugins
|
||||||
|
from nonebot.plugin import on_fullmatch as on_fullmatch
|
||||||
from nonebot.plugin import on_metaevent as on_metaevent
|
from nonebot.plugin import on_metaevent as on_metaevent
|
||||||
from nonebot.plugin import on_startswith as on_startswith
|
from nonebot.plugin import on_startswith as on_startswith
|
||||||
from nonebot.plugin import load_from_json as load_from_json
|
from nonebot.plugin import load_from_json as load_from_json
|
||||||
@@ -322,4 +354,9 @@ from nonebot.plugin import load_from_toml as load_from_toml
|
|||||||
from nonebot.plugin import load_all_plugins as load_all_plugins
|
from nonebot.plugin import load_all_plugins as load_all_plugins
|
||||||
from nonebot.plugin import on_shell_command as on_shell_command
|
from nonebot.plugin import on_shell_command as on_shell_command
|
||||||
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
|
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
|
||||||
|
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
|
||||||
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
|
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
|
||||||
|
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
|
||||||
|
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
||||||
|
|
||||||
|
__autodoc__ = {"internal": False}
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
"""
|
"""本模块定义了协议适配基类,各协议请继承以下基类。
|
||||||
协议适配基类
|
|
||||||
============
|
|
||||||
|
|
||||||
各协议请继承以下基类,并使用 ``driver.register_adapter`` 注册适配器
|
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 0
|
||||||
|
description: nonebot.adapters 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Iterable
|
from nonebot.internal.adapter import Bot as Bot
|
||||||
|
from nonebot.internal.adapter import Event as Event
|
||||||
|
from nonebot.internal.adapter import Adapter as Adapter
|
||||||
|
from nonebot.internal.adapter import Message as Message
|
||||||
|
from nonebot.internal.adapter import MessageSegment as MessageSegment
|
||||||
|
from nonebot.internal.adapter import MessageTemplate as MessageTemplate
|
||||||
|
|
||||||
try:
|
__autodoc__ = {
|
||||||
import pkg_resources
|
"Bot": True,
|
||||||
|
"Event": True,
|
||||||
pkg_resources.declare_namespace(__name__)
|
"Adapter": True,
|
||||||
del pkg_resources
|
"Message": True,
|
||||||
except ImportError:
|
"Message.__getitem__": True,
|
||||||
import pkgutil
|
"Message.__contains__": True,
|
||||||
|
"Message._construct": True,
|
||||||
__path__: Iterable[str] = pkgutil.extend_path(__path__, __name__) # type: ignore
|
"MessageSegment": True,
|
||||||
del pkgutil
|
"MessageSegment.__str__": True,
|
||||||
except Exception:
|
"MessageSegment.__add__": True,
|
||||||
pass
|
"MessageTemplate": True,
|
||||||
|
}
|
||||||
from ._bot import Bot as Bot
|
|
||||||
from ._event import Event as Event
|
|
||||||
from ._adapter import Adapter as Adapter
|
|
||||||
from ._message import Message as Message
|
|
||||||
from ._message import MessageSegment as MessageSegment
|
|
||||||
from ._template import MessageTemplate as MessageTemplate
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import abc
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from typing import Any, Dict, AsyncGenerator
|
|
||||||
|
|
||||||
from ._bot import Bot
|
|
||||||
from nonebot.config import Config
|
|
||||||
from nonebot.drivers import (
|
|
||||||
Driver,
|
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
WebSocket,
|
|
||||||
ForwardDriver,
|
|
||||||
ReverseDriver,
|
|
||||||
HTTPServerSetup,
|
|
||||||
WebSocketServerSetup,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Adapter(abc.ABC):
|
|
||||||
def __init__(self, driver: Driver, **kwargs: Any):
|
|
||||||
self.driver: Driver = driver
|
|
||||||
self.bots: Dict[str, Bot] = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_name(cls) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self) -> Config:
|
|
||||||
return self.driver.config
|
|
||||||
|
|
||||||
def bot_connect(self, bot: Bot) -> None:
|
|
||||||
self.driver._bot_connect(bot)
|
|
||||||
self.bots[bot.self_id] = bot
|
|
||||||
|
|
||||||
def bot_disconnect(self, bot: Bot) -> None:
|
|
||||||
self.driver._bot_disconnect(bot)
|
|
||||||
self.bots.pop(bot.self_id, None)
|
|
||||||
|
|
||||||
def setup_http_server(self, setup: HTTPServerSetup):
|
|
||||||
if not isinstance(self.driver, ReverseDriver):
|
|
||||||
raise TypeError("Current driver does not support http server")
|
|
||||||
self.driver.setup_http_server(setup)
|
|
||||||
|
|
||||||
def setup_websocket_server(self, setup: WebSocketServerSetup):
|
|
||||||
if not isinstance(self.driver, ReverseDriver):
|
|
||||||
raise TypeError("Current driver does not support websocket server")
|
|
||||||
self.driver.setup_websocket_server(setup)
|
|
||||||
|
|
||||||
async def request(self, setup: Request) -> Response:
|
|
||||||
if not isinstance(self.driver, ForwardDriver):
|
|
||||||
raise TypeError("Current driver does not support http client")
|
|
||||||
return await self.driver.request(setup)
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
|
||||||
if not isinstance(self.driver, ForwardDriver):
|
|
||||||
raise TypeError("Current driver does not support websocket client")
|
|
||||||
async with self.driver.websocket(setup) as ws:
|
|
||||||
yield ws
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def _call_api(self, bot: Bot, api: str, **data) -> Any:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
``adapter`` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``api: str``: API 名称
|
|
||||||
* ``**data``: API 数据
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import abc
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from ._message import Message
|
|
||||||
from nonebot.utils import DataclassEncoder
|
|
||||||
|
|
||||||
|
|
||||||
class Event(abc.ABC, BaseModel):
|
|
||||||
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
extra = "allow"
|
|
||||||
json_encoders = {Message: DataclassEncoder}
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_type(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``Literal["message", "notice", "request", "meta_event"]``
|
|
||||||
* 其他自定义 ``str``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_event_name(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件名称的方法。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_event_description(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件描述的方法,通常为事件具体内容。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
|
||||||
|
|
||||||
def get_log_string(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件日志信息的方法,通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 ``NoLogException`` 异常。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
|
|
||||||
:异常:
|
|
||||||
|
|
||||||
- ``NoLogException``
|
|
||||||
"""
|
|
||||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_user_id(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件主体 id 的方法,通常是用户 id 。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_session_id(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_message(self) -> "Message":
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件消息内容的方法。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``Message``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_plaintext(self) -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取消息纯文本的方法,通常不需要修改,默认通过 ``get_message().extract_plain_text`` 获取。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``str``
|
|
||||||
"""
|
|
||||||
return self.get_message().extract_plain_text()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def is_tome(self) -> bool:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取事件是否与机器人有关的方法。
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
* ``bool``
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import abc
|
|
||||||
from copy import deepcopy
|
|
||||||
from dataclasses import field, asdict, dataclass
|
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Type,
|
|
||||||
Union,
|
|
||||||
Generic,
|
|
||||||
Mapping,
|
|
||||||
TypeVar,
|
|
||||||
Iterable,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ._template import MessageTemplate
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
TMS = TypeVar("TMS", covariant=True)
|
|
||||||
TM = TypeVar("TM", bound="Message")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MessageSegment(Mapping, abc.ABC, Generic[TM]):
|
|
||||||
"""消息段基类"""
|
|
||||||
|
|
||||||
type: str
|
|
||||||
"""
|
|
||||||
- 类型: ``str``
|
|
||||||
- 说明: 消息段类型
|
|
||||||
"""
|
|
||||||
data: Dict[str, Any] = field(default_factory=lambda: {})
|
|
||||||
"""
|
|
||||||
- 类型: ``Dict[str, Union[str, list]]``
|
|
||||||
- 说明: 消息段数据
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_message_class(cls) -> Type[TM]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""该消息段所代表的 str,在命令匹配部分使用"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(str(self))
|
|
||||||
|
|
||||||
def __ne__(self: T, other: T) -> bool:
|
|
||||||
return not self == other
|
|
||||||
|
|
||||||
def __add__(self, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
|
|
||||||
return self.get_message_class()(self) + other # type: ignore
|
|
||||||
|
|
||||||
def __radd__(self, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
|
|
||||||
return self.get_message_class()(other) + self # type: ignore
|
|
||||||
|
|
||||||
def __getitem__(self, key: str):
|
|
||||||
return getattr(self, key)
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Any):
|
|
||||||
return setattr(self, key, value)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
yield from asdict(self).keys()
|
|
||||||
|
|
||||||
def __contains__(self, key: Any) -> bool:
|
|
||||||
return key in asdict(self).keys()
|
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None):
|
|
||||||
return getattr(self, key, default)
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
return asdict(self).keys()
|
|
||||||
|
|
||||||
def values(self):
|
|
||||||
return asdict(self).values()
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return asdict(self).items()
|
|
||||||
|
|
||||||
def copy(self: T) -> T:
|
|
||||||
return deepcopy(self)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def is_text(self) -> bool:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class Message(List[TMS], abc.ABC):
|
|
||||||
"""消息数组"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self: TM,
|
|
||||||
message: Union[str, None, Mapping, Iterable[Mapping], TMS, TM, Any] = None,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``message: Union[str, list, dict, MessageSegment, Message, Any]``: 消息内容
|
|
||||||
"""
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if message is None:
|
|
||||||
return
|
|
||||||
elif isinstance(message, Message):
|
|
||||||
self.extend(message)
|
|
||||||
elif isinstance(message, MessageSegment):
|
|
||||||
self.append(message)
|
|
||||||
else:
|
|
||||||
self.extend(self._construct(message))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def template(cls: Type[TM], format_string: Union[str, TM]) -> MessageTemplate[TM]:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
根据创建消息模板, 用法和 ``str.format`` 大致相同, 但是可以输出消息对象, 并且支持以 ``Message`` 对象作为消息模板
|
|
||||||
并且提供了拓展的格式化控制符, 可以用适用于该消息类型的 ``MessageSegment`` 的工厂方法创建消息
|
|
||||||
|
|
||||||
:示例:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
>>> Message.template("{} {}").format("hello", "world") # 基础演示
|
|
||||||
Message(MessageSegment(type='text', data={'text': 'hello world'}))
|
|
||||||
>>> Message.template("{} {}").format(MessageSegment.image("file///..."), "world") # 支持消息段等对象
|
|
||||||
Message(MessageSegment(type='image', data={'file': 'file///...'}), MessageSegment(type='text', data={'text': 'world'}))
|
|
||||||
>>> Message.template( # 支持以Message对象作为消息模板
|
|
||||||
... MessageSegment.text('test {event.user_id}') + MessageSegment.face(233) +
|
|
||||||
... MessageSegment.text('test {event.message}')).format(event={'user_id':123456, 'message':'hello world'})
|
|
||||||
Message(MessageSegment(type='text', data={'text': 'test 123456'}),
|
|
||||||
MessageSegment(type='face', data={'face': 233}),
|
|
||||||
MessageSegment(type='text', data={'text': 'test hello world'}))
|
|
||||||
>>> Message.template("{link:image}").format(link='https://...') # 支持拓展格式化控制符
|
|
||||||
Message(MessageSegment(type='image', data={'file': 'https://...'}))
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``format_string: str``: 格式化字符串
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``MessageFormatter[TM]``: 消息格式化器
|
|
||||||
"""
|
|
||||||
return MessageTemplate(format_string, cls)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_segment_class(cls) -> Type[TMS]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "".join(str(seg) for seg in self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_validators__(cls):
|
|
||||||
yield cls._validate
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _validate(cls, value):
|
|
||||||
return cls(value)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _construct(msg: Union[str, Mapping, Iterable[Mapping], Any]) -> Iterable[TMS]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __add__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
|
|
||||||
result = self.copy()
|
|
||||||
result += other
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __radd__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
|
|
||||||
result = self.__class__(other) # type: ignore
|
|
||||||
return result + self
|
|
||||||
|
|
||||||
def __iadd__(self: TM, other: Union[str, Mapping, Iterable[Mapping]]) -> TM:
|
|
||||||
if isinstance(other, MessageSegment):
|
|
||||||
self.append(other)
|
|
||||||
elif isinstance(other, Message):
|
|
||||||
self.extend(other)
|
|
||||||
else:
|
|
||||||
self.extend(self._construct(other))
|
|
||||||
return self
|
|
||||||
|
|
||||||
def append(self: TM, obj: Union[str, TMS]) -> TM:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
添加一个消息段到消息数组末尾
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``obj: Union[str, MessageSegment]``: 要添加的消息段
|
|
||||||
"""
|
|
||||||
if isinstance(obj, MessageSegment):
|
|
||||||
super(Message, self).append(obj)
|
|
||||||
elif isinstance(obj, str):
|
|
||||||
self.extend(self._construct(obj))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unexpected type: {type(obj)} {obj}")
|
|
||||||
return self
|
|
||||||
|
|
||||||
def extend(self: TM, obj: Union[TM, Iterable[TMS]]) -> TM:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
拼接一个消息数组或多个消息段到消息数组末尾
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``obj: Union[Message, Iterable[MessageSegment]]``: 要添加的消息数组
|
|
||||||
"""
|
|
||||||
for segment in obj:
|
|
||||||
self.append(segment)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def copy(self: TM) -> TM:
|
|
||||||
return deepcopy(self)
|
|
||||||
|
|
||||||
def extract_plain_text(self: "Message[MessageSegment]") -> str:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
提取消息内纯文本消息
|
|
||||||
"""
|
|
||||||
|
|
||||||
return "".join(str(seg) for seg in self if seg.is_text())
|
|
||||||
@@ -1,115 +1,128 @@
|
|||||||
|
"""本模块定义了 NoneBot 本身运行所需的配置项。
|
||||||
|
|
||||||
|
NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
|
||||||
|
[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。
|
||||||
|
|
||||||
|
配置项需符合特殊格式或 json 序列化格式
|
||||||
|
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 1
|
||||||
|
description: nonebot.config 模块
|
||||||
"""
|
"""
|
||||||
配置
|
|
||||||
====
|
|
||||||
|
|
||||||
NoneBot 使用 `pydantic`_ 以及 `python-dotenv`_ 来读取配置。
|
|
||||||
|
|
||||||
配置项需符合特殊格式或 json 序列化格式。详情见 `pydantic Field Type`_ 文档。
|
|
||||||
|
|
||||||
.. _pydantic:
|
|
||||||
https://pydantic-docs.helpmanual.io/
|
|
||||||
.. _python-dotenv:
|
|
||||||
https://saurabh-kumar.com/python-dotenv/
|
|
||||||
.. _pydantic Field Type:
|
|
||||||
https://pydantic-docs.helpmanual.io/usage/types/
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from typing import Any, Set, Dict, Tuple, Union, Mapping, Optional
|
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
|
||||||
|
|
||||||
from pydantic import BaseSettings, IPvAnyAddress
|
from pydantic.utils import deep_update
|
||||||
|
from pydantic.fields import Undefined, UndefinedType
|
||||||
|
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
||||||
from pydantic.env_settings import (
|
from pydantic.env_settings import (
|
||||||
|
DotenvType,
|
||||||
SettingsError,
|
SettingsError,
|
||||||
EnvSettingsSource,
|
EnvSettingsSource,
|
||||||
InitSettingsSource,
|
InitSettingsSource,
|
||||||
SettingsSourceCallable,
|
SettingsSourceCallable,
|
||||||
read_env_file,
|
|
||||||
env_file_sentinel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.utils import escape_tag
|
|
||||||
|
|
||||||
|
|
||||||
class CustomEnvSettings(EnvSettingsSource):
|
class CustomEnvSettings(EnvSettingsSource):
|
||||||
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
||||||
"""
|
"""从环境变量和 dotenv 配置文件中读取配置项。"""
|
||||||
Build environment variables suitable for passing to the Model.
|
|
||||||
"""
|
d: Dict[str, Any] = {}
|
||||||
d: Dict[str, Optional[str]] = {}
|
|
||||||
|
|
||||||
if settings.__config__.case_sensitive:
|
if settings.__config__.case_sensitive:
|
||||||
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
|
env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover
|
||||||
else:
|
else:
|
||||||
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
||||||
|
|
||||||
env_file_vars: Dict[str, Optional[str]] = {}
|
env_file_vars = self._read_env_files(settings.__config__.case_sensitive)
|
||||||
env_file = (
|
env_vars = {**env_file_vars, **env_vars}
|
||||||
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,
|
|
||||||
case_sensitive=settings.__config__.case_sensitive,
|
|
||||||
)
|
|
||||||
env_vars = {**env_file_vars, **env_vars}
|
|
||||||
|
|
||||||
for field in settings.__fields__.values():
|
for field in settings.__fields__.values():
|
||||||
env_val: Optional[str] = None
|
env_val: Union[str, None, UndefinedType] = Undefined
|
||||||
for env_name in field.field_info.extra["env_names"]:
|
for env_name in field.field_info.extra["env_names"]:
|
||||||
env_val = env_vars.get(env_name)
|
env_val = env_vars.get(env_name, Undefined)
|
||||||
if env_name in env_file_vars:
|
if env_name in env_file_vars:
|
||||||
del env_file_vars[env_name]
|
del env_file_vars[env_name]
|
||||||
if env_val is not None:
|
if env_val is not Undefined:
|
||||||
break
|
break
|
||||||
|
|
||||||
if env_val is None:
|
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||||
continue
|
if is_complex:
|
||||||
|
if isinstance(env_val, UndefinedType):
|
||||||
|
# field is complex but no value found so far, try explode_env_vars
|
||||||
|
if env_val_built := self.explode_env_vars(field, env_vars):
|
||||||
|
d[field.alias] = env_val_built
|
||||||
|
elif env_val is None:
|
||||||
|
d[field.alias] = env_val
|
||||||
|
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():
|
if isinstance(env_val, dict):
|
||||||
try:
|
d[field.alias] = deep_update(
|
||||||
env_val = settings.__config__.json_loads(env_val)
|
env_val, self.explode_env_vars(field, env_vars)
|
||||||
except ValueError as e: # pragma: no cover
|
)
|
||||||
raise SettingsError(
|
else:
|
||||||
f'error parsing JSON for "{env_name}"' # type: ignore
|
d[field.alias] = env_val
|
||||||
) from e
|
elif not isinstance(env_val, UndefinedType):
|
||||||
d[field.alias] = env_val
|
# 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:
|
# remain user custom config
|
||||||
for env_name, env_val in env_file_vars.items():
|
for env_name in env_file_vars:
|
||||||
if (env_val is None or len(env_val) == 0) and env_name in env_vars:
|
env_val = env_vars[env_name]
|
||||||
env_val = env_vars[env_name]
|
if env_val and (val_striped := env_val.strip()):
|
||||||
|
# there's a value, decode that as JSON
|
||||||
try:
|
try:
|
||||||
if env_val:
|
env_val = settings.__config__.parse_env_var(env_name, val_striped)
|
||||||
env_val = settings.__config__.json_loads(env_val.strip())
|
except ValueError:
|
||||||
except ValueError as e:
|
logger.trace(
|
||||||
logger.opt(colors=True, exception=e).trace(
|
"Error while parsing JSON for "
|
||||||
f"Error while parsing JSON for {escape_tag(env_name)}. Assumed as string."
|
f"{env_name!r}={val_striped!r}. "
|
||||||
|
"Assumed as string."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# explode value when it's a nested dict
|
||||||
|
env_name, *nested_keys = env_name.split(self.env_nested_delimiter)
|
||||||
|
if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):
|
||||||
|
result = {}
|
||||||
|
*keys, last_key = nested_keys
|
||||||
|
_tmp = result
|
||||||
|
for key in keys:
|
||||||
|
_tmp = _tmp.setdefault(key, {})
|
||||||
|
_tmp[last_key] = env_val
|
||||||
|
d[env_name] = deep_update(d.get(env_name, {}), result)
|
||||||
|
elif not nested_keys:
|
||||||
d[env_name] = env_val
|
d[env_name] = env_val
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class BaseConfig(BaseSettings):
|
class BaseConfig(BaseSettings):
|
||||||
# dummy getattr for pylance checking, actually not used
|
if TYPE_CHECKING:
|
||||||
def __getattr__(self, name: str) -> Any: # pragma: no cover
|
# dummy getattr for pylance checking, actually not used
|
||||||
return self.__dict__.get(name)
|
def __getattr__(self, name: str) -> Any: # pragma: no cover
|
||||||
|
return self.__dict__.get(name)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
extra = Extra.allow
|
||||||
|
env_nested_delimiter = "__"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def customise_sources(
|
def customise_sources(
|
||||||
cls,
|
cls,
|
||||||
@@ -121,7 +134,10 @@ class BaseConfig(BaseSettings):
|
|||||||
return (
|
return (
|
||||||
init_settings,
|
init_settings,
|
||||||
CustomEnvSettings(
|
CustomEnvSettings(
|
||||||
env_settings.env_file, env_settings.env_file_encoding
|
env_settings.env_file,
|
||||||
|
env_settings.env_file_encoding,
|
||||||
|
env_settings.env_nested_delimiter,
|
||||||
|
env_settings.env_prefix_len,
|
||||||
),
|
),
|
||||||
InitSettingsSource(common_config),
|
InitSettingsSource(common_config),
|
||||||
file_secret_settings,
|
file_secret_settings,
|
||||||
@@ -129,159 +145,106 @@ class BaseConfig(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class Env(BaseConfig):
|
class Env(BaseConfig):
|
||||||
"""
|
"""运行环境配置。大小写不敏感。
|
||||||
运行环境配置。大小写不敏感。
|
|
||||||
|
|
||||||
将会从 ``nonebot.init 参数`` > ``环境变量`` > ``.env 环境配置文件`` 的优先级读取配置。
|
将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
environment: str = "prod"
|
environment: str = "prod"
|
||||||
"""
|
"""当前环境名。
|
||||||
- **类型**: ``str``
|
|
||||||
- **默认值**: ``"prod"``
|
|
||||||
|
|
||||||
:说明:
|
NoneBot 将从 `.env.{environment}` 文件中加载配置。
|
||||||
当前环境名。 NoneBot 将从 ``.env.{environment}`` 文件中加载配置。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "allow"
|
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseConfig):
|
class Config(BaseConfig):
|
||||||
"""
|
"""NoneBot 主要配置。大小写不敏感。
|
||||||
NoneBot 主要配置。大小写不敏感。
|
|
||||||
|
|
||||||
除了 NoneBot 的配置项外,还可以自行添加配置项到 ``.env.{environment}`` 文件中。
|
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
||||||
这些配置将会在 json 反序列化后一起带入 ``Config`` 类中。
|
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
|
||||||
|
|
||||||
|
配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_common_config: dict
|
_env_file: DotenvType = ".env", ".env.prod"
|
||||||
_env_file: str
|
|
||||||
|
|
||||||
# nonebot configs
|
# nonebot configs
|
||||||
driver: str = "~fastapi"
|
driver: str = "~fastapi"
|
||||||
"""
|
"""NoneBot 运行所使用的 `Driver` 。继承自 {ref}`nonebot.drivers.Driver` 。
|
||||||
- **类型**: ``str``
|
|
||||||
- **默认值**: ``"~fastapi"``
|
|
||||||
|
|
||||||
:说明:
|
配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。
|
||||||
|
|
||||||
NoneBot 运行所使用的 ``Driver`` 。继承自 ``nonebot.drivers.Driver`` 。
|
`~` 为 `nonebot.drivers.` 的缩写。
|
||||||
|
|
||||||
配置格式为 ``<module>[:<Driver>][+<module>[:<Mixin>]]*``。
|
配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)
|
||||||
|
|
||||||
``~`` 为 ``nonebot.drivers.`` 的缩写。
|
|
||||||
"""
|
"""
|
||||||
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
||||||
"""
|
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||||
- **类型**: ``IPvAnyAddress``
|
port: int = Field(default=8080, ge=1, le=65535)
|
||||||
- **默认值**: ``127.0.0.1``
|
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
NoneBot 的 HTTP 和 WebSocket 服务端监听的 IP/主机名。
|
|
||||||
"""
|
|
||||||
port: int = 8080
|
|
||||||
"""
|
|
||||||
- **类型**: ``int``
|
|
||||||
- **默认值**: ``8080``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
NoneBot 的 HTTP 和 WebSocket 服务端监听的端口。
|
|
||||||
"""
|
|
||||||
log_level: Union[int, str] = "INFO"
|
log_level: Union[int, str] = "INFO"
|
||||||
"""
|
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。
|
||||||
- **类型**: ``Union[int, str]``
|
|
||||||
- **默认值**: ``INFO``
|
|
||||||
|
|
||||||
:说明:
|
参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
|
||||||
|
|
||||||
配置 NoneBot 日志输出等级,可以为 ``int`` 类型等级或等级名称,参考 `loguru 日志等级`_。
|
:::tip 提示
|
||||||
|
日志等级名称应为大写,如 `INFO`。
|
||||||
:示例:
|
:::
|
||||||
|
|
||||||
.. code-block:: default
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```conf
|
||||||
LOG_LEVEL=25
|
LOG_LEVEL=25
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
```
|
||||||
.. _loguru 日志等级:
|
|
||||||
https://loguru.readthedocs.io/en/stable/api/logger.html#levels
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# bot connection configs
|
# bot connection configs
|
||||||
api_timeout: Optional[float] = 30.0
|
api_timeout: Optional[float] = 30.0
|
||||||
"""
|
"""API 请求超时时间,单位: 秒。"""
|
||||||
- **类型**: ``Optional[float]``
|
|
||||||
- **默认值**: ``30.``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
API 请求超时时间,单位: 秒。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# bot runtime configs
|
# bot runtime configs
|
||||||
superusers: Set[str] = set()
|
superusers: Set[str] = set()
|
||||||
"""
|
"""机器人超级用户。
|
||||||
- **类型**: ``Set[str]``
|
|
||||||
- **默认值**: ``set()``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
机器人超级用户。
|
|
||||||
|
|
||||||
:示例:
|
|
||||||
|
|
||||||
.. code-block:: default
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```conf
|
||||||
SUPERUSERS=["12345789"]
|
SUPERUSERS=["12345789"]
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
nickname: Set[str] = set()
|
nickname: Set[str] = set()
|
||||||
"""
|
"""机器人昵称。"""
|
||||||
- **类型**: ``Set[str]``
|
|
||||||
- **默认值**: ``set()``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
机器人昵称。
|
|
||||||
"""
|
|
||||||
command_start: Set[str] = {"/"}
|
command_start: Set[str] = {"/"}
|
||||||
"""
|
"""命令的起始标记,用于判断一条消息是不是命令。
|
||||||
- **类型**: ``Set[str]``
|
|
||||||
- **默认值**: ``{"/"}``
|
|
||||||
|
|
||||||
:说明:
|
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||||
|
|
||||||
命令的起始标记,用于判断一条消息是不是命令。
|
用法:
|
||||||
|
```conf
|
||||||
|
COMMAND_START=["/", ""]
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
command_sep: Set[str] = {"."}
|
command_sep: Set[str] = {"."}
|
||||||
"""
|
"""命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
|
||||||
- **类型**: ``Set[str]``
|
|
||||||
- **默认值**: ``{"."}``
|
|
||||||
|
|
||||||
:说明:
|
参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。
|
||||||
|
|
||||||
命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
|
用法:
|
||||||
|
```conf
|
||||||
|
COMMAND_SEP=["."]
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
session_expire_timeout: timedelta = timedelta(minutes=2)
|
session_expire_timeout: timedelta = timedelta(minutes=2)
|
||||||
"""
|
"""等待用户回复的超时时间。
|
||||||
- **类型**: ``timedelta``
|
|
||||||
- **默认值**: ``timedelta(minutes=2)``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
等待用户回复的超时时间。
|
|
||||||
|
|
||||||
:示例:
|
|
||||||
|
|
||||||
.. code-block:: default
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```conf
|
||||||
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
|
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
|
||||||
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
|
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
|
||||||
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
|
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# adapter configs
|
# adapter configs
|
||||||
@@ -292,5 +255,10 @@ class Config(BaseConfig):
|
|||||||
# or from env file using json loads
|
# or from env file using json loads
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "allow"
|
env_file = ".env", ".env.prod"
|
||||||
env_file = ".env.prod"
|
|
||||||
|
|
||||||
|
__autodoc__ = {
|
||||||
|
"CustomEnvSettings": False,
|
||||||
|
"BaseConfig": False,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,55 @@
|
|||||||
|
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 9
|
||||||
|
description: nonebot.consts 模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
# used by Matcher
|
# used by Matcher
|
||||||
RECEIVE_KEY = "_receive_{id}"
|
RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}"
|
||||||
LAST_RECEIVE_KEY = "_last_receive"
|
"""`receive` 存储 key"""
|
||||||
ARG_KEY = "{key}"
|
LAST_RECEIVE_KEY: Literal["_last_receive"] = "_last_receive"
|
||||||
REJECT_TARGET = "_current_target"
|
"""`last_receive` 存储 key"""
|
||||||
REJECT_CACHE_TARGET = "_next_target"
|
ARG_KEY: Literal["{key}"] = "{key}"
|
||||||
|
"""`arg` 存储 key"""
|
||||||
|
REJECT_TARGET: Literal["_current_target"] = "_current_target"
|
||||||
|
"""当前 `reject` 目标存储 key"""
|
||||||
|
REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target"
|
||||||
|
"""下一个 `reject` 目标存储 key"""
|
||||||
|
|
||||||
# used by Rule
|
# used by Rule
|
||||||
PREFIX_KEY = "_prefix"
|
PREFIX_KEY: Literal["_prefix"] = "_prefix"
|
||||||
|
"""命令前缀存储 key"""
|
||||||
|
|
||||||
CMD_KEY = "command"
|
CMD_KEY: Literal["command"] = "command"
|
||||||
RAW_CMD_KEY = "raw_command"
|
"""命令元组存储 key"""
|
||||||
CMD_ARG_KEY = "command_arg"
|
RAW_CMD_KEY: Literal["raw_command"] = "raw_command"
|
||||||
|
"""命令文本存储 key"""
|
||||||
|
CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
|
||||||
|
"""命令参数存储 key"""
|
||||||
|
CMD_START_KEY: Literal["command_start"] = "command_start"
|
||||||
|
"""命令开头存储 key"""
|
||||||
|
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
|
||||||
|
"""命令与参数间空白符存储 key"""
|
||||||
|
|
||||||
SHELL_ARGS = "_args"
|
SHELL_ARGS: Literal["_args"] = "_args"
|
||||||
SHELL_ARGV = "_argv"
|
"""shell 命令 parse 后参数字典存储 key"""
|
||||||
|
SHELL_ARGV: Literal["_argv"] = "_argv"
|
||||||
|
"""shell 命令原始参数列表存储 key"""
|
||||||
|
|
||||||
REGEX_MATCHED = "_matched"
|
REGEX_MATCHED: Literal["_matched"] = "_matched"
|
||||||
REGEX_GROUP = "_matched_groups"
|
"""正则匹配结果存储 key"""
|
||||||
REGEX_DICT = "_matched_dict"
|
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")
|
||||||
|
|||||||
@@ -1,145 +1,159 @@
|
|||||||
"""
|
"""本模块模块实现了依赖注入的定义与处理。
|
||||||
依赖注入处理模块
|
|
||||||
================
|
|
||||||
|
|
||||||
该模块实现了依赖注入的定义与处理。
|
FrontMatter:
|
||||||
|
sidebar_position: 0
|
||||||
|
description: nonebot.dependencies 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Dict, List, Type, Generic, TypeVar, Callable, Optional
|
from dataclasses import field, dataclass
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Type,
|
||||||
|
Tuple,
|
||||||
|
Generic,
|
||||||
|
TypeVar,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
Optional,
|
||||||
|
Awaitable,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
from pydantic import BaseConfig
|
from pydantic import BaseConfig
|
||||||
from pydantic.schema import get_annotation_from_field_info
|
from pydantic.schema import get_annotation_from_field_info
|
||||||
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from .utils import get_typed_signature
|
from nonebot.typing import _DependentCallable
|
||||||
from nonebot.exception import TypeMisMatch
|
from nonebot.exception import SkippedException
|
||||||
from nonebot.utils import run_sync, is_coroutine_callable
|
from nonebot.utils import run_sync, is_coroutine_callable
|
||||||
|
|
||||||
T = TypeVar("T", bound="Dependent")
|
from .utils import check_field_type, get_typed_signature
|
||||||
|
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
T = TypeVar("T", bound="Dependent")
|
||||||
|
|
||||||
|
|
||||||
class Param(abc.ABC, FieldInfo):
|
class Param(abc.ABC, FieldInfo):
|
||||||
|
"""依赖注入的基本单元 —— 参数。
|
||||||
|
|
||||||
|
继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, validate: bool = False, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validate = validate
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, dependent: "Dependent", name: str, param: inspect.Parameter
|
cls, param: inspect.Parameter, allow_types: Tuple[Type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> Optional["Param"]:
|
||||||
return None
|
return
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, dependent: "Dependent", value: Any
|
cls, value: Any, allow_types: Tuple[Type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> Optional["Param"]:
|
||||||
return None
|
return
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def _solve(self, **kwargs: Any) -> Any:
|
async def _solve(self, **kwargs: Any) -> Any:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def _check(self, **kwargs: Any) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(BaseConfig):
|
class CustomConfig(BaseConfig):
|
||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class Dependent(Generic[R]):
|
class Dependent(Generic[R]):
|
||||||
def __init__(
|
"""依赖注入容器
|
||||||
self,
|
|
||||||
*,
|
参数:
|
||||||
call: Callable[..., Any],
|
call: 依赖注入的可调用对象,可以是任何 Callable 对象
|
||||||
pre_checkers: Optional[List[Param]] = None,
|
pre_checkers: 依赖注入解析前的参数检查
|
||||||
params: Optional[List[ModelField]] = None,
|
params: 具名参数列表
|
||||||
parameterless: Optional[List[Param]] = None,
|
parameterless: 匿名参数列表
|
||||||
allow_types: Optional[List[Type[Param]]] = None,
|
allow_types: 允许的参数类型
|
||||||
) -> None:
|
"""
|
||||||
self.call = call
|
|
||||||
self.pre_checkers = pre_checkers or []
|
call: _DependentCallable[R]
|
||||||
self.params = params or []
|
params: Tuple[ModelField, ...] = field(default_factory=tuple)
|
||||||
self.parameterless = parameterless or []
|
parameterless: Tuple[Param, ...] = field(default_factory=tuple)
|
||||||
self.allow_types = allow_types or []
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
if inspect.isfunction(self.call) or inspect.isclass(self.call):
|
||||||
|
call_str = self.call.__name__
|
||||||
|
else:
|
||||||
|
call_str = repr(self.call)
|
||||||
return (
|
return (
|
||||||
f"<Dependent call={self.call}, params={self.params},"
|
f"Dependent(call={call_str}"
|
||||||
f" parameterless={self.parameterless}>"
|
+ (f", parameterless={self.parameterless}" if self.parameterless else "")
|
||||||
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
async def __call__(self, **kwargs: Any) -> R:
|
async def __call__(self, **kwargs: Any) -> R:
|
||||||
values = await self.solve(**kwargs)
|
try:
|
||||||
|
# do pre-check
|
||||||
|
await self.check(**kwargs)
|
||||||
|
|
||||||
if is_coroutine_callable(self.call):
|
# solve param values
|
||||||
return await self.call(**values)
|
values = await self.solve(**kwargs)
|
||||||
else:
|
|
||||||
return await run_sync(self.call)(**values)
|
|
||||||
|
|
||||||
def parse_param(self, name: str, param: inspect.Parameter) -> Param:
|
# call function
|
||||||
for allow_type in self.allow_types:
|
if is_coroutine_callable(self.call):
|
||||||
field_info = allow_type._check_param(self, name, param)
|
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||||
if field_info:
|
else:
|
||||||
return field_info
|
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||||
else:
|
except SkippedException as e:
|
||||||
raise ValueError(
|
logger.trace(f"{self} skipped due to {e}")
|
||||||
f"Unknown parameter {name} for function {self.call} with type {param.annotation}"
|
raise
|
||||||
)
|
|
||||||
|
|
||||||
def parse_parameterless(self, value: Any) -> Param:
|
@staticmethod
|
||||||
for allow_type in self.allow_types:
|
def parse_params(
|
||||||
field_info = allow_type._check_parameterless(self, value)
|
call: _DependentCallable[R], allow_types: Tuple[Type[Param], ...]
|
||||||
if field_info:
|
) -> Tuple[ModelField, ...]:
|
||||||
return field_info
|
fields: List[ModelField] = []
|
||||||
else:
|
params = get_typed_signature(call).parameters.values()
|
||||||
raise ValueError(
|
|
||||||
f"Unknown parameterless {value} for function {self.call} with type {type(value)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def prepend_parameterless(self, value: Any) -> None:
|
for param in params:
|
||||||
self.parameterless.insert(0, self.parse_parameterless(value))
|
|
||||||
|
|
||||||
def append_parameterless(self, value: Any) -> None:
|
|
||||||
self.parameterless.append(self.parse_parameterless(value))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(
|
|
||||||
cls: Type[T],
|
|
||||||
*,
|
|
||||||
call: Callable[..., Any],
|
|
||||||
parameterless: Optional[List[Any]] = None,
|
|
||||||
allow_types: Optional[List[Type[Param]]] = None,
|
|
||||||
) -> T:
|
|
||||||
signature = get_typed_signature(call)
|
|
||||||
params = signature.parameters
|
|
||||||
dependent = cls(
|
|
||||||
call=call,
|
|
||||||
allow_types=allow_types,
|
|
||||||
)
|
|
||||||
|
|
||||||
for param_name, param in params.items():
|
|
||||||
default_value = Required
|
default_value = Required
|
||||||
if param.default != param.empty:
|
if param.default != param.empty:
|
||||||
default_value = param.default
|
default_value = param.default
|
||||||
|
|
||||||
if isinstance(default_value, Param):
|
if isinstance(default_value, Param):
|
||||||
field_info = default_value
|
field_info = default_value
|
||||||
default_value = field_info.default
|
|
||||||
else:
|
else:
|
||||||
field_info = dependent.parse_param(param_name, param)
|
for allow_type in allow_types:
|
||||||
default_value = field_info.default
|
if field_info := allow_type._check_param(param, allow_types):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown parameter {param.name} "
|
||||||
|
f"for function {call} with type {param.annotation}"
|
||||||
|
)
|
||||||
|
|
||||||
|
default_value = field_info.default
|
||||||
|
|
||||||
annotation: Any = Any
|
annotation: Any = Any
|
||||||
required = default_value == Required
|
required = default_value == Required
|
||||||
if param.annotation != param.empty:
|
if param.annotation != param.empty:
|
||||||
annotation = param.annotation
|
annotation = param.annotation
|
||||||
annotation = get_annotation_from_field_info(
|
annotation = get_annotation_from_field_info(
|
||||||
annotation, field_info, param_name
|
annotation, field_info, param.name
|
||||||
)
|
)
|
||||||
dependent.params.append(
|
|
||||||
|
fields.append(
|
||||||
ModelField(
|
ModelField(
|
||||||
name=param_name,
|
name=param.name,
|
||||||
type_=annotation,
|
type_=annotation,
|
||||||
class_validators=None,
|
class_validators=None,
|
||||||
model_config=CustomConfig,
|
model_config=CustomConfig,
|
||||||
@@ -149,46 +163,65 @@ class Dependent(Generic[R]):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
parameterless_params = [
|
return tuple(fields)
|
||||||
dependent.parse_parameterless(param) for param in (parameterless or [])
|
|
||||||
]
|
|
||||||
dependent.parameterless.extend(parameterless_params)
|
|
||||||
|
|
||||||
logger.trace(
|
@staticmethod
|
||||||
f"Parsed dependent with call={call}, "
|
def parse_parameterless(
|
||||||
f"params={[param.field_info for param in dependent.params]}, "
|
parameterless: Tuple[Any, ...], allow_types: Tuple[Type[Param], ...]
|
||||||
f"parameterless={dependent.parameterless}"
|
) -> Tuple[Param, ...]:
|
||||||
|
parameterless_params: List[Param] = []
|
||||||
|
for value in parameterless:
|
||||||
|
for allow_type in allow_types:
|
||||||
|
if param := allow_type._check_parameterless(value, allow_types):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown parameterless {value}")
|
||||||
|
parameterless_params.append(param)
|
||||||
|
return tuple(parameterless_params)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
call: _DependentCallable[R],
|
||||||
|
parameterless: Optional[Iterable[Any]] = None,
|
||||||
|
allow_types: Iterable[Type[Param]],
|
||||||
|
) -> "Dependent[R]":
|
||||||
|
allow_types = tuple(allow_types)
|
||||||
|
|
||||||
|
params = cls.parse_params(call, allow_types)
|
||||||
|
parameterless_params = (
|
||||||
|
()
|
||||||
|
if parameterless is None
|
||||||
|
else cls.parse_parameterless(tuple(parameterless), allow_types)
|
||||||
)
|
)
|
||||||
|
|
||||||
return dependent
|
return cls(call, params, parameterless_params)
|
||||||
|
|
||||||
async def solve(
|
async def check(self, **params: Any) -> None:
|
||||||
self,
|
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
||||||
**params: Any,
|
await asyncio.gather(
|
||||||
) -> Dict[str, Any]:
|
*(cast(Param, param.field_info)._check(**params) for param in self.params)
|
||||||
values: Dict[str, Any] = {}
|
)
|
||||||
|
|
||||||
for checker in self.pre_checkers:
|
async def _solve_field(self, field: ModelField, params: Dict[str, Any]) -> Any:
|
||||||
await checker._solve(**params)
|
param = cast(Param, field.field_info)
|
||||||
|
value = await param._solve(**params)
|
||||||
|
if value is Undefined:
|
||||||
|
value = field.get_default()
|
||||||
|
v = check_field_type(field, value)
|
||||||
|
return v if param.validate else value
|
||||||
|
|
||||||
|
async def solve(self, **params: Any) -> Dict[str, Any]:
|
||||||
|
# solve parameterless
|
||||||
for param in self.parameterless:
|
for param in self.parameterless:
|
||||||
await param._solve(**params)
|
await param._solve(**params)
|
||||||
|
|
||||||
for field in self.params:
|
# solve param values
|
||||||
field_info = field.field_info
|
values = await asyncio.gather(
|
||||||
assert isinstance(field_info, Param), "Params must be subclasses of Param"
|
*(self._solve_field(field, params) for field in self.params)
|
||||||
value = await field_info._solve(**params)
|
)
|
||||||
if value == Undefined:
|
return {field.name: value for field, value in zip(self.params, values)}
|
||||||
value = field.get_default()
|
|
||||||
_, errs_ = field.validate(value, values, loc=(str(field_info), field.alias))
|
|
||||||
if errs_:
|
|
||||||
logger.debug(
|
|
||||||
f"{field_info} "
|
|
||||||
f"type {type(value)} not match depends {self.call} "
|
|
||||||
f"annotation {field._type_display()}, ignored"
|
|
||||||
)
|
|
||||||
raise TypeMisMatch(field, value)
|
|
||||||
else:
|
|
||||||
values[field.name] = value
|
|
||||||
|
|
||||||
return values
|
|
||||||
|
__autodoc__ = {"CustomConfig": False}
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
|
"""
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 1
|
||||||
|
description: nonebot.dependencies.utils 模块
|
||||||
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Dict, Callable
|
from typing import Any, Dict, Callable, ForwardRef
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
from pydantic.fields import ModelField
|
||||||
|
from pydantic.typing import evaluate_forwardref
|
||||||
|
|
||||||
|
from nonebot.exception import TypeMisMatch
|
||||||
|
|
||||||
|
|
||||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||||
|
"""获取可调用对象签名"""
|
||||||
|
|
||||||
signature = inspect.signature(call)
|
signature = inspect.signature(call)
|
||||||
globalns = getattr(call, "__globals__", {})
|
globalns = getattr(call, "__globals__", {})
|
||||||
typed_params = [
|
typed_params = [
|
||||||
@@ -17,11 +28,12 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|||||||
)
|
)
|
||||||
for param in signature.parameters.values()
|
for param in signature.parameters.values()
|
||||||
]
|
]
|
||||||
typed_signature = inspect.Signature(typed_params)
|
return inspect.Signature(typed_params)
|
||||||
return typed_signature
|
|
||||||
|
|
||||||
|
|
||||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||||
|
"""获取参数的类型注解"""
|
||||||
|
|
||||||
annotation = param.annotation
|
annotation = param.annotation
|
||||||
if isinstance(annotation, str):
|
if isinstance(annotation, str):
|
||||||
annotation = ForwardRef(annotation)
|
annotation = ForwardRef(annotation)
|
||||||
@@ -33,3 +45,12 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
|
|||||||
)
|
)
|
||||||
return inspect.Parameter.empty
|
return inspect.Parameter.empty
|
||||||
return annotation
|
return annotation
|
||||||
|
|
||||||
|
|
||||||
|
def check_field_type(field: ModelField, value: Any) -> Any:
|
||||||
|
"""检查字段类型是否匹配"""
|
||||||
|
|
||||||
|
v, errs_ = field.validate(value, {}, loc=())
|
||||||
|
if errs_:
|
||||||
|
raise TypeMisMatch(field, value)
|
||||||
|
return v
|
||||||
|
|||||||
@@ -1,297 +1,48 @@
|
|||||||
"""
|
"""本模块定义了驱动适配器基类。
|
||||||
后端驱动适配基类
|
|
||||||
=================
|
|
||||||
|
|
||||||
各驱动请继承以下基类
|
各驱动请继承以下基类。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 0
|
||||||
|
description: nonebot.drivers 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
from nonebot.internal.driver import URL as URL
|
||||||
import asyncio
|
from nonebot.internal.driver import Mixin as Mixin
|
||||||
from dataclasses import dataclass
|
from nonebot.internal.driver import Driver as Driver
|
||||||
from contextlib import asynccontextmanager
|
from nonebot.internal.driver import Cookies as Cookies
|
||||||
from typing import (
|
from nonebot.internal.driver import Request as Request
|
||||||
TYPE_CHECKING,
|
from nonebot.internal.driver import Response as Response
|
||||||
Any,
|
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||||
Set,
|
from nonebot.internal.driver import WebSocket as WebSocket
|
||||||
Dict,
|
from nonebot.internal.driver import HTTPVersion as HTTPVersion
|
||||||
Type,
|
from nonebot.internal.driver import ForwardMixin as ForwardMixin
|
||||||
Callable,
|
from nonebot.internal.driver import ReverseMixin as ReverseMixin
|
||||||
Awaitable,
|
from nonebot.internal.driver import ForwardDriver as ForwardDriver
|
||||||
AsyncGenerator,
|
from nonebot.internal.driver import ReverseDriver as ReverseDriver
|
||||||
)
|
from nonebot.internal.driver import combine_driver as combine_driver
|
||||||
|
from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin
|
||||||
|
from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin
|
||||||
|
from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
|
||||||
from ._model import URL as URL
|
__autodoc__ = {
|
||||||
from nonebot.log import logger
|
"URL": True,
|
||||||
from nonebot.utils import escape_tag
|
"Cookies": True,
|
||||||
from ._model import Request as Request
|
"Request": True,
|
||||||
from nonebot.config import Env, Config
|
"Response": True,
|
||||||
from ._model import Response as Response
|
"WebSocket": True,
|
||||||
from ._model import WebSocket as WebSocket
|
"HTTPVersion": True,
|
||||||
from ._model import HTTPVersion as HTTPVersion
|
"Driver": True,
|
||||||
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
|
"Mixin": True,
|
||||||
|
"ForwardMixin": True,
|
||||||
if TYPE_CHECKING:
|
"ForwardDriver": True,
|
||||||
from nonebot.adapters import Bot, Adapter
|
"HTTPClientMixin": True,
|
||||||
|
"WebSocketClientMixin": True,
|
||||||
|
"ReverseMixin": True,
|
||||||
class Driver(abc.ABC):
|
"ReverseDriver": True,
|
||||||
"""
|
"ASGIMixin": True,
|
||||||
Driver 基类。
|
"combine_driver": True,
|
||||||
"""
|
"HTTPServerSetup": True,
|
||||||
|
"WebSocketServerSetup": True,
|
||||||
_adapters: Dict[str, "Adapter"] = {}
|
}
|
||||||
"""
|
|
||||||
:类型: ``Dict[str, Adapter]``
|
|
||||||
:说明: 已注册的适配器列表
|
|
||||||
"""
|
|
||||||
_bot_connection_hook: Set[T_BotConnectionHook] = set()
|
|
||||||
"""
|
|
||||||
:类型: ``Set[T_BotConnectionHook]``
|
|
||||||
:说明: Bot 连接建立时执行的函数
|
|
||||||
"""
|
|
||||||
_bot_disconnection_hook: Set[T_BotDisconnectionHook] = set()
|
|
||||||
"""
|
|
||||||
:类型: ``Set[T_BotDisconnectionHook]``
|
|
||||||
:说明: Bot 连接断开时执行的函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, env: Env, config: Config):
|
|
||||||
"""
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``env: Env``: 包含环境信息的 Env 对象
|
|
||||||
* ``config: Config``: 包含配置信息的 Config 对象
|
|
||||||
"""
|
|
||||||
self.env: str = env.environment
|
|
||||||
"""
|
|
||||||
:类型: ``str``
|
|
||||||
:说明: 环境名称
|
|
||||||
"""
|
|
||||||
self.config: Config = config
|
|
||||||
"""
|
|
||||||
:类型: ``Config``
|
|
||||||
:说明: 配置对象
|
|
||||||
"""
|
|
||||||
self._clients: Dict[str, "Bot"] = {}
|
|
||||||
"""
|
|
||||||
:类型: ``Dict[str, Bot]``
|
|
||||||
:说明: 已连接的 Bot
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bots(self) -> Dict[str, "Bot"]:
|
|
||||||
"""
|
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Dict[str, Bot]``
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
获取当前所有已连接的 Bot
|
|
||||||
"""
|
|
||||||
return self._clients
|
|
||||||
|
|
||||||
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
注册一个协议适配器
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``name: str``: 适配器名称,用于在连接时进行识别
|
|
||||||
* ``adapter: Type[Bot]``: 适配器 Class
|
|
||||||
* ``**kwargs``: 其他传递给适配器的参数
|
|
||||||
"""
|
|
||||||
name = adapter.get_name()
|
|
||||||
if name in self._adapters:
|
|
||||||
logger.opt(colors=True).debug(
|
|
||||||
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self._adapters[name] = adapter(self, **kwargs)
|
|
||||||
logger.opt(colors=True).debug(
|
|
||||||
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def type(self) -> str:
|
|
||||||
"""驱动类型名称"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def logger(self):
|
|
||||||
"""驱动专属 logger 日志记录器"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
启动驱动框架
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
* ``*args``
|
|
||||||
* ``**kwargs``
|
|
||||||
"""
|
|
||||||
logger.opt(colors=True).debug(
|
|
||||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
|
||||||
)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def on_startup(self, func: Callable) -> Callable:
|
|
||||||
"""注册一个在驱动启动时运行的函数"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def on_shutdown(self, func: Callable) -> Callable:
|
|
||||||
"""注册一个在驱动停止时运行的函数"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def on_bot_connect(self, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数使他在 bot 通过 WebSocket 连接成功时执行。
|
|
||||||
|
|
||||||
:函数参数:
|
|
||||||
|
|
||||||
* ``bot: Bot``: 当前连接上的 Bot 对象
|
|
||||||
"""
|
|
||||||
self._bot_connection_hook.add(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
def on_bot_disconnect(self, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数使他在 bot 通过 WebSocket 连接断开时执行。
|
|
||||||
|
|
||||||
:函数参数:
|
|
||||||
|
|
||||||
* ``bot: Bot``: 当前连接上的 Bot 对象
|
|
||||||
"""
|
|
||||||
self._bot_disconnection_hook.add(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
def _bot_connect(self, bot: "Bot") -> None:
|
|
||||||
"""在 WebSocket 连接成功后,调用该函数来注册 bot 对象"""
|
|
||||||
if bot.self_id in self._clients:
|
|
||||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
|
||||||
self._clients[bot.self_id] = bot
|
|
||||||
|
|
||||||
async def _run_hook(bot: "Bot") -> None:
|
|
||||||
coros = list(map(lambda x: x(bot), 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>"
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.create_task(_run_hook(bot))
|
|
||||||
|
|
||||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
|
||||||
"""在 WebSocket 连接断开后,调用该函数来注销 bot 对象"""
|
|
||||||
if bot.self_id in self._clients:
|
|
||||||
del self._clients[bot.self_id]
|
|
||||||
|
|
||||||
async def _run_hook(bot: "Bot") -> None:
|
|
||||||
coros = list(map(lambda x: x(bot), 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>"
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.create_task(_run_hook(bot))
|
|
||||||
|
|
||||||
|
|
||||||
class ForwardMixin(abc.ABC):
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def type(self) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def request(self, setup: Request) -> Response:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
|
||||||
raise NotImplementedError
|
|
||||||
yield # used for static type checking's generator detection
|
|
||||||
|
|
||||||
|
|
||||||
class ForwardDriver(Driver, ForwardMixin):
|
|
||||||
"""
|
|
||||||
Forward Driver 基类。将客户端框架封装,以满足适配器使用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ReverseDriver(Driver):
|
|
||||||
"""
|
|
||||||
Reverse Driver 基类。将后端框架封装,以满足适配器使用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def server_app(self) -> Any:
|
|
||||||
"""驱动 APP 对象"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def asgi(self) -> Any:
|
|
||||||
"""驱动 ASGI 对象"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
|
|
||||||
# check first
|
|
||||||
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
|
|
||||||
assert all(
|
|
||||||
map(lambda m: issubclass(m, ForwardMixin), mixins)
|
|
||||||
), "`mixins` must be subclass of ForwardMixin"
|
|
||||||
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
|
|
||||||
return CombinedDriver
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HTTPServerSetup:
|
|
||||||
path: URL # path should not be absolute, check it by URL.is_absolute() == False
|
|
||||||
method: str
|
|
||||||
name: str
|
|
||||||
handle_func: Callable[[Request], Awaitable[Response]]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WebSocketServerSetup:
|
|
||||||
path: URL # path should not be absolute, check it by URL.is_absolute() == False
|
|
||||||
name: str
|
|
||||||
handle_func: Callable[[WebSocket], Awaitable[Any]]
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import signal
|
|
||||||
import asyncio
|
|
||||||
import threading
|
|
||||||
from typing import Set, Callable, Awaitable
|
|
||||||
|
|
||||||
from nonebot.log import logger
|
|
||||||
from nonebot.drivers import Driver
|
|
||||||
from nonebot.typing import overrides
|
|
||||||
from nonebot.config import Env, Config
|
|
||||||
|
|
||||||
STARTUP_FUNC = Callable[[], Awaitable[None]]
|
|
||||||
SHUTDOWN_FUNC = Callable[[], 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:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
注册一个启动时执行的函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``func: Callable[[], Awaitable[None]]``
|
|
||||||
"""
|
|
||||||
self.startup_funcs.add(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
@overrides(Driver)
|
|
||||||
def on_shutdown(self, func: SHUTDOWN_FUNC) -> SHUTDOWN_FUNC:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
注册一个停止时执行的函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``func: Callable[[], Awaitable[None]]``
|
|
||||||
"""
|
|
||||||
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() 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() 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()
|
|
||||||
46
nonebot/drivers/_lifespan.py
Normal file
46
nonebot/drivers/_lifespan.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from typing_extensions import TypeAlias
|
||||||
|
from typing import Any, List, Union, Callable, Awaitable, cast
|
||||||
|
|
||||||
|
from nonebot.utils import run_sync, is_coroutine_callable
|
||||||
|
|
||||||
|
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||||
|
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||||
|
LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||||
|
|
||||||
|
|
||||||
|
class Lifespan:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
||||||
|
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
self._startup_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
self._shutdown_funcs.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _run_lifespan_func(
|
||||||
|
funcs: List[LIFESPAN_FUNC],
|
||||||
|
) -> None:
|
||||||
|
for func in funcs:
|
||||||
|
if is_coroutine_callable(func):
|
||||||
|
await cast(ASYNC_LIFESPAN_FUNC, func)()
|
||||||
|
else:
|
||||||
|
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||||
|
|
||||||
|
async def startup(self) -> None:
|
||||||
|
if self._startup_funcs:
|
||||||
|
await self._run_lifespan_func(self._startup_funcs)
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
if self._shutdown_funcs:
|
||||||
|
await self._run_lifespan_func(self._shutdown_funcs)
|
||||||
|
|
||||||
|
async def __aenter__(self) -> None:
|
||||||
|
await self.startup()
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||||
|
await self.shutdown()
|
||||||
@@ -1,35 +1,53 @@
|
|||||||
"""
|
"""[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。
|
||||||
AIOHTTP 驱动适配
|
|
||||||
================
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nb driver install aiohttp
|
||||||
|
# 或者
|
||||||
|
pip install nonebot2[aiohttp]
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
本驱动仅支持客户端连接
|
本驱动仅支持客户端连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 2
|
||||||
|
description: nonebot.drivers.aiohttp 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from typing_extensions import override
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import TYPE_CHECKING, AsyncGenerator
|
||||||
|
|
||||||
from nonebot.typing import overrides
|
|
||||||
from nonebot.drivers import Request, Response
|
from nonebot.drivers import Request, Response
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.drivers._block_driver import BlockDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import HTTPVersion, ForwardMixin, combine_driver
|
from nonebot.drivers import (
|
||||||
|
HTTPVersion,
|
||||||
|
HTTPClientMixin,
|
||||||
|
WebSocketClientMixin,
|
||||||
|
combine_driver,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
except ImportError:
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
"Please install aiohttp first to use this driver. "
|
||||||
) from None
|
"Install with pip: `pip install nonebot2[aiohttp]`"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||||
|
"""AIOHTTP Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
return "aiohttp"
|
return "aiohttp"
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
if setup.version == HTTPVersion.H10:
|
if setup.version == HTTPVersion.H10:
|
||||||
version = aiohttp.HttpVersion10
|
version = aiohttp.HttpVersion10
|
||||||
@@ -39,30 +57,36 @@ class Mixin(ForwardMixin):
|
|||||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(setup.timeout)
|
timeout = aiohttp.ClientTimeout(setup.timeout)
|
||||||
files = None
|
|
||||||
|
data = setup.data
|
||||||
if setup.files:
|
if setup.files:
|
||||||
files = aiohttp.FormData()
|
data = aiohttp.FormData(data or {})
|
||||||
for name, file in setup.files:
|
for name, file in setup.files:
|
||||||
files.add_field(name, file[1], content_type=file[2], filename=file[0])
|
data.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
|
||||||
|
cookies = {
|
||||||
|
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession(
|
||||||
|
cookies=cookies, version=version, trust_env=True
|
||||||
|
) as session:
|
||||||
async with session.request(
|
async with session.request(
|
||||||
setup.method,
|
setup.method,
|
||||||
setup.url,
|
setup.url,
|
||||||
data=setup.content or setup.data or files,
|
data=setup.content or data,
|
||||||
json=setup.json,
|
json=setup.json,
|
||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
proxy=setup.proxy,
|
proxy=setup.proxy,
|
||||||
) as response:
|
) as response:
|
||||||
res = Response(
|
return Response(
|
||||||
response.status,
|
response.status,
|
||||||
headers=response.headers.copy(),
|
headers=response.headers.copy(),
|
||||||
content=await response.read(),
|
content=await response.read(),
|
||||||
request=setup,
|
request=setup,
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||||
if setup.version == HTTPVersion.H10:
|
if setup.version == HTTPVersion.H10:
|
||||||
@@ -80,11 +104,12 @@ class Mixin(ForwardMixin):
|
|||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
proxy=setup.proxy,
|
proxy=setup.proxy,
|
||||||
) as ws:
|
) as ws:
|
||||||
websocket = WebSocket(request=setup, session=session, websocket=ws)
|
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||||
yield websocket
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(BaseWebSocket):
|
class WebSocket(BaseWebSocket):
|
||||||
|
"""AIOHTTP Websocket Wrapper"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -97,15 +122,15 @@ class WebSocket(BaseWebSocket):
|
|||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
def closed(self):
|
def closed(self):
|
||||||
return self.websocket.closed
|
return self.websocket.closed
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def close(self, code: int = 1000):
|
async def close(self, code: int = 1000):
|
||||||
await self.websocket.close(code=code)
|
await self.websocket.close(code=code)
|
||||||
await self.session.close()
|
await self.session.close()
|
||||||
@@ -116,27 +141,47 @@ class WebSocket(BaseWebSocket):
|
|||||||
raise WebSocketClosed(self.websocket.close_code or 1006)
|
raise WebSocketClosed(self.websocket.close_code or 1006)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def receive(self) -> str:
|
async def receive(self) -> str:
|
||||||
msg = await self._receive()
|
msg = await self._receive()
|
||||||
if msg.type != aiohttp.WSMsgType.TEXT:
|
if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||||
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
|
raise TypeError(
|
||||||
|
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||||
|
)
|
||||||
return msg.data
|
return msg.data
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def receive_bytes(self) -> bytes:
|
async def receive_text(self) -> str:
|
||||||
msg = await self._receive()
|
msg = await self._receive()
|
||||||
if msg.type != aiohttp.WSMsgType.TEXT:
|
if msg.type != aiohttp.WSMsgType.TEXT:
|
||||||
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
|
raise TypeError(
|
||||||
|
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||||
|
)
|
||||||
return msg.data
|
return msg.data
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send(self, data: str) -> None:
|
async def receive_bytes(self) -> bytes:
|
||||||
|
msg = await self._receive()
|
||||||
|
if msg.type != aiohttp.WSMsgType.BINARY:
|
||||||
|
raise TypeError(
|
||||||
|
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||||
|
)
|
||||||
|
return msg.data
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def send_text(self, data: str) -> None:
|
||||||
await self.websocket.send_str(data)
|
await self.websocket.send_str(data)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send_bytes(self, data: bytes) -> None:
|
async def send_bytes(self, data: bytes) -> None:
|
||||||
await self.websocket.send_bytes(data)
|
await self.websocket.send_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
Driver = combine_driver(BlockDriver, Mixin)
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class Driver(Mixin, NoneDriver):
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""AIOHTTP Driver"""
|
||||||
|
|||||||
@@ -1,33 +1,51 @@
|
|||||||
|
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nb driver install fastapi
|
||||||
|
# 或者
|
||||||
|
pip install nonebot2[fastapi]
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
本驱动仅支持服务端连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 1
|
||||||
|
description: nonebot.drivers.fastapi 模块
|
||||||
"""
|
"""
|
||||||
FastAPI 驱动适配
|
|
||||||
================
|
|
||||||
|
|
||||||
本驱动同时支持服务端以及客户端连接
|
|
||||||
|
|
||||||
后端使用方法请参考: `FastAPI 文档`_
|
|
||||||
|
|
||||||
.. _FastAPI 文档:
|
|
||||||
https://fastapi.tiangolo.com/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import contextlib
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, List, Tuple, Callable, Optional
|
from typing_extensions import override
|
||||||
|
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseSettings
|
||||||
from fastapi.responses import Response
|
|
||||||
from fastapi import FastAPI, Request, UploadFile, status
|
|
||||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
|
||||||
|
|
||||||
from ._model import FileTypes
|
|
||||||
from nonebot.config import Env
|
from nonebot.config import Env
|
||||||
from nonebot.typing import overrides
|
from nonebot.drivers import ASGIMixin
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.internal.driver import FileTypes
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
from nonebot.drivers import Request as BaseRequest
|
from nonebot.drivers import Request as BaseRequest
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
from nonebot.drivers import 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 first to use this driver. "
|
||||||
|
"Install with pip: `pip install nonebot2[fastapi]`"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
def catch_closed(func):
|
def catch_closed(func):
|
||||||
@@ -37,139 +55,83 @@ def catch_closed(func):
|
|||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except WebSocketDisconnect as e:
|
except WebSocketDisconnect as e:
|
||||||
raise WebSocketClosed(e.code)
|
raise WebSocketClosed(e.code)
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError("WebSocket received unexpected frame type")
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseSettings):
|
||||||
"""
|
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
|
||||||
FastAPI 驱动框架设置,详情参考 FastAPI 文档
|
|
||||||
"""
|
|
||||||
|
|
||||||
fastapi_openapi_url: Optional[str] = None
|
fastapi_openapi_url: Optional[str] = None
|
||||||
"""
|
"""`openapi.json` 地址,默认为 `None` 即关闭"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[str]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
``openapi.json`` 地址,默认为 ``None`` 即关闭
|
|
||||||
"""
|
|
||||||
fastapi_docs_url: Optional[str] = None
|
fastapi_docs_url: Optional[str] = None
|
||||||
"""
|
"""`swagger` 地址,默认为 `None` 即关闭"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[str]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
``swagger`` 地址,默认为 ``None`` 即关闭
|
|
||||||
"""
|
|
||||||
fastapi_redoc_url: Optional[str] = None
|
fastapi_redoc_url: Optional[str] = None
|
||||||
"""
|
"""`redoc` 地址,默认为 `None` 即关闭"""
|
||||||
:类型:
|
fastapi_include_adapter_schema: bool = True
|
||||||
|
"""是否包含适配器路由的 schema,默认为 `True`"""
|
||||||
``Optional[str]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
``redoc`` 地址,默认为 ``None`` 即关闭
|
|
||||||
"""
|
|
||||||
fastapi_reload: bool = False
|
fastapi_reload: bool = False
|
||||||
"""
|
"""开启/关闭冷重载"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``bool``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
开启/关闭冷重载
|
|
||||||
"""
|
|
||||||
fastapi_reload_dirs: Optional[List[str]] = None
|
fastapi_reload_dirs: Optional[List[str]] = None
|
||||||
"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
fastapi_reload_delay: float = 0.25
|
||||||
|
"""重载延迟,默认为 uvicorn 默认值"""
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
重载监控文件夹列表,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
fastapi_reload_delay: Optional[float] = None
|
|
||||||
"""
|
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[float]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
重载延迟,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
fastapi_reload_includes: Optional[List[str]] = None
|
fastapi_reload_includes: Optional[List[str]] = None
|
||||||
"""
|
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
fastapi_reload_excludes: Optional[List[str]] = None
|
fastapi_reload_excludes: Optional[List[str]] = None
|
||||||
"""
|
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
fastapi_extra: Dict[str, Any] = {}
|
||||||
|
"""传递给 `FastAPI` 的其他参数。"""
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
|
|
||||||
class Driver(ReverseDriver):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""FastAPI 驱动框架。包含反向 Server 功能。"""
|
"""FastAPI 驱动框架。"""
|
||||||
|
|
||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
super(Driver, self).__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self.fastapi_config: Config = Config(**config.dict())
|
self.fastapi_config: Config = Config(**config.dict())
|
||||||
|
|
||||||
|
self._lifespan = Lifespan()
|
||||||
|
|
||||||
self._server_app = FastAPI(
|
self._server_app = FastAPI(
|
||||||
|
lifespan=self._lifespan_manager,
|
||||||
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
||||||
docs_url=self.fastapi_config.fastapi_docs_url,
|
docs_url=self.fastapi_config.fastapi_docs_url,
|
||||||
redoc_url=self.fastapi_config.fastapi_redoc_url,
|
redoc_url=self.fastapi_config.fastapi_redoc_url,
|
||||||
|
**self.fastapi_config.fastapi_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""驱动名称: ``fastapi``"""
|
"""驱动名称: `fastapi`"""
|
||||||
return "fastapi"
|
return "fastapi"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def server_app(self) -> FastAPI:
|
def server_app(self) -> FastAPI:
|
||||||
"""``FastAPI APP`` 对象"""
|
"""`FastAPI APP` 对象"""
|
||||||
return self._server_app
|
return self._server_app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def asgi(self) -> FastAPI:
|
def asgi(self) -> FastAPI:
|
||||||
"""``FastAPI APP`` 对象"""
|
"""`FastAPI APP` 对象"""
|
||||||
return self._server_app
|
return self._server_app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def logger(self) -> logging.Logger:
|
def logger(self) -> logging.Logger:
|
||||||
"""fastapi 使用的 logger"""
|
"""fastapi 使用的 logger"""
|
||||||
return logging.getLogger("fastapi")
|
return logging.getLogger("fastapi")
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def setup_http_server(self, setup: HTTPServerSetup):
|
def setup_http_server(self, setup: HTTPServerSetup):
|
||||||
async def _handle(request: Request) -> Response:
|
async def _handle(request: Request) -> Response:
|
||||||
return await self._handle_http(request, setup)
|
return await self._handle_http(request, setup)
|
||||||
@@ -179,9 +141,10 @@ class Driver(ReverseDriver):
|
|||||||
_handle,
|
_handle,
|
||||||
name=setup.name,
|
name=setup.name,
|
||||||
methods=[setup.method],
|
methods=[setup.method],
|
||||||
|
include_in_schema=self.fastapi_config.fastapi_include_adapter_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
||||||
async def _handle(websocket: WebSocket) -> None:
|
async def _handle(websocket: WebSocket) -> None:
|
||||||
await self._handle_ws(websocket, setup)
|
await self._handle_ws(websocket, setup)
|
||||||
@@ -192,17 +155,23 @@ class Driver(ReverseDriver):
|
|||||||
name=setup.name,
|
name=setup.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def on_startup(self, func: Callable) -> Callable:
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
|
return self._lifespan.on_startup(func)
|
||||||
return self.server_app.on_event("startup")(func)
|
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def on_shutdown(self, func: Callable) -> Callable:
|
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
|
return self._lifespan.on_shutdown(func)
|
||||||
return self.server_app.on_event("shutdown")(func)
|
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@contextlib.asynccontextmanager
|
||||||
|
async def _lifespan_manager(self, app: FastAPI):
|
||||||
|
await self._lifespan.startup()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
await self._lifespan.shutdown()
|
||||||
|
|
||||||
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
@@ -211,8 +180,8 @@ class Driver(ReverseDriver):
|
|||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""使用 ``uvicorn`` 启动 FastAPI"""
|
"""使用 `uvicorn` 启动 FastAPI"""
|
||||||
super().run(host, port, app, **kwargs)
|
super().run(host, port, app=app, **kwargs)
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
@@ -248,14 +217,12 @@ class Driver(ReverseDriver):
|
|||||||
setup: HTTPServerSetup,
|
setup: HTTPServerSetup,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
json: Any = None
|
json: Any = None
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
json = await request.json()
|
json = await request.json()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
files: Optional[List[Tuple[str, FileTypes]]] = None
|
files: Optional[List[Tuple[str, FileTypes]]] = None
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
data = {}
|
data = {}
|
||||||
files = []
|
files = []
|
||||||
@@ -266,8 +233,7 @@ class Driver(ReverseDriver):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data[key] = value
|
data[key] = value
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
http_request = BaseRequest(
|
http_request = BaseRequest(
|
||||||
request.method,
|
request.method,
|
||||||
str(request.url),
|
str(request.url),
|
||||||
@@ -281,7 +247,9 @@ class Driver(ReverseDriver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = await setup.handle_func(http_request)
|
response = await setup.handle_func(http_request)
|
||||||
return Response(response.content, response.status_code, dict(response.headers))
|
return Response(
|
||||||
|
response.content, response.status_code, dict(response.headers.items())
|
||||||
|
)
|
||||||
|
|
||||||
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
|
async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):
|
||||||
request = BaseRequest(
|
request = BaseRequest(
|
||||||
@@ -300,43 +268,56 @@ class Driver(ReverseDriver):
|
|||||||
|
|
||||||
|
|
||||||
class FastAPIWebSocket(BaseWebSocket):
|
class FastAPIWebSocket(BaseWebSocket):
|
||||||
@overrides(BaseWebSocket)
|
"""FastAPI WebSocket Wrapper"""
|
||||||
|
|
||||||
|
@override
|
||||||
def __init__(self, *, request: BaseRequest, websocket: WebSocket):
|
def __init__(self, *, request: BaseRequest, websocket: WebSocket):
|
||||||
super().__init__(request=request)
|
super().__init__(request=request)
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
def closed(self) -> bool:
|
def closed(self) -> bool:
|
||||||
return (
|
return (
|
||||||
self.websocket.client_state == WebSocketState.DISCONNECTED
|
self.websocket.client_state == WebSocketState.DISCONNECTED
|
||||||
or self.websocket.application_state == WebSocketState.DISCONNECTED
|
or self.websocket.application_state == WebSocketState.DISCONNECTED
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def accept(self) -> None:
|
async def accept(self) -> None:
|
||||||
await self.websocket.accept()
|
await self.websocket.accept()
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def close(
|
async def close(
|
||||||
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ""
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.websocket.close(code)
|
await self.websocket.close(code, reason)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
|
async def receive(self) -> Union[str, bytes]:
|
||||||
|
# assert self.websocket.application_state == WebSocketState.CONNECTED
|
||||||
|
msg = await self.websocket.receive()
|
||||||
|
if msg["type"] == "websocket.disconnect":
|
||||||
|
raise WebSocketClosed(msg["code"])
|
||||||
|
return msg["text"] if "text" in msg else msg["bytes"]
|
||||||
|
|
||||||
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive(self) -> str:
|
async def receive_text(self) -> str:
|
||||||
return await self.websocket.receive_text()
|
return await self.websocket.receive_text()
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive_bytes(self) -> bytes:
|
async def receive_bytes(self) -> bytes:
|
||||||
return await self.websocket.receive_bytes()
|
return await self.websocket.receive_bytes()
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send(self, data: str) -> None:
|
async def send_text(self, data: str) -> None:
|
||||||
await self.websocket.send({"type": "websocket.send", "text": data})
|
await self.websocket.send({"type": "websocket.send", "text": data})
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send_bytes(self, data: bytes) -> None:
|
async def send_bytes(self, data: bytes) -> None:
|
||||||
await self.websocket.send({"type": "websocket.send", "bytes": data})
|
await self.websocket.send({"type": "websocket.send", "bytes": data})
|
||||||
|
|
||||||
|
|
||||||
|
__autodoc__ = {"catch_closed": False}
|
||||||
|
|||||||
@@ -1,34 +1,53 @@
|
|||||||
from typing import AsyncGenerator
|
"""[HTTPX](https://www.python-httpx.org/) 驱动适配
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from nonebot.typing import overrides
|
```bash
|
||||||
from nonebot.drivers._block_driver import BlockDriver
|
nb driver install httpx
|
||||||
|
# 或者
|
||||||
|
pip install nonebot2[httpx]
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
本驱动仅支持客户端 HTTP 连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 3
|
||||||
|
description: nonebot.drivers.httpx 模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import (
|
from nonebot.drivers import (
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
WebSocket,
|
|
||||||
HTTPVersion,
|
HTTPVersion,
|
||||||
ForwardMixin,
|
HTTPClientMixin,
|
||||||
combine_driver,
|
combine_driver,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
except ImportError:
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
"Please install httpx first to use this driver. "
|
||||||
) from None
|
"Install with pip: `pip install nonebot2[httpx]`"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(HTTPClientMixin):
|
||||||
|
"""HTTPX Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
return "httpx"
|
return "httpx"
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
|
cookies=setup.cookies.jar,
|
||||||
http2=setup.version == HTTPVersion.H2,
|
http2=setup.version == HTTPVersion.H2,
|
||||||
proxies=setup.proxy,
|
proxies=setup.proxy,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
@@ -45,16 +64,17 @@ class Mixin(ForwardMixin):
|
|||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
response.status_code,
|
response.status_code,
|
||||||
headers=response.headers,
|
headers=response.headers.multi_items(),
|
||||||
content=response.content,
|
content=response.content,
|
||||||
request=setup,
|
request=setup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
|
||||||
async with super(Mixin, self).websocket(setup) as ws:
|
|
||||||
yield ws
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
Driver = combine_driver(BlockDriver, Mixin)
|
class Driver(Mixin, NoneDriver):
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""HTTPX Driver"""
|
||||||
|
|||||||
155
nonebot/drivers/none.py
Normal file
155
nonebot/drivers/none.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""None 驱动适配
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
本驱动不支持任何服务器或客户端连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 6
|
||||||
|
description: nonebot.drivers.none 模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
import signal
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.consts import WINDOWS
|
||||||
|
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
|
||||||
|
@override
|
||||||
|
def type(self) -> str:
|
||||||
|
"""驱动名称: `none`"""
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def logger(self):
|
||||||
|
"""none driver 使用的 logger"""
|
||||||
|
return logger
|
||||||
|
|
||||||
|
@override
|
||||||
|
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""注册一个启动时执行的函数"""
|
||||||
|
return self._lifespan.on_startup(func)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
|
"""注册一个停止时执行的函数"""
|
||||||
|
return self._lifespan.on_shutdown(func)
|
||||||
|
|
||||||
|
@override
|
||||||
|
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
|
||||||
@@ -1,39 +1,61 @@
|
|||||||
"""
|
"""[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配
|
||||||
Quart 驱动适配
|
|
||||||
================
|
|
||||||
|
|
||||||
后端使用方法请参考: `Quart 文档`_
|
```bash
|
||||||
|
nb driver install quart
|
||||||
|
# 或者
|
||||||
|
pip install nonebot2[quart]
|
||||||
|
```
|
||||||
|
|
||||||
.. _Quart 文档:
|
:::tip 提示
|
||||||
https://pgjones.gitlab.io/quart/index.html
|
本驱动仅支持服务端连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 5
|
||||||
|
description: nonebot.drivers.quart 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import List, Tuple, TypeVar, Callable, Optional, Coroutine
|
from typing_extensions import override
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
TypeVar,
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
Coroutine,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
from ._model import FileTypes
|
|
||||||
from nonebot.config import Env
|
from nonebot.config import Env
|
||||||
from nonebot.typing import overrides
|
from nonebot.drivers import ASGIMixin
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
|
from nonebot.internal.driver import FileTypes
|
||||||
|
from nonebot.drivers import Driver as BaseDriver
|
||||||
from nonebot.config import Config as NoneBotConfig
|
from nonebot.config import Config as NoneBotConfig
|
||||||
from nonebot.drivers import Request as BaseRequest
|
from nonebot.drivers import Request as BaseRequest
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
from nonebot.drivers import HTTPServerSetup, WebSocketServerSetup
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import uvicorn
|
||||||
from quart import request as _request
|
from quart import request as _request
|
||||||
from quart import websocket as _websocket
|
from quart.ctx import WebsocketContext
|
||||||
|
from quart.globals import websocket_ctx
|
||||||
from quart import Quart, Request, Response
|
from quart import Quart, Request, Response
|
||||||
from quart.datastructures import FileStorage
|
from quart.datastructures import FileStorage
|
||||||
from quart import Websocket as QuartWebSocket
|
from quart import Websocket as QuartWebSocket
|
||||||
except ImportError:
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ValueError(
|
raise ImportError(
|
||||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
"Please install Quart first to use this driver. "
|
||||||
) from None
|
"Install with pip: `pip install nonebot2[quart]`"
|
||||||
|
) from e
|
||||||
|
|
||||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
||||||
|
|
||||||
@@ -50,102 +72,62 @@ def catch_closed(func):
|
|||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseSettings):
|
||||||
"""
|
"""Quart 驱动框架设置"""
|
||||||
Quart 驱动框架设置
|
|
||||||
"""
|
|
||||||
|
|
||||||
quart_reload: bool = False
|
quart_reload: bool = False
|
||||||
"""
|
"""开启/关闭冷重载"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``bool``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
开启/关闭冷重载
|
|
||||||
"""
|
|
||||||
quart_reload_dirs: Optional[List[str]] = None
|
quart_reload_dirs: Optional[List[str]] = None
|
||||||
"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
quart_reload_delay: float = 0.25
|
||||||
|
"""重载延迟,默认为 uvicorn 默认值"""
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
重载监控文件夹列表,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
quart_reload_delay: Optional[float] = None
|
|
||||||
"""
|
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[float]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
重载延迟,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
quart_reload_includes: Optional[List[str]] = None
|
quart_reload_includes: Optional[List[str]] = None
|
||||||
"""
|
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
|
||||||
|
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
quart_reload_excludes: Optional[List[str]] = None
|
quart_reload_excludes: Optional[List[str]] = None
|
||||||
"""
|
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
:类型:
|
quart_extra: Dict[str, Any] = {}
|
||||||
|
"""传递给 `Quart` 的其他参数。"""
|
||||||
``Optional[List[str]]``
|
|
||||||
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
|
|
||||||
class Driver(ReverseDriver):
|
class Driver(BaseDriver, ASGIMixin):
|
||||||
"""
|
"""Quart 驱动框架"""
|
||||||
Quart 驱动框架
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, env: Env, config: NoneBotConfig):
|
def __init__(self, env: Env, config: NoneBotConfig):
|
||||||
super().__init__(env, config)
|
super().__init__(env, config)
|
||||||
|
|
||||||
self.quart_config = Config(**config.dict())
|
self.quart_config = Config(**config.dict())
|
||||||
|
|
||||||
self._server_app = Quart(self.__class__.__qualname__)
|
self._server_app = Quart(
|
||||||
|
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""驱动名称: ``quart``"""
|
"""驱动名称: `quart`"""
|
||||||
return "quart"
|
return "quart"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def server_app(self) -> Quart:
|
def server_app(self) -> Quart:
|
||||||
"""``Quart`` 对象"""
|
"""`Quart` 对象"""
|
||||||
return self._server_app
|
return self._server_app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def asgi(self):
|
def asgi(self):
|
||||||
"""``Quart`` 对象"""
|
"""`Quart` 对象"""
|
||||||
return self._server_app
|
return self._server_app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def logger(self):
|
def logger(self):
|
||||||
"""Quart 使用的 logger"""
|
"""Quart 使用的 logger"""
|
||||||
return self._server_app.logger
|
return self._server_app.logger
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def setup_http_server(self, setup: HTTPServerSetup):
|
def setup_http_server(self, setup: HTTPServerSetup):
|
||||||
async def _handle() -> Response:
|
async def _handle() -> Response:
|
||||||
return await self._handle_http(setup)
|
return await self._handle_http(setup)
|
||||||
@@ -157,7 +139,7 @@ class Driver(ReverseDriver):
|
|||||||
view_func=_handle,
|
view_func=_handle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:
|
||||||
async def _handle() -> None:
|
async def _handle() -> None:
|
||||||
return await self._handle_ws(setup)
|
return await self._handle_ws(setup)
|
||||||
@@ -168,21 +150,17 @@ class Driver(ReverseDriver):
|
|||||||
view_func=_handle,
|
view_func=_handle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
|
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
|
||||||
"""参考文档: `Startup and Shutdown`_
|
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
||||||
|
|
||||||
.. _Startup and Shutdown:
|
|
||||||
https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html
|
|
||||||
"""
|
|
||||||
return self.server_app.before_serving(func) # type: ignore
|
return self.server_app.before_serving(func) # type: ignore
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
|
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
|
||||||
"""参考文档: `Startup and Shutdown`_"""
|
"""参考文档: [`Startup and Shutdown`](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html)"""
|
||||||
return self.server_app.after_serving(func) # type: ignore
|
return self.server_app.after_serving(func) # type: ignore
|
||||||
|
|
||||||
@overrides(ReverseDriver)
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: Optional[str] = None,
|
||||||
@@ -191,7 +169,7 @@ class Driver(ReverseDriver):
|
|||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""使用 ``uvicorn`` 启动 Quart"""
|
"""使用 `uvicorn` 启动 Quart"""
|
||||||
super().run(host, port, app, **kwargs)
|
super().run(host, port, app, **kwargs)
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@@ -225,9 +203,7 @@ class Driver(ReverseDriver):
|
|||||||
async def _handle_http(self, setup: HTTPServerSetup) -> Response:
|
async def _handle_http(self, setup: HTTPServerSetup) -> Response:
|
||||||
request: Request = _request
|
request: Request = _request
|
||||||
|
|
||||||
json = None
|
json = await request.get_json() if request.is_json else None
|
||||||
if request.is_json:
|
|
||||||
json = await request.get_json()
|
|
||||||
|
|
||||||
data = await request.form
|
data = await request.form
|
||||||
files_dict = await request.files
|
files_dict = await request.files
|
||||||
@@ -240,7 +216,7 @@ class Driver(ReverseDriver):
|
|||||||
http_request = BaseRequest(
|
http_request = BaseRequest(
|
||||||
request.method,
|
request.method,
|
||||||
request.url,
|
request.url,
|
||||||
headers=request.headers.items(),
|
headers=list(request.headers.items()),
|
||||||
cookies=list(request.cookies.items()),
|
cookies=list(request.cookies.items()),
|
||||||
content=await request.get_data(
|
content=await request.get_data(
|
||||||
cache=False, as_text=False, parse_form_data=False
|
cache=False, as_text=False, parse_form_data=False
|
||||||
@@ -260,49 +236,61 @@ class Driver(ReverseDriver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
|
async def _handle_ws(self, setup: WebSocketServerSetup) -> None:
|
||||||
websocket: QuartWebSocket = _websocket
|
ctx = cast(WebsocketContext, websocket_ctx.copy())
|
||||||
|
websocket = websocket_ctx.websocket
|
||||||
|
|
||||||
http_request = BaseRequest(
|
http_request = BaseRequest(
|
||||||
websocket.method,
|
websocket.method,
|
||||||
websocket.url,
|
websocket.url,
|
||||||
headers=websocket.headers.items(),
|
headers=list(websocket.headers.items()),
|
||||||
cookies=list(websocket.cookies.items()),
|
cookies=list(websocket.cookies.items()),
|
||||||
version=websocket.http_version,
|
version=websocket.http_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
ws = WebSocket(request=http_request, websocket=websocket)
|
ws = WebSocket(request=http_request, websocket_ctx=ctx)
|
||||||
|
|
||||||
await setup.handle_func(ws)
|
await setup.handle_func(ws)
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(BaseWebSocket):
|
class WebSocket(BaseWebSocket):
|
||||||
def __init__(self, *, request: BaseRequest, websocket: QuartWebSocket):
|
"""Quart WebSocket Wrapper"""
|
||||||
|
|
||||||
|
def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext):
|
||||||
super().__init__(request=request)
|
super().__init__(request=request)
|
||||||
self.websocket = websocket
|
self.websocket_ctx = websocket_ctx
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(BaseWebSocket)
|
def websocket(self) -> QuartWebSocket:
|
||||||
|
return self.websocket_ctx.websocket
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
def closed(self):
|
def closed(self):
|
||||||
# FIXME
|
# FIXME
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
await self.websocket.accept()
|
await self.websocket.accept()
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def close(self, code: int = 1000, reason: str = ""):
|
async def close(self, code: int = 1000, reason: str = ""):
|
||||||
await self.websocket.close(code, reason)
|
await self.websocket.close(code, reason)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive(self) -> str:
|
async def receive(self) -> Union[str, bytes]:
|
||||||
|
return await self.websocket.receive()
|
||||||
|
|
||||||
|
@override
|
||||||
|
@catch_closed
|
||||||
|
async def receive_text(self) -> str:
|
||||||
msg = await self.websocket.receive()
|
msg = await self.websocket.receive()
|
||||||
if isinstance(msg, bytes):
|
if isinstance(msg, bytes):
|
||||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive_bytes(self) -> bytes:
|
async def receive_bytes(self) -> bytes:
|
||||||
msg = await self.websocket.receive()
|
msg = await self.websocket.receive()
|
||||||
@@ -310,10 +298,13 @@ class WebSocket(BaseWebSocket):
|
|||||||
raise TypeError("WebSocket received unexpected frame type: str")
|
raise TypeError("WebSocket received unexpected frame type: str")
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send(self, data: str):
|
async def send_text(self, data: str):
|
||||||
await self.websocket.send(data)
|
await self.websocket.send(data)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send_bytes(self, data: bytes):
|
async def send_bytes(self, data: bytes):
|
||||||
await self.websocket.send(data)
|
await self.websocket.send(data)
|
||||||
|
|
||||||
|
|
||||||
|
__autodoc__ = {"catch_closed": False}
|
||||||
|
|||||||
@@ -1,58 +1,77 @@
|
|||||||
|
"""[websockets](https://websockets.readthedocs.io/) 驱动适配
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nb driver install websockets
|
||||||
|
# 或者
|
||||||
|
pip install nonebot2[websockets]
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
本驱动仅支持客户端 WebSocket 连接
|
||||||
|
:::
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 4
|
||||||
|
description: nonebot.drivers.websockets 模块
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import AsyncGenerator
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing_extensions import ParamSpec, override
|
||||||
|
from typing import TYPE_CHECKING, Union, TypeVar, Callable, Awaitable, AsyncGenerator
|
||||||
|
|
||||||
from nonebot.typing import overrides
|
from nonebot.drivers import Request
|
||||||
from nonebot.log import LoguruHandler
|
from nonebot.log import LoguruHandler
|
||||||
from nonebot.drivers import Request, Response
|
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.drivers._block_driver import BlockDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers import ForwardMixin, combine_driver
|
from nonebot.drivers import WebSocketClientMixin, combine_driver
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from websockets.exceptions import ConnectionClosed
|
from websockets.exceptions import ConnectionClosed
|
||||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||||
except ImportError:
|
except ModuleNotFoundError as e: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
"Please install websockets first to use this driver. "
|
||||||
)
|
"Install with pip: `pip install nonebot2[websockets]`"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
P = ParamSpec("P")
|
||||||
|
|
||||||
logger = logging.Logger("websockets.client", "INFO")
|
logger = logging.Logger("websockets.client", "INFO")
|
||||||
logger.addHandler(LoguruHandler())
|
logger.addHandler(LoguruHandler())
|
||||||
|
|
||||||
|
|
||||||
def catch_closed(func):
|
def catch_closed(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def decorator(*args, **kwargs):
|
async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except ConnectionClosed as e:
|
except ConnectionClosed as e:
|
||||||
if e.rcvd_then_sent:
|
if e.rcvd_then_sent:
|
||||||
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason)
|
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason) # type: ignore
|
||||||
else:
|
else:
|
||||||
raise WebSocketClosed(e.sent.code, e.sent.reason)
|
raise WebSocketClosed(e.sent.code, e.sent.reason) # type: ignore
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Mixin(ForwardMixin):
|
class Mixin(WebSocketClientMixin):
|
||||||
|
"""Websockets Mixin"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
return "websockets"
|
return "websockets"
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
|
||||||
return await super(Mixin, self).request(setup)
|
|
||||||
|
|
||||||
@overrides(ForwardMixin)
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||||
connection = Connect(
|
connection = Connect(
|
||||||
str(setup.url),
|
str(setup.url),
|
||||||
extra_headers=setup.headers.items(),
|
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||||
open_timeout=setup.timeout,
|
open_timeout=setup.timeout,
|
||||||
)
|
)
|
||||||
async with connection as ws:
|
async with connection as ws:
|
||||||
@@ -60,33 +79,40 @@ class Mixin(ForwardMixin):
|
|||||||
|
|
||||||
|
|
||||||
class WebSocket(BaseWebSocket):
|
class WebSocket(BaseWebSocket):
|
||||||
@overrides(BaseWebSocket)
|
"""Websockets WebSocket Wrapper"""
|
||||||
|
|
||||||
|
@override
|
||||||
def __init__(self, *, request: Request, websocket: WebSocketClientProtocol):
|
def __init__(self, *, request: Request, websocket: WebSocketClientProtocol):
|
||||||
super().__init__(request=request)
|
super().__init__(request=request)
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
def closed(self) -> bool:
|
def closed(self) -> bool:
|
||||||
return self.websocket.closed
|
return self.websocket.closed
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def close(self, code: int = 1000, reason: str = ""):
|
async def close(self, code: int = 1000, reason: str = ""):
|
||||||
await self.websocket.close(code, reason)
|
await self.websocket.close(code, reason)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive(self) -> str:
|
async def receive(self) -> Union[str, bytes]:
|
||||||
|
return await self.websocket.recv()
|
||||||
|
|
||||||
|
@override
|
||||||
|
@catch_closed
|
||||||
|
async def receive_text(self) -> str:
|
||||||
msg = await self.websocket.recv()
|
msg = await self.websocket.recv()
|
||||||
if isinstance(msg, bytes):
|
if isinstance(msg, bytes):
|
||||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive_bytes(self) -> bytes:
|
async def receive_bytes(self) -> bytes:
|
||||||
msg = await self.websocket.recv()
|
msg = await self.websocket.recv()
|
||||||
@@ -94,13 +120,20 @@ class WebSocket(BaseWebSocket):
|
|||||||
raise TypeError("WebSocket received unexpected frame type: str")
|
raise TypeError("WebSocket received unexpected frame type: str")
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send(self, data: str) -> None:
|
async def send_text(self, data: str) -> None:
|
||||||
await self.websocket.send(data)
|
await self.websocket.send(data)
|
||||||
|
|
||||||
@overrides(BaseWebSocket)
|
@override
|
||||||
async def send_bytes(self, data: bytes) -> None:
|
async def send_bytes(self, data: bytes) -> None:
|
||||||
await self.websocket.send(data)
|
await self.websocket.send(data)
|
||||||
|
|
||||||
|
|
||||||
Driver = combine_driver(BlockDriver, Mixin)
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class Driver(Mixin, NoneDriver):
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
Driver = combine_driver(NoneDriver, Mixin)
|
||||||
|
"""Websockets Driver"""
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
"""
|
"""本模块包含了所有 NoneBot 运行时可能会抛出的异常。
|
||||||
异常
|
|
||||||
====
|
|
||||||
|
|
||||||
下列文档中的异常是所有 NoneBot 运行时可能会抛出的。
|
|
||||||
这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。
|
这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NoneBotException
|
||||||
|
├── ParserExit
|
||||||
|
├── ProcessException
|
||||||
|
| ├── IgnoredException
|
||||||
|
| ├── SkippedException
|
||||||
|
| | └── TypeMisMatch
|
||||||
|
| ├── MockApiException
|
||||||
|
| └── StopPropagation
|
||||||
|
├── MatcherException
|
||||||
|
| ├── PausedException
|
||||||
|
| ├── RejectedException
|
||||||
|
| └── FinishedException
|
||||||
|
├── AdapterException
|
||||||
|
| ├── NoLogException
|
||||||
|
| ├── ApiNotAvailable
|
||||||
|
| ├── NetworkError
|
||||||
|
| └── ActionFailed
|
||||||
|
└── DriverException
|
||||||
|
└── WebSocketClosed
|
||||||
|
```
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 10
|
||||||
|
description: nonebot.exception 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
@@ -12,253 +35,208 @@ from pydantic.fields import ModelField
|
|||||||
|
|
||||||
|
|
||||||
class NoneBotException(Exception):
|
class NoneBotException(Exception):
|
||||||
"""
|
"""所有 NoneBot 发生的异常基类。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
所有 NoneBot 发生的异常基类。
|
def __str__(self) -> str:
|
||||||
"""
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
# Rule Exception
|
# Rule Exception
|
||||||
class ParserExit(NoneBotException):
|
class ParserExit(NoneBotException):
|
||||||
"""
|
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
``shell command`` 处理消息失败时返回的异常
|
def __init__(self, status: int = 0, message: Optional[str] = None) -> None:
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``status``
|
|
||||||
* ``message``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, status: int = 0, message: Optional[str] = None):
|
|
||||||
self.status = status
|
self.status = status
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<ParserExit status={self.status} message={self.message}>"
|
return (
|
||||||
|
f"ParserExit(status={self.status}"
|
||||||
def __str__(self):
|
+ (f", message={self.message!r}" if self.message else "")
|
||||||
return self.__repr__()
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Processor Exception
|
# Processor Exception
|
||||||
class ProcessException(NoneBotException):
|
class ProcessException(NoneBotException):
|
||||||
"""
|
"""事件处理过程中发生的异常基类。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
事件处理过程中发生的异常基类。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class IgnoredException(ProcessException):
|
class IgnoredException(ProcessException):
|
||||||
"""
|
"""指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。
|
||||||
:说明:
|
|
||||||
|
|
||||||
指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。
|
参数:
|
||||||
|
reason: 忽略事件的原因
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``reason``: 忽略事件的原因
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, reason):
|
def __init__(self, reason: Any) -> None:
|
||||||
self.reason = reason
|
self.reason: Any = reason
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<IgnoredException, reason={self.reason}>"
|
return f"IgnoredException(reason={self.reason!r})"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
class SkippedException(ProcessException):
|
||||||
|
"""指示 NoneBot 立即结束当前 `Dependent` 的运行。
|
||||||
|
|
||||||
|
例如,可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
|
def always_skip():
|
||||||
|
Matcher.skip()
|
||||||
|
|
||||||
|
@matcher.handle()
|
||||||
|
async def handler(dependency = Depends(always_skip)):
|
||||||
|
# never run
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TypeMisMatch(SkippedException):
|
||||||
|
"""当前 `Handler` 的参数类型不匹配。"""
|
||||||
|
|
||||||
|
def __init__(self, param: ModelField, value: Any) -> None:
|
||||||
|
self.param: ModelField = param
|
||||||
|
self.value: Any = value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"TypeMisMatch(param={self.param.name}, "
|
||||||
|
f"type={self.param._type_display()}, value={self.value!r}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockApiException(ProcessException):
|
class MockApiException(ProcessException):
|
||||||
"""
|
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。
|
||||||
:说明:
|
可由 api hook 抛出。
|
||||||
|
|
||||||
指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
|
参数:
|
||||||
|
result: 返回的内容
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``result``: 返回的内容
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, result: Any):
|
def __init__(self, result: Any):
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<ApiCancelledException, result={self.result}>"
|
return f"MockApiException(result={self.result!r})"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
|
|
||||||
class StopPropagation(ProcessException):
|
class StopPropagation(ProcessException):
|
||||||
"""
|
"""指示 NoneBot 终止事件向下层传播。
|
||||||
:说明:
|
|
||||||
|
|
||||||
指示 NoneBot 终止事件向下层传播。
|
在 {ref}`nonebot.matcher.Matcher.block` 为 `True`
|
||||||
|
或使用 {ref}`nonebot.matcher.Matcher.stop_propagation` 方法时抛出。
|
||||||
|
|
||||||
:用法:
|
用法:
|
||||||
|
```python
|
||||||
在 ``Matcher.block == True`` 时抛出。
|
matcher = on_notice(block=True)
|
||||||
|
# 或者
|
||||||
|
@matcher.handle()
|
||||||
|
async def handler(matcher: Matcher):
|
||||||
|
matcher.stop_propagation()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Matcher Exceptions
|
# Matcher Exceptions
|
||||||
class MatcherException(NoneBotException):
|
class MatcherException(NoneBotException):
|
||||||
"""
|
"""所有 Matcher 发生的异常基类。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
所有 Matcher 发生的异常基类。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SkippedException(MatcherException):
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
指示 NoneBot 立即结束当前 ``Handler`` 的处理,继续处理下一个 ``Handler``。
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
可以在 ``Handler`` 中通过 ``Matcher.skip()`` 抛出。
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TypeMisMatch(SkippedException):
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
当前 ``Handler`` 的参数类型不匹配。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, param: ModelField, value: Any):
|
|
||||||
self.param: ModelField = param
|
|
||||||
self.value: Any = value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
self.__repr__()
|
|
||||||
|
|
||||||
|
|
||||||
class PausedException(MatcherException):
|
class PausedException(MatcherException):
|
||||||
"""
|
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。
|
||||||
:说明:
|
可用于用户输入新信息。
|
||||||
|
|
||||||
指示 NoneBot 结束当前 ``Handler`` 并等待下一条消息后继续下一个 ``Handler``。
|
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。
|
||||||
可用于用户输入新信息。
|
|
||||||
|
|
||||||
:用法:
|
用法:
|
||||||
|
```python
|
||||||
可以在 ``Handler`` 中通过 ``Matcher.pause()`` 抛出。
|
@matcher.handle()
|
||||||
|
async def handler():
|
||||||
|
await matcher.pause("some message")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RejectedException(MatcherException):
|
class RejectedException(MatcherException):
|
||||||
"""
|
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。
|
||||||
:说明:
|
可用于用户重新输入。
|
||||||
|
|
||||||
指示 NoneBot 结束当前 ``Handler`` 并等待下一条消息后重新运行当前 ``Handler``。
|
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。
|
||||||
可用于用户重新输入。
|
|
||||||
|
|
||||||
:用法:
|
用法:
|
||||||
|
```python
|
||||||
可以在 ``Handler`` 中通过 ``Matcher.reject()`` 抛出。
|
@matcher.handle()
|
||||||
|
async def handler():
|
||||||
|
await matcher.reject("some message")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class FinishedException(MatcherException):
|
class FinishedException(MatcherException):
|
||||||
"""
|
"""指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。
|
||||||
:说明:
|
|
||||||
|
|
||||||
指示 NoneBot 结束当前 ``Handler`` 且后续 ``Handler`` 不再被运行。
|
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.finish` 抛出。
|
||||||
可用于结束用户会话。
|
|
||||||
|
|
||||||
:用法:
|
用法:
|
||||||
|
```python
|
||||||
可以在 ``Handler`` 中通过 ``Matcher.finish()`` 抛出。
|
@matcher.handle()
|
||||||
|
async def handler():
|
||||||
|
await matcher.finish("some message")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Adapter Exceptions
|
# Adapter Exceptions
|
||||||
class AdapterException(NoneBotException):
|
class AdapterException(NoneBotException):
|
||||||
"""
|
"""代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。
|
||||||
:说明:
|
|
||||||
|
|
||||||
代表 ``Adapter`` 抛出的异常,所有的 ``Adapter`` 都要在内部继承自这个 ``Exception``
|
参数:
|
||||||
|
adapter_name: 标识 adapter
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``adapter_name: str``: 标识 adapter
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, adapter_name: str) -> None:
|
def __init__(self, adapter_name: str, *args: object) -> None:
|
||||||
self.adapter_name = adapter_name
|
super().__init__(*args)
|
||||||
|
self.adapter_name: str = adapter_name
|
||||||
|
|
||||||
|
|
||||||
class NoLogException(AdapterException):
|
class NoLogException(AdapterException):
|
||||||
"""
|
"""指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。
|
||||||
:说明:
|
|
||||||
|
|
||||||
指示 NoneBot 对当前 ``Event`` 进行处理但不显示 Log 信息,可在 ``get_log_string`` 时抛出
|
可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ApiNotAvailable(AdapterException):
|
class ApiNotAvailable(AdapterException):
|
||||||
"""
|
"""在 API 连接不可用时抛出。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
在 API 连接不可用时抛出。
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(AdapterException):
|
class NetworkError(AdapterException):
|
||||||
|
"""在网络出现问题时抛出,
|
||||||
|
如: API 请求地址不正确, API 请求无返回或返回状态非正常等。
|
||||||
"""
|
"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
在网络出现问题时抛出,如: API 请求地址不正确, API 请求无返回或返回状态非正常等。
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ActionFailed(AdapterException):
|
class ActionFailed(AdapterException):
|
||||||
"""
|
"""API 请求成功返回数据,但 API 操作失败。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
API 请求成功返回数据,但 API 操作失败。
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Driver Exceptions
|
# Driver Exceptions
|
||||||
class DriverException(NoneBotException):
|
class DriverException(NoneBotException):
|
||||||
"""
|
"""`Driver` 抛出的异常基类。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
``Driver`` 抛出的异常基类
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClosed(DriverException):
|
class WebSocketClosed(DriverException):
|
||||||
"""
|
"""WebSocket 连接已关闭。"""
|
||||||
:说明:
|
|
||||||
|
|
||||||
WebSocket 连接已关闭
|
def __init__(self, code: int, reason: Optional[str] = None) -> None:
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code: int, reason: Optional[str] = None):
|
|
||||||
self.code = code
|
self.code = code
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<WebSocketClosed code={self.code} reason={self.reason}>"
|
return (
|
||||||
|
f"WebSocketClosed(code={self.code}"
|
||||||
|
+ (f", reason={self.reason!r}" if self.reason else "")
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|||||||
6
nonebot/internal/adapter/__init__.py
Normal file
6
nonebot/internal/adapter/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .bot import Bot as Bot
|
||||||
|
from .event import Event as Event
|
||||||
|
from .adapter import Adapter as Adapter
|
||||||
|
from .message import Message as Message
|
||||||
|
from .message import MessageSegment as MessageSegment
|
||||||
|
from .template import MessageTemplate as MessageTemplate
|
||||||
111
nonebot/internal/adapter/adapter.py
Normal file
111
nonebot/internal/adapter/adapter.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import abc
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any, Dict, AsyncGenerator
|
||||||
|
|
||||||
|
from nonebot.config import Config
|
||||||
|
from nonebot.internal.driver import (
|
||||||
|
Driver,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
ASGIMixin,
|
||||||
|
WebSocket,
|
||||||
|
HTTPClientMixin,
|
||||||
|
HTTPServerSetup,
|
||||||
|
WebSocketClientMixin,
|
||||||
|
WebSocketServerSetup,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .bot import Bot
|
||||||
|
|
||||||
|
|
||||||
|
class Adapter(abc.ABC):
|
||||||
|
"""协议适配器基类。
|
||||||
|
|
||||||
|
通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
driver: {ref}`nonebot.drivers.Driver` 实例
|
||||||
|
kwargs: 其他由 {ref}`nonebot.drivers.Driver.register_adapter` 传入的额外参数
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, driver: Driver, **kwargs: Any):
|
||||||
|
self.driver: Driver = driver
|
||||||
|
"""{ref}`nonebot.drivers.Driver` 实例"""
|
||||||
|
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:
|
||||||
|
"""当前协议适配器的名称"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Config:
|
||||||
|
"""全局 NoneBot 配置"""
|
||||||
|
return self.driver.config
|
||||||
|
|
||||||
|
def bot_connect(self, bot: Bot) -> None:
|
||||||
|
"""告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters.Bot` 连接。
|
||||||
|
|
||||||
|
当有新的 {ref}`nonebot.adapters.Bot` 实例连接建立成功时调用。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: {ref}`nonebot.adapters.Bot` 实例
|
||||||
|
"""
|
||||||
|
self.driver._bot_connect(bot)
|
||||||
|
self.bots[bot.self_id] = bot
|
||||||
|
|
||||||
|
def bot_disconnect(self, bot: Bot) -> None:
|
||||||
|
"""告知 NoneBot {ref}`nonebot.adapters.Bot` 连接已断开。
|
||||||
|
|
||||||
|
当有 {ref}`nonebot.adapters.Bot` 实例连接断开时调用。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: {ref}`nonebot.adapters.Bot` 实例
|
||||||
|
"""
|
||||||
|
if self.bots.pop(bot.self_id, None) is None:
|
||||||
|
raise RuntimeError(f"{bot} not found in adapter {self.get_name()}")
|
||||||
|
self.driver._bot_disconnect(bot)
|
||||||
|
|
||||||
|
def setup_http_server(self, setup: HTTPServerSetup):
|
||||||
|
"""设置一个 HTTP 服务器路由配置"""
|
||||||
|
if not isinstance(self.driver, ASGIMixin):
|
||||||
|
raise TypeError("Current driver does not support http server")
|
||||||
|
self.driver.setup_http_server(setup)
|
||||||
|
|
||||||
|
def setup_websocket_server(self, setup: WebSocketServerSetup):
|
||||||
|
"""设置一个 WebSocket 服务器路由配置"""
|
||||||
|
if not isinstance(self.driver, ASGIMixin):
|
||||||
|
raise TypeError("Current driver does not support websocket server")
|
||||||
|
self.driver.setup_websocket_server(setup)
|
||||||
|
|
||||||
|
async def request(self, setup: Request) -> Response:
|
||||||
|
"""进行一个 HTTP 客户端请求"""
|
||||||
|
if not isinstance(self.driver, HTTPClientMixin):
|
||||||
|
raise TypeError("Current driver does not support http client")
|
||||||
|
return await self.driver.request(setup)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||||
|
"""建立一个 WebSocket 客户端连接请求"""
|
||||||
|
if not isinstance(self.driver, WebSocketClientMixin):
|
||||||
|
raise TypeError("Current driver does not support websocket client")
|
||||||
|
async with self.driver.websocket(setup) as ws:
|
||||||
|
yield ws
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
|
||||||
|
"""`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
api: API 名称
|
||||||
|
data: API 数据
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
__autodoc__ = {"Adapter._call_api": True}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import abc
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing_extensions import Protocol
|
from typing import TYPE_CHECKING, Any, Set, Union, Optional, Protocol
|
||||||
from typing import TYPE_CHECKING, Any, Set, Union, Optional
|
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.config import Config
|
from nonebot.config import Config
|
||||||
@@ -10,79 +9,75 @@ from nonebot.exception import MockApiException
|
|||||||
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._event import Event
|
from .event import Event
|
||||||
from ._adapter import Adapter
|
from .adapter import Adapter
|
||||||
from ._message import Message, MessageSegment
|
from .message import Message, MessageSegment
|
||||||
|
|
||||||
|
class _ApiCall(Protocol):
|
||||||
class _ApiCall(Protocol):
|
async def __call__(self, **kwargs: Any) -> Any:
|
||||||
async def __call__(self, **kwargs: Any) -> Any:
|
...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Bot(abc.ABC):
|
class Bot(abc.ABC):
|
||||||
"""
|
"""Bot 基类。
|
||||||
Bot 基类。用于处理上报消息,并提供 API 调用接口。
|
|
||||||
|
用于处理上报消息,并提供 API 调用接口。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
adapter: 协议适配器实例
|
||||||
|
self_id: 机器人 ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_calling_api_hook: Set[T_CallingAPIHook] = set()
|
_calling_api_hook: Set[T_CallingAPIHook] = set()
|
||||||
"""
|
"""call_api 时执行的函数"""
|
||||||
:类型: ``Set[T_CallingAPIHook]``
|
|
||||||
:说明: call_api 时执行的函数
|
|
||||||
"""
|
|
||||||
_called_api_hook: Set[T_CalledAPIHook] = set()
|
_called_api_hook: Set[T_CalledAPIHook] = set()
|
||||||
"""
|
"""call_api 后执行的函数"""
|
||||||
:类型: ``Set[T_CalledAPIHook]``
|
|
||||||
:说明: call_api 后执行的函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, adapter: "Adapter", self_id: str):
|
def __init__(self, adapter: "Adapter", self_id: str):
|
||||||
"""
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``self_id: str``: 机器人 ID
|
|
||||||
* ``request: HTTPConnection``: request 连接对象
|
|
||||||
"""
|
|
||||||
self.adapter: "Adapter" = adapter
|
self.adapter: "Adapter" = adapter
|
||||||
|
"""协议适配器实例"""
|
||||||
self.self_id: str = self_id
|
self.self_id: str = self_id
|
||||||
"""机器人 ID"""
|
"""机器人 ID"""
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> _ApiCall:
|
def __repr__(self) -> str:
|
||||||
|
return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> "_ApiCall":
|
||||||
|
if name.startswith("__") and name.endswith("__"):
|
||||||
|
raise AttributeError(
|
||||||
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||||
|
)
|
||||||
return partial(self.call_api, name)
|
return partial(self.call_api, name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
|
"""协议适配器名称"""
|
||||||
return self.adapter.get_name()
|
return self.adapter.get_name()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> Config:
|
def config(self) -> Config:
|
||||||
|
"""全局 NoneBot 配置"""
|
||||||
return self.adapter.config
|
return self.adapter.config
|
||||||
|
|
||||||
async def call_api(self, api: str, **data: Any) -> Any:
|
async def call_api(self, api: str, **data: Any) -> Any:
|
||||||
"""
|
"""调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用
|
||||||
:说明:
|
|
||||||
|
|
||||||
调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用
|
参数:
|
||||||
|
api: API 名称
|
||||||
:参数:
|
data: API 数据
|
||||||
|
|
||||||
* ``api: str``: API 名称
|
|
||||||
* ``**data``: API 数据
|
|
||||||
|
|
||||||
:示例:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
await bot.call_api("send_msg", message="hello world")
|
await bot.call_api("send_msg", message="hello world")
|
||||||
await bot.send_msg(message="hello world")
|
await bot.send_msg(message="hello world")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result: Any = None
|
result: Any = None
|
||||||
skip_calling_api: bool = False
|
skip_calling_api: bool = False
|
||||||
exception: Optional[Exception] = None
|
exception: Optional[Exception] = None
|
||||||
|
|
||||||
coros = list(map(lambda x: x(self, api, data), self._calling_api_hook))
|
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
||||||
if coros:
|
|
||||||
try:
|
try:
|
||||||
logger.debug("Running CallingAPI hooks...")
|
logger.debug("Running CallingAPI hooks...")
|
||||||
await asyncio.gather(*coros)
|
await asyncio.gather(*coros)
|
||||||
@@ -104,15 +99,17 @@ class Bot(abc.ABC):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception = e
|
exception = e
|
||||||
|
|
||||||
coros = list(
|
if coros := [
|
||||||
map(lambda x: x(self, exception, api, data, result), self._called_api_hook)
|
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
||||||
)
|
]:
|
||||||
if coros:
|
|
||||||
try:
|
try:
|
||||||
logger.debug("Running CalledAPI hooks...")
|
logger.debug("Running CalledAPI hooks...")
|
||||||
await asyncio.gather(*coros)
|
await asyncio.gather(*coros)
|
||||||
except MockApiException as e:
|
except MockApiException as e:
|
||||||
|
# mock api result
|
||||||
result = e.result
|
result = e.result
|
||||||
|
# ignore exception
|
||||||
|
exception = None
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calling API {api} result is mocked. Return {result} instead."
|
f"Calling API {api} result is mocked. Return {result} instead."
|
||||||
)
|
)
|
||||||
@@ -128,51 +125,44 @@ class Bot(abc.ABC):
|
|||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def send(
|
async def send(
|
||||||
self, event: "Event", message: Union[str, "Message", "MessageSegment"], **kwargs
|
self,
|
||||||
|
event: "Event",
|
||||||
|
message: Union[str, "Message", "MessageSegment"],
|
||||||
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""调用机器人基础发送消息接口
|
||||||
:说明:
|
|
||||||
|
|
||||||
调用机器人基础发送消息接口
|
参数:
|
||||||
|
event: 上报事件
|
||||||
:参数:
|
message: 要发送的消息
|
||||||
|
kwargs: 任意额外参数
|
||||||
* ``event: Event``: 上报事件
|
|
||||||
* ``message: Union[str, Message, MessageSegment]``: 要发送的消息
|
|
||||||
* ``**kwargs``
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook:
|
def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook:
|
||||||
"""
|
"""调用 api 预处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
调用 api 预处理。
|
钩子函数参数:
|
||||||
|
|
||||||
:参数:
|
- bot: 当前 bot 对象
|
||||||
|
- api: 调用的 api 名称
|
||||||
* ``bot: Bot``: 当前 bot 对象
|
- data: api 调用的参数字典
|
||||||
* ``api: str``: 调用的 api 名称
|
|
||||||
* ``data: Dict[str, Any]``: api 调用的参数字典
|
|
||||||
"""
|
"""
|
||||||
cls._calling_api_hook.add(func)
|
cls._calling_api_hook.add(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def on_called_api(cls, func: T_CalledAPIHook) -> T_CalledAPIHook:
|
def on_called_api(cls, func: T_CalledAPIHook) -> T_CalledAPIHook:
|
||||||
"""
|
"""调用 api 后处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
调用 api 后处理。
|
钩子函数参数:
|
||||||
|
|
||||||
:参数:
|
- bot: 当前 bot 对象
|
||||||
|
- exception: 调用 api 时发生的错误
|
||||||
* ``bot: Bot``: 当前 bot 对象
|
- api: 调用的 api 名称
|
||||||
* ``exception: Optional[Exception]``: 调用 api 时发生的错误
|
- data: api 调用的参数字典
|
||||||
* ``api: str``: 调用的 api 名称
|
- result: api 调用的返回
|
||||||
* ``data: Dict[str, Any]``: api 调用的参数字典
|
|
||||||
* ``result: Any``: api 调用的返回
|
|
||||||
"""
|
"""
|
||||||
cls._called_api_hook.add(func)
|
cls._called_api_hook.add(func)
|
||||||
return func
|
return func
|
||||||
82
nonebot/internal/adapter/event.py
Normal file
82
nonebot/internal/adapter/event.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import abc
|
||||||
|
from typing import Any, Type, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from nonebot.utils import DataclassEncoder
|
||||||
|
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
E = TypeVar("E", bound="Event")
|
||||||
|
|
||||||
|
|
||||||
|
class Event(abc.ABC, BaseModel):
|
||||||
|
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "allow"
|
||||||
|
json_encoders = {Message: DataclassEncoder}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls: Type["E"], value: Any) -> "E":
|
||||||
|
if isinstance(value, Event) and not isinstance(value, cls):
|
||||||
|
raise TypeError(f"{value} is incompatible with Event type {cls}")
|
||||||
|
return super().validate(value)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_type(self) -> str:
|
||||||
|
"""获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_event_name(self) -> str:
|
||||||
|
"""获取事件名称的方法。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_event_description(self) -> str:
|
||||||
|
"""获取事件描述的方法,通常为事件具体内容。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||||
|
|
||||||
|
def get_log_string(self) -> str:
|
||||||
|
"""获取事件日志信息的方法。
|
||||||
|
|
||||||
|
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,
|
||||||
|
可以抛出 `NoLogException` 异常。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
NoLogException: 希望 NoneBot 隐藏该事件日志
|
||||||
|
"""
|
||||||
|
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_user_id(self) -> str:
|
||||||
|
"""获取事件主体 id 的方法,通常是用户 id 。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_session_id(self) -> str:
|
||||||
|
"""获取会话 id 的方法,用于判断当前事件属于哪一个会话,
|
||||||
|
通常是用户 id、群组 id 组合。
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_message(self) -> "Message":
|
||||||
|
"""获取事件消息内容的方法。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_plaintext(self) -> str:
|
||||||
|
"""获取消息纯文本的方法。
|
||||||
|
|
||||||
|
通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。
|
||||||
|
"""
|
||||||
|
return self.get_message().extract_plain_text()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_tome(self) -> bool:
|
||||||
|
"""获取事件是否与机器人有关的方法。"""
|
||||||
|
raise NotImplementedError
|
||||||
430
nonebot/internal/adapter/message.py
Normal file
430
nonebot/internal/adapter/message.py
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
import abc
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing_extensions import Self
|
||||||
|
from dataclasses import field, asdict, dataclass
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Type,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
Generic,
|
||||||
|
TypeVar,
|
||||||
|
Iterable,
|
||||||
|
Optional,
|
||||||
|
SupportsIndex,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pydantic import parse_obj_as
|
||||||
|
|
||||||
|
from .template import MessageTemplate
|
||||||
|
|
||||||
|
TMS = TypeVar("TMS", bound="MessageSegment")
|
||||||
|
TM = TypeVar("TM", bound="Message")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageSegment(abc.ABC, Generic[TM]):
|
||||||
|
"""消息段基类"""
|
||||||
|
|
||||||
|
type: str
|
||||||
|
"""消息段类型"""
|
||||||
|
data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
"""消息段数据"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_message_class(cls) -> Type[TM]:
|
||||||
|
"""获取消息数组类型"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""该消息段所代表的 str,在命令匹配部分使用"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(str(self))
|
||||||
|
|
||||||
|
def __ne__(self, other: Self) -> bool:
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
def __add__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||||
|
return self.get_message_class()(self) + other
|
||||||
|
|
||||||
|
def __radd__(self: TMS, other: Union[str, TMS, Iterable[TMS]]) -> TM:
|
||||||
|
return self.get_message_class()(other) + self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
yield cls._validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate(cls, value) -> Self:
|
||||||
|
if isinstance(value, cls):
|
||||||
|
return value
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
|
||||||
|
if "type" not in value:
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected dict with 'type' for MessageSegment, got {value}"
|
||||||
|
)
|
||||||
|
return cls(type=value["type"], data=value.get("data", {}))
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any = None):
|
||||||
|
return asdict(self).get(key, default)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return asdict(self).keys()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return asdict(self).values()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return asdict(self).items()
|
||||||
|
|
||||||
|
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
|
||||||
|
def is_text(self) -> bool:
|
||||||
|
"""当前消息段是否为纯文本"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Message(List[TMS], abc.ABC):
|
||||||
|
"""消息序列
|
||||||
|
|
||||||
|
参数:
|
||||||
|
message: 消息内容
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: Union[str, None, Iterable[TMS], TMS] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
if message is None:
|
||||||
|
return
|
||||||
|
elif isinstance(message, str):
|
||||||
|
self.extend(self._construct(message))
|
||||||
|
elif isinstance(message, MessageSegment):
|
||||||
|
self.append(message)
|
||||||
|
elif isinstance(message, Iterable):
|
||||||
|
self.extend(message)
|
||||||
|
else:
|
||||||
|
self.extend(self._construct(message)) # pragma: no cover
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
|
||||||
|
"""创建消息模板。
|
||||||
|
|
||||||
|
用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。
|
||||||
|
并且提供了拓展的格式化控制符,
|
||||||
|
可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
format_string: 格式化模板
|
||||||
|
|
||||||
|
返回:
|
||||||
|
消息格式化器
|
||||||
|
"""
|
||||||
|
return MessageTemplate(format_string, cls)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_segment_class(cls) -> Type[TMS]:
|
||||||
|
"""获取消息段类型"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "".join(str(seg) for seg in self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
yield cls._validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate(cls, value) -> Self:
|
||||||
|
if isinstance(value, cls):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, Message):
|
||||||
|
raise ValueError(f"Type {type(value)} can not be converted to {cls}")
|
||||||
|
elif isinstance(value, str):
|
||||||
|
pass
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
value = parse_obj_as(cls.get_segment_class(), value)
|
||||||
|
elif isinstance(value, Iterable):
|
||||||
|
value = [parse_obj_as(cls.get_segment_class(), v) for v in value]
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected str, dict or iterable for Message, got {type(value)}"
|
||||||
|
)
|
||||||
|
return cls(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _construct(msg: str) -> Iterable[TMS]:
|
||||||
|
"""构造消息数组"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __add__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||||
|
result = self.copy()
|
||||||
|
result += other
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __radd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||||
|
result = self.__class__(other)
|
||||||
|
return result + self
|
||||||
|
|
||||||
|
def __iadd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
||||||
|
if isinstance(other, str):
|
||||||
|
self.extend(self._construct(other))
|
||||||
|
elif isinstance(other, MessageSegment):
|
||||||
|
self.append(other)
|
||||||
|
elif isinstance(other, Iterable):
|
||||||
|
self.extend(other)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unsupported type {type(other)!r}")
|
||||||
|
return self
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, args: str) -> Self:
|
||||||
|
"""获取仅包含指定消息段类型的消息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
args: 消息段类型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
所有类型为 `args` 的消息段
|
||||||
|
"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, args: Tuple[str, int]) -> TMS:
|
||||||
|
"""索引指定类型的消息段
|
||||||
|
|
||||||
|
参数:
|
||||||
|
args: 消息段类型和索引
|
||||||
|
|
||||||
|
返回:
|
||||||
|
类型为 `args[0]` 的消息段第 `args[1]` 个
|
||||||
|
"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, args: Tuple[str, slice]) -> Self:
|
||||||
|
"""切片指定类型的消息段
|
||||||
|
|
||||||
|
参数:
|
||||||
|
args: 消息段类型和切片
|
||||||
|
|
||||||
|
返回:
|
||||||
|
类型为 `args[0]` 的消息段切片 `args[1]`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, args: int) -> TMS:
|
||||||
|
"""索引消息段
|
||||||
|
|
||||||
|
参数:
|
||||||
|
args: 索引
|
||||||
|
|
||||||
|
返回:
|
||||||
|
第 `args` 个消息段
|
||||||
|
"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, args: slice) -> Self:
|
||||||
|
"""切片消息段
|
||||||
|
|
||||||
|
参数:
|
||||||
|
args: 切片
|
||||||
|
|
||||||
|
返回:
|
||||||
|
消息切片 `args`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __getitem__(
|
||||||
|
self,
|
||||||
|
args: Union[
|
||||||
|
str,
|
||||||
|
Tuple[str, int],
|
||||||
|
Tuple[str, slice],
|
||||||
|
int,
|
||||||
|
slice,
|
||||||
|
],
|
||||||
|
) -> 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)
|
||||||
|
elif isinstance(arg1, slice) and arg2 is None:
|
||||||
|
return self.__class__(super().__getitem__(arg1))
|
||||||
|
elif isinstance(arg1, str) and arg2 is None:
|
||||||
|
return self.__class__(seg for seg in self if seg.type == arg1)
|
||||||
|
elif isinstance(arg1, str) and isinstance(arg2, int):
|
||||||
|
return [seg for seg in self if seg.type == arg1][arg2]
|
||||||
|
elif isinstance(arg1, str) and isinstance(arg2, slice):
|
||||||
|
return self.__class__([seg for seg in self if seg.type == arg1][arg2])
|
||||||
|
else:
|
||||||
|
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
||||||
|
|
||||||
|
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!r} is not in message")
|
||||||
|
return super().index(first_segment, *args)
|
||||||
|
return super().index(value, *args)
|
||||||
|
|
||||||
|
def get(self, type_: str, count: Optional[int] = None) -> Self:
|
||||||
|
"""获取指定类型的消息段
|
||||||
|
|
||||||
|
参数:
|
||||||
|
type_: 消息段类型
|
||||||
|
count: 获取个数
|
||||||
|
|
||||||
|
返回:
|
||||||
|
构建的新消息
|
||||||
|
"""
|
||||||
|
if count is None:
|
||||||
|
return self[type_]
|
||||||
|
|
||||||
|
iterator, filtered = (
|
||||||
|
seg for seg in self if seg.type == type_
|
||||||
|
), self.__class__()
|
||||||
|
for _ in range(count):
|
||||||
|
seg = next(iterator, None)
|
||||||
|
if seg is None:
|
||||||
|
break
|
||||||
|
filtered.append(seg)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def count(self, value: Union[TMS, str]) -> int:
|
||||||
|
"""计算指定消息段的个数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
value: 消息段或消息段类型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
个数
|
||||||
|
"""
|
||||||
|
return len(self[value]) if isinstance(value, str) else super().count(value)
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""添加一个消息段到消息数组末尾。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
obj: 要添加的消息段
|
||||||
|
"""
|
||||||
|
if isinstance(obj, MessageSegment):
|
||||||
|
super().append(obj)
|
||||||
|
elif isinstance(obj, str):
|
||||||
|
self.extend(self._construct(obj))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover
|
||||||
|
return self
|
||||||
|
|
||||||
|
def extend(self, obj: Union[Self, Iterable[TMS]]) -> Self:
|
||||||
|
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
obj: 要添加的消息数组
|
||||||
|
"""
|
||||||
|
for segment in obj:
|
||||||
|
self.append(segment)
|
||||||
|
return self
|
||||||
|
|
||||||
|
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())
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import inspect
|
|
||||||
import functools
|
import functools
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@@ -21,17 +21,22 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import Message, MessageSegment
|
from .message import Message, MessageSegment
|
||||||
|
|
||||||
TM = TypeVar("TM", bound="Message")
|
TM = TypeVar("TM", bound="Message")
|
||||||
TF = TypeVar("TF", str, "Message")
|
TF = TypeVar("TF", str, "Message")
|
||||||
|
|
||||||
FormatSpecFunc = Callable[[Any], str]
|
FormatSpecFunc: TypeAlias = Callable[[Any], str]
|
||||||
FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc)
|
FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc)
|
||||||
|
|
||||||
|
|
||||||
class MessageTemplate(Formatter, Generic[TF]):
|
class MessageTemplate(Formatter, Generic[TF]):
|
||||||
"""消息模板格式化实现类"""
|
"""消息模板格式化实现类。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template: 模板
|
||||||
|
factory: 消息类型工厂,默认为 `str`
|
||||||
|
"""
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -45,21 +50,16 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __init__(self, template, factory=str) -> None:
|
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
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``template: Union[str, Message]``: 模板
|
|
||||||
* ``factory: Union[str, Message]``: 消息构造类型,默认为 `str`
|
|
||||||
"""
|
|
||||||
self.template: TF = template
|
|
||||||
self.factory: Type[TF] = factory
|
|
||||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
||||||
|
|
||||||
def add_format_spec(
|
def add_format_spec(
|
||||||
self, spec: FormatSpecFunc_T, name: Optional[str] = None
|
self, spec: FormatSpecFunc_T, name: Optional[str] = None
|
||||||
) -> FormatSpecFunc_T:
|
) -> FormatSpecFunc_T:
|
||||||
@@ -69,31 +69,46 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
self.format_specs[name] = spec
|
self.format_specs[name] = spec
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
def format(self, *args: Any, **kwargs: Any) -> TF:
|
def format(self, *args, **kwargs):
|
||||||
"""
|
"""根据传入参数和模板生成消息对象"""
|
||||||
:说明:
|
return self._format(args, kwargs)
|
||||||
|
|
||||||
|
def format_map(self, mapping: Mapping[str, Any]) -> TF:
|
||||||
|
"""根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用"""
|
||||||
|
return self._format([], mapping)
|
||||||
|
|
||||||
|
def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF:
|
||||||
|
full_message = self.factory()
|
||||||
|
used_args, arg_index = set(), 0
|
||||||
|
|
||||||
根据模板和参数生成消息对象
|
|
||||||
"""
|
|
||||||
msg = self.factory()
|
|
||||||
if isinstance(self.template, str):
|
if isinstance(self.template, str):
|
||||||
msg += self.vformat(self.template, args, kwargs)
|
msg, arg_index = self._vformat(
|
||||||
|
self.template, args, kwargs, used_args, arg_index
|
||||||
|
)
|
||||||
|
full_message += msg
|
||||||
elif isinstance(self.template, self.factory):
|
elif isinstance(self.template, self.factory):
|
||||||
template = cast("Message[MessageSegment]", self.template)
|
template = cast("Message[MessageSegment]", self.template)
|
||||||
for seg in template:
|
for seg in template:
|
||||||
msg += self.vformat(str(seg), args, kwargs) if seg.is_text() else seg
|
if not seg.is_text():
|
||||||
|
full_message += seg
|
||||||
|
else:
|
||||||
|
msg, arg_index = self._vformat(
|
||||||
|
str(seg), args, kwargs, used_args, arg_index
|
||||||
|
)
|
||||||
|
full_message += msg
|
||||||
else:
|
else:
|
||||||
raise TypeError("template must be a string or instance of Message!")
|
raise TypeError("template must be a string or instance of Message!")
|
||||||
|
|
||||||
return msg # type:ignore
|
self.check_unused_args(used_args, args, kwargs)
|
||||||
|
return cast(TF, full_message)
|
||||||
|
|
||||||
def vformat(
|
def vformat(
|
||||||
self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
self,
|
||||||
|
format_string: str,
|
||||||
|
args: Sequence[Any],
|
||||||
|
kwargs: Mapping[str, Any],
|
||||||
) -> TF:
|
) -> TF:
|
||||||
used_args = set()
|
raise NotImplementedError("`vformat` has merged into `_format`")
|
||||||
result, _ = self._vformat(format_string, args, kwargs, used_args, 2)
|
|
||||||
self.check_unused_args(list(used_args), args, kwargs)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _vformat(
|
def _vformat(
|
||||||
self,
|
self,
|
||||||
@@ -101,18 +116,13 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
args: Sequence[Any],
|
args: Sequence[Any],
|
||||||
kwargs: Mapping[str, Any],
|
kwargs: Mapping[str, Any],
|
||||||
used_args: Set[Union[int, str]],
|
used_args: Set[Union[int, str]],
|
||||||
recursion_depth: int,
|
|
||||||
auto_arg_index: int = 0,
|
auto_arg_index: int = 0,
|
||||||
) -> Tuple[TF, int]:
|
) -> Tuple[TF, int]:
|
||||||
if recursion_depth < 0:
|
results: List[Any] = [self.factory()]
|
||||||
raise ValueError("Max string recursion exceeded")
|
|
||||||
|
|
||||||
results: List[Any] = []
|
for literal_text, field_name, format_spec, conversion in self.parse(
|
||||||
|
|
||||||
for (literal_text, field_name, format_spec, conversion) in self.parse(
|
|
||||||
format_string
|
format_string
|
||||||
):
|
):
|
||||||
|
|
||||||
# output the literal text
|
# output the literal text
|
||||||
if literal_text:
|
if literal_text:
|
||||||
results.append(literal_text)
|
results.append(literal_text)
|
||||||
@@ -146,36 +156,23 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||||
used_args.add(arg_used)
|
used_args.add(arg_used)
|
||||||
|
|
||||||
assert format_spec is not None
|
|
||||||
|
|
||||||
# do any conversion on the resulting object
|
# do any conversion on the resulting object
|
||||||
obj = self.convert_field(obj, conversion) if conversion else obj
|
obj = self.convert_field(obj, conversion) if conversion else obj
|
||||||
|
|
||||||
# expand the format spec, if needed
|
|
||||||
format_control, auto_arg_index = self._vformat(
|
|
||||||
format_spec,
|
|
||||||
args,
|
|
||||||
kwargs,
|
|
||||||
used_args,
|
|
||||||
recursion_depth - 1,
|
|
||||||
auto_arg_index,
|
|
||||||
)
|
|
||||||
|
|
||||||
# format the object and append to the result
|
# format the object and append to the result
|
||||||
formatted_text = self.format_field(obj, str(format_control))
|
formatted_text = (
|
||||||
|
self.format_field(obj, format_spec) if format_spec else obj
|
||||||
|
)
|
||||||
results.append(formatted_text)
|
results.append(formatted_text)
|
||||||
|
|
||||||
return (
|
return functools.reduce(self._add, results), auto_arg_index
|
||||||
self.factory(functools.reduce(self._add, results or [""])),
|
|
||||||
auto_arg_index,
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_field(self, value: Any, format_spec: str) -> Any:
|
def format_field(self, value: Any, format_spec: str) -> Any:
|
||||||
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
|
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
|
||||||
if formatter is None and not issubclass(self.factory, str):
|
if formatter is None and not issubclass(self.factory, str):
|
||||||
segment_class: Type["MessageSegment"] = self.factory.get_segment_class()
|
segment_class: Type["MessageSegment"] = self.factory.get_segment_class()
|
||||||
method = getattr(segment_class, format_spec, None)
|
method = getattr(segment_class, format_spec, None)
|
||||||
if inspect.ismethod(method):
|
if callable(method) and not cast(str, method.__name__).startswith("_"):
|
||||||
formatter = getattr(segment_class, format_spec)
|
formatter = getattr(segment_class, format_spec)
|
||||||
return (
|
return (
|
||||||
super().format_field(value, format_spec)
|
super().format_field(value, format_spec)
|
||||||
30
nonebot/internal/driver/__init__.py
Normal file
30
nonebot/internal/driver/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from .model import URL as URL
|
||||||
|
from .model import RawURL as RawURL
|
||||||
|
from .abstract import Mixin as Mixin
|
||||||
|
from .model import Cookies as Cookies
|
||||||
|
from .model import Request as Request
|
||||||
|
from .abstract import Driver as Driver
|
||||||
|
from .model import FileType as FileType
|
||||||
|
from .model import Response as Response
|
||||||
|
from .model import DataTypes as DataTypes
|
||||||
|
from .model import FileTypes as FileTypes
|
||||||
|
from .model import WebSocket as WebSocket
|
||||||
|
from .model import FilesTypes as FilesTypes
|
||||||
|
from .model import QueryTypes as QueryTypes
|
||||||
|
from .abstract import ASGIMixin as ASGIMixin
|
||||||
|
from .model import CookieTypes as CookieTypes
|
||||||
|
from .model import FileContent as FileContent
|
||||||
|
from .model import HTTPVersion as HTTPVersion
|
||||||
|
from .model import HeaderTypes as HeaderTypes
|
||||||
|
from .model import SimpleQuery as SimpleQuery
|
||||||
|
from .model import ContentTypes as ContentTypes
|
||||||
|
from .model import QueryVariable as QueryVariable
|
||||||
|
from .abstract import ForwardMixin as ForwardMixin
|
||||||
|
from .abstract import ReverseMixin as ReverseMixin
|
||||||
|
from .abstract import ForwardDriver as ForwardDriver
|
||||||
|
from .abstract import ReverseDriver as ReverseDriver
|
||||||
|
from .combine import combine_driver as combine_driver
|
||||||
|
from .model import HTTPServerSetup as HTTPServerSetup
|
||||||
|
from .abstract import HTTPClientMixin as HTTPClientMixin
|
||||||
|
from .model import WebSocketServerSetup as WebSocketServerSetup
|
||||||
|
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||||
284
nonebot/internal/driver/abstract.py
Normal file
284
nonebot/internal/driver/abstract.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import abc
|
||||||
|
import asyncio
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
|
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||||
|
|
||||||
|
from nonebot.log import logger
|
||||||
|
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.internal.params import BotParam, DependParam, DefaultParam
|
||||||
|
from nonebot.typing import (
|
||||||
|
T_DependencyCache,
|
||||||
|
T_BotConnectionHook,
|
||||||
|
T_BotDisconnectionHook,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from nonebot.internal.adapter import Bot, Adapter
|
||||||
|
|
||||||
|
|
||||||
|
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
|
||||||
|
|
||||||
|
|
||||||
|
class Driver(abc.ABC):
|
||||||
|
"""驱动器基类。
|
||||||
|
|
||||||
|
驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
env: 包含环境信息的 Env 对象
|
||||||
|
config: 包含配置信息的 Config 对象
|
||||||
|
"""
|
||||||
|
|
||||||
|
_adapters: Dict[str, "Adapter"] = {}
|
||||||
|
"""已注册的适配器列表"""
|
||||||
|
_bot_connection_hook: Set[Dependent[Any]] = set()
|
||||||
|
"""Bot 连接建立时执行的函数"""
|
||||||
|
_bot_disconnection_hook: Set[Dependent[Any]] = set()
|
||||||
|
"""Bot 连接断开时执行的函数"""
|
||||||
|
|
||||||
|
def __init__(self, env: Env, config: Config):
|
||||||
|
self.env: str = env.environment
|
||||||
|
"""环境名称"""
|
||||||
|
self.config: Config = config
|
||||||
|
"""全局配置对象"""
|
||||||
|
self._bots: Dict[str, "Bot"] = {}
|
||||||
|
self._bot_tasks: Set[asyncio.Task] = set()
|
||||||
|
|
||||||
|
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._bots
|
||||||
|
|
||||||
|
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
|
||||||
|
"""注册一个协议适配器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
adapter: 适配器类
|
||||||
|
kwargs: 其他传递给适配器的参数
|
||||||
|
"""
|
||||||
|
name = adapter.get_name()
|
||||||
|
if name in self._adapters:
|
||||||
|
logger.opt(colors=True).debug(
|
||||||
|
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self._adapters[name] = adapter(self, **kwargs)
|
||||||
|
logger.opt(colors=True).debug(
|
||||||
|
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def type(self) -> str:
|
||||||
|
"""驱动类型名称"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def logger(self):
|
||||||
|
"""驱动专属 logger 日志记录器"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
"""启动驱动框架"""
|
||||||
|
logger.opt(colors=True).debug(
|
||||||
|
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.on_shutdown(self._cleanup)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def on_startup(self, func: Callable) -> Callable:
|
||||||
|
"""注册一个在驱动器启动时执行的函数"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def on_shutdown(self, func: Callable) -> Callable:
|
||||||
|
"""注册一个在驱动器停止时执行的函数"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
|
||||||
|
"""装饰一个函数使他在 bot 连接成功时执行。
|
||||||
|
|
||||||
|
钩子函数参数:
|
||||||
|
|
||||||
|
- bot: 当前连接上的 Bot 对象
|
||||||
|
"""
|
||||||
|
cls._bot_connection_hook.add(
|
||||||
|
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
|
||||||
|
)
|
||||||
|
return func
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on_bot_disconnect(cls, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
|
||||||
|
"""装饰一个函数使他在 bot 连接断开时执行。
|
||||||
|
|
||||||
|
钩子函数参数:
|
||||||
|
|
||||||
|
- bot: 当前连接上的 Bot 对象
|
||||||
|
"""
|
||||||
|
cls._bot_disconnection_hook.add(
|
||||||
|
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
|
||||||
|
)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def _bot_connect(self, bot: "Bot") -> None:
|
||||||
|
"""在连接成功后,调用该函数来注册 bot 对象"""
|
||||||
|
if bot.self_id in self._bots:
|
||||||
|
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||||
|
self._bots[bot.self_id] = bot
|
||||||
|
|
||||||
|
async def _run_hook(bot: "Bot") -> None:
|
||||||
|
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>"
|
||||||
|
)
|
||||||
|
|
||||||
|
task = asyncio.create_task(_run_hook(bot))
|
||||||
|
task.add_done_callback(self._bot_tasks.discard)
|
||||||
|
self._bot_tasks.add(task)
|
||||||
|
|
||||||
|
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||||
|
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||||
|
if bot.self_id in self._bots:
|
||||||
|
del self._bots[bot.self_id]
|
||||||
|
|
||||||
|
async def _run_hook(bot: "Bot") -> None:
|
||||||
|
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>"
|
||||||
|
)
|
||||||
|
|
||||||
|
task = asyncio.create_task(_run_hook(bot))
|
||||||
|
task.add_done_callback(self._bot_tasks.discard)
|
||||||
|
self._bot_tasks.add(task)
|
||||||
|
|
||||||
|
async def _cleanup(self) -> None:
|
||||||
|
"""清理驱动器资源"""
|
||||||
|
if self._bot_tasks:
|
||||||
|
logger.opt(colors=True).debug(
|
||||||
|
"<y>Waiting for running bot connection hooks...</y>"
|
||||||
|
)
|
||||||
|
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Mixin(abc.ABC):
|
||||||
|
"""可与其他驱动器共用的混入基类。"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def type(self) -> str:
|
||||||
|
"""混入驱动类型名称"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardMixin(Mixin):
|
||||||
|
"""客户端混入基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class ReverseMixin(Mixin):
|
||||||
|
"""服务端混入基类。"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClientMixin(ForwardMixin):
|
||||||
|
"""HTTP 客户端混入基类。"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def request(self, setup: Request) -> Response:
|
||||||
|
"""发送一个 HTTP 请求"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClientMixin(ForwardMixin):
|
||||||
|
"""WebSocket 客户端混入基类。"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
@asynccontextmanager
|
||||||
|
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
|
||||||
|
"""发起一个 WebSocket 连接"""
|
||||||
|
raise NotImplementedError
|
||||||
|
yield # used for static type checking's generator detection
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIMixin(ReverseMixin):
|
||||||
|
"""ASGI 服务端基类。
|
||||||
|
|
||||||
|
将后端框架封装,以满足适配器使用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def server_app(self) -> Any:
|
||||||
|
"""驱动 APP 对象"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def asgi(self) -> Any:
|
||||||
|
"""驱动 ASGI 对象"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
|
||||||
|
"""设置一个 HTTP 服务器路由配置"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
|
||||||
|
"""设置一个 WebSocket 服务器路由配置"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
ForwardDriver: TypeAlias = ForwardMixin
|
||||||
|
"""支持客户端请求的驱动器。
|
||||||
|
|
||||||
|
**Deprecated**,请使用 {ref}`nonebot.drivers.ForwardMixin` 或其子类代替。
|
||||||
|
"""
|
||||||
|
|
||||||
|
ReverseDriver: TypeAlias = ReverseMixin
|
||||||
|
"""支持服务端请求的驱动器。
|
||||||
|
|
||||||
|
**Deprecated**,请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。
|
||||||
|
"""
|
||||||
45
nonebot/internal/driver/combine.py
Normal file
45
nonebot/internal/driver/combine.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from typing import TYPE_CHECKING, Type, Union, TypeVar, overload
|
||||||
|
|
||||||
|
from .abstract import Mixin, Driver
|
||||||
|
|
||||||
|
D = TypeVar("D", bound="Driver")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class CombinedDriver(Driver, Mixin):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def combine_driver(driver: Type[D]) -> Type[D]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def combine_driver(driver: Type[D], *mixins: Type[Mixin]) -> Type["CombinedDriver"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def combine_driver(
|
||||||
|
driver: Type[D], *mixins: Type[Mixin]
|
||||||
|
) -> Union[Type[D], Type["CombinedDriver"]]:
|
||||||
|
"""将一个驱动器和多个混入类合并。"""
|
||||||
|
# check first
|
||||||
|
if not issubclass(driver, Driver):
|
||||||
|
raise TypeError("`driver` must be subclass of Driver")
|
||||||
|
if not all(issubclass(m, Mixin) for m in mixins):
|
||||||
|
raise TypeError("`mixins` must be subclass of Mixin")
|
||||||
|
|
||||||
|
if not mixins:
|
||||||
|
return driver
|
||||||
|
|
||||||
|
def type_(self: "CombinedDriver") -> str:
|
||||||
|
return (
|
||||||
|
driver.type.__get__(self)
|
||||||
|
+ "+"
|
||||||
|
+ "+".join(x.type.__get__(self) for x in mixins)
|
||||||
|
)
|
||||||
|
|
||||||
|
return type(
|
||||||
|
"CombinedDriver", (*mixins, driver), {"type": property(type_)}
|
||||||
|
) # type: ignore
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import urllib.request
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
from http.cookiejar import Cookie, CookieJar
|
from http.cookiejar import Cookie, CookieJar
|
||||||
from typing import (
|
from typing import (
|
||||||
IO,
|
IO,
|
||||||
@@ -9,36 +12,40 @@ from typing import (
|
|||||||
Tuple,
|
Tuple,
|
||||||
Union,
|
Union,
|
||||||
Mapping,
|
Mapping,
|
||||||
|
Callable,
|
||||||
Iterator,
|
Iterator,
|
||||||
Optional,
|
Optional,
|
||||||
|
Awaitable,
|
||||||
MutableMapping,
|
MutableMapping,
|
||||||
)
|
)
|
||||||
|
|
||||||
from yarl import URL as URL
|
from yarl import URL as URL
|
||||||
from multidict import CIMultiDict
|
from multidict import CIMultiDict
|
||||||
|
|
||||||
RawURL = Tuple[bytes, bytes, Optional[int], bytes]
|
RawURL: TypeAlias = Tuple[bytes, bytes, Optional[int], bytes]
|
||||||
|
|
||||||
SimpleQuery = Union[str, int, float]
|
SimpleQuery: TypeAlias = Union[str, int, float]
|
||||||
QueryVariable = Union[SimpleQuery, List[SimpleQuery]]
|
QueryVariable: TypeAlias = Union[SimpleQuery, List[SimpleQuery]]
|
||||||
QueryTypes = Union[
|
QueryTypes: TypeAlias = Union[
|
||||||
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
|
None, str, Mapping[str, QueryVariable], List[Tuple[str, QueryVariable]]
|
||||||
]
|
]
|
||||||
|
|
||||||
HeaderTypes = Union[
|
HeaderTypes: TypeAlias = Union[
|
||||||
None,
|
None,
|
||||||
CIMultiDict[str],
|
CIMultiDict[str],
|
||||||
Dict[str, str],
|
Dict[str, str],
|
||||||
List[Tuple[str, str]],
|
List[Tuple[str, str]],
|
||||||
]
|
]
|
||||||
|
|
||||||
CookieTypes = Union[None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]]
|
CookieTypes: TypeAlias = Union[
|
||||||
|
None, "Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]
|
||||||
|
]
|
||||||
|
|
||||||
ContentTypes = Union[str, bytes, None]
|
ContentTypes: TypeAlias = Union[str, bytes, None]
|
||||||
DataTypes = Union[dict, None]
|
DataTypes: TypeAlias = Union[dict, None]
|
||||||
FileContent = Union[IO[bytes], bytes]
|
FileContent: TypeAlias = Union[IO[bytes], bytes]
|
||||||
FileType = Tuple[Optional[str], FileContent, Optional[str]]
|
FileType: TypeAlias = Tuple[Optional[str], FileContent, Optional[str]]
|
||||||
FileTypes = Union[
|
FileTypes: TypeAlias = Union[
|
||||||
# file (or bytes)
|
# file (or bytes)
|
||||||
FileContent,
|
FileContent,
|
||||||
# (filename, file (or bytes))
|
# (filename, file (or bytes))
|
||||||
@@ -46,7 +53,7 @@ FileTypes = Union[
|
|||||||
# (filename, file (or bytes), content_type)
|
# (filename, file (or bytes), content_type)
|
||||||
FileType,
|
FileType,
|
||||||
]
|
]
|
||||||
FilesTypes = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
|
FilesTypes: TypeAlias = Union[Dict[str, FileTypes], List[Tuple[str, FileTypes]], None]
|
||||||
|
|
||||||
|
|
||||||
class HTTPVersion(Enum):
|
class HTTPVersion(Enum):
|
||||||
@@ -102,12 +109,9 @@ class Request:
|
|||||||
self.url: URL = url
|
self.url: URL = url
|
||||||
|
|
||||||
# headers
|
# headers
|
||||||
self.headers: CIMultiDict[str]
|
self.headers: CIMultiDict[str] = (
|
||||||
if headers is not None:
|
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||||
self.headers = CIMultiDict(headers)
|
)
|
||||||
else:
|
|
||||||
self.headers = CIMultiDict()
|
|
||||||
|
|
||||||
# cookies
|
# cookies
|
||||||
self.cookies = Cookies(cookies)
|
self.cookies = Cookies(cookies)
|
||||||
|
|
||||||
@@ -121,16 +125,14 @@ class Request:
|
|||||||
files_ = files.items() if isinstance(files, dict) else files
|
files_ = files.items() if isinstance(files, dict) else files
|
||||||
for name, file_info in files_:
|
for name, file_info in files_:
|
||||||
if not isinstance(file_info, tuple):
|
if not isinstance(file_info, tuple):
|
||||||
self.files.append((name, (None, file_info, None)))
|
self.files.append((name, (name, file_info, None)))
|
||||||
elif len(file_info) == 2:
|
elif len(file_info) == 2:
|
||||||
self.files.append((name, (file_info[0], file_info[1], None)))
|
self.files.append((name, (file_info[0], file_info[1], None)))
|
||||||
else:
|
else:
|
||||||
self.files.append((name, file_info)) # type: ignore
|
self.files.append((name, file_info)) # type: ignore
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
class_name = self.__class__.__name__
|
return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')"
|
||||||
url = str(self.url)
|
|
||||||
return f"<{class_name}({self.method!r}, {url!r})>"
|
|
||||||
|
|
||||||
|
|
||||||
class Response:
|
class Response:
|
||||||
@@ -146,31 +148,30 @@ class Response:
|
|||||||
self.status_code: int = status_code
|
self.status_code: int = status_code
|
||||||
|
|
||||||
# headers
|
# headers
|
||||||
self.headers: CIMultiDict[str]
|
self.headers: CIMultiDict[str] = (
|
||||||
if headers is not None:
|
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||||
self.headers = CIMultiDict(headers)
|
)
|
||||||
else:
|
|
||||||
self.headers = CIMultiDict()
|
|
||||||
|
|
||||||
# body
|
# body
|
||||||
self.content: ContentTypes = content
|
self.content: ContentTypes = content
|
||||||
|
|
||||||
# request
|
# request
|
||||||
self.request: Optional[Request] = request
|
self.request: Optional[Request] = request
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(abc.ABC):
|
class WebSocket(abc.ABC):
|
||||||
def __init__(self, *, request: Request):
|
def __init__(self, *, request: Request):
|
||||||
# request
|
|
||||||
self.request: Request = request
|
self.request: Request = request
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}('{self.request.url!s}')"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def closed(self) -> bool:
|
def closed(self) -> bool:
|
||||||
"""
|
"""连接是否已经关闭"""
|
||||||
:类型: ``bool``
|
|
||||||
:说明: 连接是否已经关闭
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -184,7 +185,12 @@ class WebSocket(abc.ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def receive(self) -> str:
|
async def receive(self) -> Union[str, bytes]:
|
||||||
|
"""接收一条 WebSocket text/bytes 信息"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def receive_text(self) -> str:
|
||||||
"""接收一条 WebSocket text 信息"""
|
"""接收一条 WebSocket text 信息"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -193,8 +199,17 @@ class WebSocket(abc.ABC):
|
|||||||
"""接收一条 WebSocket binary 信息"""
|
"""接收一条 WebSocket binary 信息"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def send(self, data: Union[str, bytes]) -> None:
|
||||||
|
"""发送一条 WebSocket text/bytes 信息"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
await self.send_text(data)
|
||||||
|
elif isinstance(data, bytes):
|
||||||
|
await self.send_bytes(data)
|
||||||
|
else:
|
||||||
|
raise TypeError("WebSocker send method expects str or bytes!")
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def send(self, data: str) -> None:
|
async def send_text(self, data: str) -> None:
|
||||||
"""发送一条 WebSocket text 信息"""
|
"""发送一条 WebSocket text 信息"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -246,8 +261,8 @@ class Cookies(MutableMapping):
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
default: Optional[str] = None,
|
default: Optional[str] = None,
|
||||||
domain: str = None,
|
domain: Optional[str] = None,
|
||||||
path: str = None,
|
path: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
value: Optional[str] = None
|
value: Optional[str] = None
|
||||||
for cookie in self.jar:
|
for cookie in self.jar:
|
||||||
@@ -288,6 +303,11 @@ class Cookies(MutableMapping):
|
|||||||
for cookie in cookies.jar:
|
for cookie in cookies.jar:
|
||||||
self.jar.set_cookie(cookie)
|
self.jar.set_cookie(cookie)
|
||||||
|
|
||||||
|
def as_header(self, request: Request) -> Dict[str, str]:
|
||||||
|
urllib_request = self._CookieCompatRequest(request)
|
||||||
|
self.jar.add_cookie_header(urllib_request)
|
||||||
|
return urllib_request.added_headers
|
||||||
|
|
||||||
def __setitem__(self, name: str, value: str) -> None:
|
def __setitem__(self, name: str, value: str) -> None:
|
||||||
return self.set(name, value)
|
return self.set(name, value)
|
||||||
|
|
||||||
@@ -304,14 +324,44 @@ class Cookies(MutableMapping):
|
|||||||
return len(self.jar)
|
return len(self.jar)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Cookie]:
|
def __iter__(self) -> Iterator[Cookie]:
|
||||||
return (cookie for cookie in self.jar)
|
return iter(self.jar)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
cookies_repr = ", ".join(
|
cookies_repr = ", ".join(
|
||||||
[
|
f"Cookie({cookie.name}={cookie.value} for {cookie.domain})"
|
||||||
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
|
for cookie in self.jar
|
||||||
for cookie in self.jar
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
return f"{self.__class__.__name__}({cookies_repr})"
|
||||||
|
|
||||||
return f"<Cookies [{cookies_repr}]>"
|
class _CookieCompatRequest(urllib.request.Request):
|
||||||
|
def __init__(self, request: Request) -> None:
|
||||||
|
super().__init__(
|
||||||
|
url=str(request.url),
|
||||||
|
headers=dict(request.headers),
|
||||||
|
method=request.method,
|
||||||
|
)
|
||||||
|
self.request = request
|
||||||
|
self.added_headers: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def add_unredirected_header(self, key: str, value: str) -> None:
|
||||||
|
super().add_unredirected_header(key, value)
|
||||||
|
self.added_headers[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HTTPServerSetup:
|
||||||
|
"""HTTP 服务器路由配置。"""
|
||||||
|
|
||||||
|
path: URL # path should not be absolute, check it by URL.is_absolute() == False
|
||||||
|
method: str
|
||||||
|
name: str
|
||||||
|
handle_func: Callable[[Request], Awaitable[Response]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WebSocketServerSetup:
|
||||||
|
"""WebSocket 服务器路由配置。"""
|
||||||
|
|
||||||
|
path: URL # path should not be absolute, check it by URL.is_absolute() == False
|
||||||
|
name: str
|
||||||
|
handle_func: Callable[[WebSocket], Awaitable[Any]]
|
||||||
12
nonebot/internal/matcher/__init__.py
Normal file
12
nonebot/internal/matcher/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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 MatcherSource as MatcherSource
|
||||||
|
from .matcher import current_event as current_event
|
||||||
|
from .matcher import current_handler as current_handler
|
||||||
|
from .matcher import current_matcher as current_matcher
|
||||||
103
nonebot/internal/matcher/manager.py
Normal file
103
nonebot/internal/matcher/manager.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
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: object) -> 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)
|
||||||
890
nonebot/internal/matcher/matcher.py
Normal file
890
nonebot/internal/matcher/matcher.py
Normal file
@@ -0,0 +1,890 @@
|
|||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from typing_extensions import Self
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from contextlib import AsyncExitStack, contextmanager
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
List,
|
||||||
|
Type,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
TypeVar,
|
||||||
|
Callable,
|
||||||
|
ClassVar,
|
||||||
|
Iterable,
|
||||||
|
NoReturn,
|
||||||
|
Optional,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.internal.rule import Rule
|
||||||
|
from nonebot.utils import classproperty
|
||||||
|
from nonebot.dependencies import Param, Dependent
|
||||||
|
from nonebot.internal.permission import User, Permission
|
||||||
|
from nonebot.internal.adapter import (
|
||||||
|
Bot,
|
||||||
|
Event,
|
||||||
|
Message,
|
||||||
|
MessageSegment,
|
||||||
|
MessageTemplate,
|
||||||
|
)
|
||||||
|
from nonebot.typing import (
|
||||||
|
T_State,
|
||||||
|
T_Handler,
|
||||||
|
T_TypeUpdater,
|
||||||
|
T_DependencyCache,
|
||||||
|
T_PermissionUpdater,
|
||||||
|
)
|
||||||
|
from nonebot.consts import (
|
||||||
|
ARG_KEY,
|
||||||
|
RECEIVE_KEY,
|
||||||
|
REJECT_TARGET,
|
||||||
|
LAST_RECEIVE_KEY,
|
||||||
|
REJECT_CACHE_TARGET,
|
||||||
|
)
|
||||||
|
from nonebot.exception import (
|
||||||
|
PausedException,
|
||||||
|
StopPropagation,
|
||||||
|
SkippedException,
|
||||||
|
FinishedException,
|
||||||
|
RejectedException,
|
||||||
|
)
|
||||||
|
from nonebot.internal.params import (
|
||||||
|
Depends,
|
||||||
|
ArgParam,
|
||||||
|
BotParam,
|
||||||
|
EventParam,
|
||||||
|
StateParam,
|
||||||
|
DependParam,
|
||||||
|
DefaultParam,
|
||||||
|
MatcherParam,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import matchers
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from nonebot.plugin import Plugin
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
||||||
|
current_event: ContextVar[Event] = ContextVar("current_event")
|
||||||
|
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||||
|
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MatcherSource:
|
||||||
|
"""Matcher 源代码上下文信息"""
|
||||||
|
|
||||||
|
plugin_name: Optional[str] = None
|
||||||
|
"""事件响应器所在插件名称"""
|
||||||
|
module_name: Optional[str] = None
|
||||||
|
"""事件响应器所在插件模块的路径名"""
|
||||||
|
lineno: Optional[int] = None
|
||||||
|
"""事件响应器所在行号"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self) -> Optional["Plugin"]:
|
||||||
|
"""事件响应器所在插件"""
|
||||||
|
from nonebot.plugin import get_plugin
|
||||||
|
|
||||||
|
if self.plugin_name is not None:
|
||||||
|
return get_plugin(self.plugin_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def module(self) -> Optional[ModuleType]:
|
||||||
|
if self.module_name is not None:
|
||||||
|
return sys.modules.get(self.module_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file(self) -> Optional[Path]:
|
||||||
|
if self.module is not None and (file := inspect.getsourcefile(self.module)):
|
||||||
|
return Path(file).absolute()
|
||||||
|
|
||||||
|
|
||||||
|
class MatcherMeta(type):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
type: str
|
||||||
|
_source: Optional[MatcherSource]
|
||||||
|
module_name: Optional[str]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.__name__}(type={self.type!r}"
|
||||||
|
+ (f", module={self.module_name}" if self.module_name else "")
|
||||||
|
+ (
|
||||||
|
f", lineno={self._source.lineno}"
|
||||||
|
if self._source and self._source.lineno is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Matcher(metaclass=MatcherMeta):
|
||||||
|
"""事件响应器类"""
|
||||||
|
|
||||||
|
_source: ClassVar[Optional[MatcherSource]] = None
|
||||||
|
|
||||||
|
type: ClassVar[str] = ""
|
||||||
|
"""事件响应器类型"""
|
||||||
|
rule: ClassVar[Rule] = Rule()
|
||||||
|
"""事件响应器匹配规则"""
|
||||||
|
permission: ClassVar[Permission] = Permission()
|
||||||
|
"""事件响应器触发权限"""
|
||||||
|
handlers: List[Dependent[Any]] = []
|
||||||
|
"""事件响应器拥有的事件处理函数列表"""
|
||||||
|
priority: ClassVar[int] = 1
|
||||||
|
"""事件响应器优先级"""
|
||||||
|
block: bool = False
|
||||||
|
"""事件响应器是否阻止事件传播"""
|
||||||
|
temp: ClassVar[bool] = False
|
||||||
|
"""事件响应器是否为临时"""
|
||||||
|
expire_time: ClassVar[Optional[datetime]] = None
|
||||||
|
"""事件响应器过期时间点"""
|
||||||
|
|
||||||
|
_default_state: ClassVar[T_State] = {}
|
||||||
|
"""事件响应器默认状态"""
|
||||||
|
|
||||||
|
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
|
||||||
|
"""事件响应器类型更新函数"""
|
||||||
|
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||||
|
"""事件响应器权限更新函数"""
|
||||||
|
|
||||||
|
HANDLER_PARAM_TYPES: ClassVar[Tuple[Type[Param], ...]] = (
|
||||||
|
DependParam,
|
||||||
|
BotParam,
|
||||||
|
EventParam,
|
||||||
|
StateParam,
|
||||||
|
ArgParam,
|
||||||
|
MatcherParam,
|
||||||
|
DefaultParam,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handlers = self.handlers.copy()
|
||||||
|
self.state = self._default_state.copy()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.__class__.__name__}(type={self.type!r}"
|
||||||
|
+ (f", module={self.module_name}" if self.module_name else "")
|
||||||
|
+ (
|
||||||
|
f", lineno={self._source.lineno}"
|
||||||
|
if self._source and self._source.lineno is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(
|
||||||
|
cls,
|
||||||
|
type_: str = "",
|
||||||
|
rule: Optional[Rule] = None,
|
||||||
|
permission: Optional[Permission] = None,
|
||||||
|
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
|
||||||
|
temp: bool = False,
|
||||||
|
priority: int = 1,
|
||||||
|
block: bool = False,
|
||||||
|
*,
|
||||||
|
plugin: Optional["Plugin"] = None,
|
||||||
|
module: Optional[ModuleType] = None,
|
||||||
|
source: Optional[MatcherSource] = None,
|
||||||
|
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||||
|
default_state: Optional[T_State] = None,
|
||||||
|
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
||||||
|
default_permission_updater: Optional[
|
||||||
|
Union[T_PermissionUpdater, Dependent[Permission]]
|
||||||
|
] = None,
|
||||||
|
) -> Type[Self]:
|
||||||
|
"""
|
||||||
|
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||||
|
|
||||||
|
参数:
|
||||||
|
type_: 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意
|
||||||
|
rule: 匹配规则
|
||||||
|
permission: 权限
|
||||||
|
handlers: 事件处理函数列表
|
||||||
|
temp: 是否为临时事件响应器,即触发一次后删除
|
||||||
|
priority: 响应优先级
|
||||||
|
block: 是否阻止事件向更低优先级的响应器传播
|
||||||
|
plugin: **Deprecated.** 事件响应器所在插件
|
||||||
|
module: **Deprecated.** 事件响应器所在模块
|
||||||
|
source: 事件响应器源代码上下文信息
|
||||||
|
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||||
|
default_state: 默认状态 `state`
|
||||||
|
default_type_updater: 默认事件类型更新函数
|
||||||
|
default_permission_updater: 默认会话权限更新函数
|
||||||
|
|
||||||
|
返回:
|
||||||
|
Type[Matcher]: 新的事件响应器类
|
||||||
|
"""
|
||||||
|
if plugin is not None:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Pass `plugin` context info to create Matcher is deprecated. "
|
||||||
|
"Use `source` instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
if module is not None:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Pass `module` context info to create Matcher is deprecated. "
|
||||||
|
"Use `source` instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
source = source or (
|
||||||
|
MatcherSource(
|
||||||
|
plugin_name=plugin and plugin.name,
|
||||||
|
module_name=module and module.__name__,
|
||||||
|
)
|
||||||
|
if plugin is not None or module is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
NewMatcher = type(
|
||||||
|
cls.__name__,
|
||||||
|
(cls,),
|
||||||
|
{
|
||||||
|
"_source": source,
|
||||||
|
"type": type_,
|
||||||
|
"rule": rule or Rule(),
|
||||||
|
"permission": permission or Permission(),
|
||||||
|
"handlers": [
|
||||||
|
handler
|
||||||
|
if isinstance(handler, Dependent)
|
||||||
|
else Dependent[Any].parse(
|
||||||
|
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
|
||||||
|
)
|
||||||
|
for handler in handlers
|
||||||
|
]
|
||||||
|
if handlers
|
||||||
|
else [],
|
||||||
|
"temp": temp,
|
||||||
|
"expire_time": (
|
||||||
|
expire_time
|
||||||
|
and (
|
||||||
|
expire_time
|
||||||
|
if isinstance(expire_time, datetime)
|
||||||
|
else datetime.now() + expire_time
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"priority": priority,
|
||||||
|
"block": block,
|
||||||
|
"_default_state": default_state or {},
|
||||||
|
"_default_type_updater": (
|
||||||
|
default_type_updater
|
||||||
|
and (
|
||||||
|
default_type_updater
|
||||||
|
if isinstance(default_type_updater, Dependent)
|
||||||
|
else Dependent[str].parse(
|
||||||
|
call=default_type_updater,
|
||||||
|
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"_default_permission_updater": (
|
||||||
|
default_permission_updater
|
||||||
|
and (
|
||||||
|
default_permission_updater
|
||||||
|
if isinstance(default_permission_updater, Dependent)
|
||||||
|
else Dependent[Permission].parse(
|
||||||
|
call=default_permission_updater,
|
||||||
|
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.trace(f"Define new matcher {NewMatcher}")
|
||||||
|
|
||||||
|
matchers[priority].append(NewMatcher)
|
||||||
|
|
||||||
|
return NewMatcher
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy(cls) -> None:
|
||||||
|
"""销毁当前的事件响应器"""
|
||||||
|
matchers[cls.priority].remove(cls)
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def plugin(cls) -> Optional["Plugin"]:
|
||||||
|
"""事件响应器所在插件"""
|
||||||
|
return cls._source and cls._source.plugin
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module(cls) -> Optional[ModuleType]:
|
||||||
|
"""事件响应器所在插件模块"""
|
||||||
|
return cls._source and cls._source.module
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def plugin_name(cls) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件名"""
|
||||||
|
return cls._source and cls._source.plugin_name
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def module_name(cls) -> Optional[str]:
|
||||||
|
"""事件响应器所在插件模块路径"""
|
||||||
|
return cls._source and cls._source.module_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def check_perm(
|
||||||
|
cls,
|
||||||
|
bot: Bot,
|
||||||
|
event: Event,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""检查是否满足触发权限
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: Bot 对象
|
||||||
|
event: 上报事件
|
||||||
|
stack: 异步上下文栈
|
||||||
|
dependency_cache: 依赖缓存
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否满足权限
|
||||||
|
"""
|
||||||
|
event_type = event.get_type()
|
||||||
|
return event_type == (cls.type or event_type) and await cls.permission(
|
||||||
|
bot, event, stack, dependency_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def check_rule(
|
||||||
|
cls,
|
||||||
|
bot: Bot,
|
||||||
|
event: Event,
|
||||||
|
state: T_State,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""检查是否满足匹配规则
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: Bot 对象
|
||||||
|
event: 上报事件
|
||||||
|
state: 当前状态
|
||||||
|
stack: 异步上下文栈
|
||||||
|
dependency_cache: 依赖缓存
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否满足匹配规则
|
||||||
|
"""
|
||||||
|
event_type = event.get_type()
|
||||||
|
return event_type == (cls.type or event_type) and await cls.rule(
|
||||||
|
bot, event, state, stack, dependency_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
|
||||||
|
"""装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
func: 响应事件类型更新函数
|
||||||
|
"""
|
||||||
|
cls._default_type_updater = Dependent[str].parse(
|
||||||
|
call=func, allow_types=cls.HANDLER_PARAM_TYPES
|
||||||
|
)
|
||||||
|
return func
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
|
||||||
|
"""装饰一个函数来更改当前事件响应器的默认会话权限更新函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
func: 会话权限更新函数
|
||||||
|
"""
|
||||||
|
cls._default_permission_updater = Dependent[Permission].parse(
|
||||||
|
call=func, allow_types=cls.HANDLER_PARAM_TYPES
|
||||||
|
)
|
||||||
|
return func
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def append_handler(
|
||||||
|
cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
|
||||||
|
) -> Dependent[Any]:
|
||||||
|
handler_ = Dependent[Any].parse(
|
||||||
|
call=handler,
|
||||||
|
parameterless=parameterless,
|
||||||
|
allow_types=cls.HANDLER_PARAM_TYPES,
|
||||||
|
)
|
||||||
|
cls.handlers.append(handler_)
|
||||||
|
return handler_
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle(
|
||||||
|
cls, parameterless: Optional[Iterable[Any]] = None
|
||||||
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
|
"""装饰一个函数来向事件响应器直接添加一个处理函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
parameterless: 非参数类型依赖列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _decorator(func: T_Handler) -> T_Handler:
|
||||||
|
cls.append_handler(func, parameterless=parameterless)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def receive(
|
||||||
|
cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
|
||||||
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
|
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
id: 消息 ID
|
||||||
|
parameterless: 非参数类型依赖列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
|
||||||
|
matcher.set_target(RECEIVE_KEY.format(id=id))
|
||||||
|
if matcher.get_target() == RECEIVE_KEY.format(id=id):
|
||||||
|
matcher.set_receive(id, event)
|
||||||
|
return
|
||||||
|
if matcher.get_receive(id, ...) is not ...:
|
||||||
|
return
|
||||||
|
await matcher.reject()
|
||||||
|
|
||||||
|
_parameterless = (Depends(_receive), *(parameterless or ()))
|
||||||
|
|
||||||
|
def _decorator(func: T_Handler) -> T_Handler:
|
||||||
|
if cls.handlers and cls.handlers[-1].call is func:
|
||||||
|
func_handler = cls.handlers[-1]
|
||||||
|
new_handler = Dependent(
|
||||||
|
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)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def got(
|
||||||
|
cls,
|
||||||
|
key: str,
|
||||||
|
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
parameterless: Optional[Iterable[Any]] = None,
|
||||||
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
|
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
||||||
|
|
||||||
|
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数,
|
||||||
|
如果 `key` 已存在则直接继续运行
|
||||||
|
|
||||||
|
参数:
|
||||||
|
key: 参数名
|
||||||
|
prompt: 在参数不存在时向用户发送的消息
|
||||||
|
parameterless: 非参数类型依赖列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _key_getter(event: Event, matcher: "Matcher"):
|
||||||
|
matcher.set_target(ARG_KEY.format(key=key))
|
||||||
|
if matcher.get_target() == ARG_KEY.format(key=key):
|
||||||
|
matcher.set_arg(key, event.get_message())
|
||||||
|
return
|
||||||
|
if matcher.get_arg(key, ...) is not ...:
|
||||||
|
return
|
||||||
|
await matcher.reject(prompt)
|
||||||
|
|
||||||
|
_parameterless = (Depends(_key_getter), *(parameterless or ()))
|
||||||
|
|
||||||
|
def _decorator(func: T_Handler) -> T_Handler:
|
||||||
|
if cls.handlers and cls.handlers[-1].call is func:
|
||||||
|
func_handler = cls.handlers[-1]
|
||||||
|
new_handler = Dependent(
|
||||||
|
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)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def send(
|
||||||
|
cls,
|
||||||
|
message: Union[str, Message, MessageSegment, MessageTemplate],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
"""发送一条消息给当前交互用户
|
||||||
|
|
||||||
|
参数:
|
||||||
|
message: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
bot = current_bot.get()
|
||||||
|
event = current_event.get()
|
||||||
|
state = current_matcher.get().state
|
||||||
|
if isinstance(message, MessageTemplate):
|
||||||
|
_message = message.format(**state)
|
||||||
|
else:
|
||||||
|
_message = message
|
||||||
|
return await bot.send(event=event, message=_message, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def finish(
|
||||||
|
cls,
|
||||||
|
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""发送一条消息给当前交互用户并结束当前事件响应器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
message: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
if message is not None:
|
||||||
|
await cls.send(message, **kwargs)
|
||||||
|
raise FinishedException
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def pause(
|
||||||
|
cls,
|
||||||
|
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
prompt: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
if prompt is not None:
|
||||||
|
await cls.send(prompt, **kwargs)
|
||||||
|
raise PausedException
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def reject(
|
||||||
|
cls,
|
||||||
|
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""最近使用 `got` / `receive` 接收的消息不符合预期,
|
||||||
|
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
prompt: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
if prompt is not None:
|
||||||
|
await cls.send(prompt, **kwargs)
|
||||||
|
raise RejectedException
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def reject_arg(
|
||||||
|
cls,
|
||||||
|
key: str,
|
||||||
|
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""最近使用 `got` 接收的消息不符合预期,
|
||||||
|
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
key: 参数名
|
||||||
|
prompt: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
matcher = current_matcher.get()
|
||||||
|
matcher.set_target(ARG_KEY.format(key=key))
|
||||||
|
if prompt is not None:
|
||||||
|
await cls.send(prompt, **kwargs)
|
||||||
|
raise RejectedException
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def reject_receive(
|
||||||
|
cls,
|
||||||
|
id: str = "",
|
||||||
|
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> NoReturn:
|
||||||
|
"""最近使用 `receive` 接收的消息不符合预期,
|
||||||
|
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
id: 消息 id
|
||||||
|
prompt: 消息内容
|
||||||
|
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数,
|
||||||
|
请参考对应 adapter 的 bot 对象 api
|
||||||
|
"""
|
||||||
|
matcher = current_matcher.get()
|
||||||
|
matcher.set_target(RECEIVE_KEY.format(id=id))
|
||||||
|
if prompt is not None:
|
||||||
|
await cls.send(prompt, **kwargs)
|
||||||
|
raise RejectedException
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def skip(cls) -> NoReturn:
|
||||||
|
"""跳过当前事件处理函数,继续下一个处理函数
|
||||||
|
|
||||||
|
通常在事件处理函数的依赖中使用。
|
||||||
|
"""
|
||||||
|
raise SkippedException
|
||||||
|
|
||||||
|
@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` 值
|
||||||
|
"""
|
||||||
|
return self.state.get(RECEIVE_KEY.format(id=id), default)
|
||||||
|
|
||||||
|
def set_receive(self, id: str, event: Event) -> None:
|
||||||
|
"""设置一个 `receive` 事件"""
|
||||||
|
self.state[RECEIVE_KEY.format(id=id)] = event
|
||||||
|
self.state[LAST_RECEIVE_KEY] = event
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
@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` 值
|
||||||
|
"""
|
||||||
|
return self.state.get(ARG_KEY.format(key=key), default)
|
||||||
|
|
||||||
|
def set_arg(self, key: str, message: Message) -> None:
|
||||||
|
"""设置一个 `got` 消息"""
|
||||||
|
self.state[ARG_KEY.format(key=key)] = message
|
||||||
|
|
||||||
|
def set_target(self, target: str, cache: bool = True) -> None:
|
||||||
|
if cache:
|
||||||
|
self.state[REJECT_CACHE_TARGET] = target
|
||||||
|
else:
|
||||||
|
self.state[REJECT_TARGET] = target
|
||||||
|
|
||||||
|
@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,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
) -> str:
|
||||||
|
updater = self.__class__._default_type_updater
|
||||||
|
return (
|
||||||
|
await updater(
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
state=self.state,
|
||||||
|
matcher=self,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
)
|
||||||
|
if updater
|
||||||
|
else "message"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.handlers.insert(0, handler)
|
||||||
|
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,
|
||||||
|
event: Event,
|
||||||
|
state: T_State,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
):
|
||||||
|
logger.trace(
|
||||||
|
f"{self} run with incoming args: "
|
||||||
|
f"bot={bot}, event={event!r}, state={state!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
self,
|
||||||
|
bot: Bot,
|
||||||
|
event: Event,
|
||||||
|
state: T_State,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await self.simple_run(bot, event, state, stack, dependency_cache)
|
||||||
|
|
||||||
|
except RejectedException:
|
||||||
|
await self.resolve_reject()
|
||||||
|
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||||
|
permission = await self.update_permission(
|
||||||
|
bot, event, stack, dependency_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
self.new(
|
||||||
|
type_,
|
||||||
|
Rule(),
|
||||||
|
permission,
|
||||||
|
self.handlers,
|
||||||
|
temp=True,
|
||||||
|
priority=0,
|
||||||
|
block=True,
|
||||||
|
source=self.__class__._source,
|
||||||
|
expire_time=bot.config.session_expire_timeout,
|
||||||
|
default_state=self.state,
|
||||||
|
default_type_updater=self.__class__._default_type_updater,
|
||||||
|
default_permission_updater=self.__class__._default_permission_updater,
|
||||||
|
)
|
||||||
|
except PausedException:
|
||||||
|
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||||
|
permission = await self.update_permission(
|
||||||
|
bot, event, stack, dependency_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
self.new(
|
||||||
|
type_,
|
||||||
|
Rule(),
|
||||||
|
permission,
|
||||||
|
self.handlers,
|
||||||
|
temp=True,
|
||||||
|
priority=0,
|
||||||
|
block=True,
|
||||||
|
source=self.__class__._source,
|
||||||
|
expire_time=bot.config.session_expire_timeout,
|
||||||
|
default_state=self.state,
|
||||||
|
default_type_updater=self.__class__._default_type_updater,
|
||||||
|
default_permission_updater=self.__class__._default_permission_updater,
|
||||||
|
)
|
||||||
|
except FinishedException:
|
||||||
|
pass
|
||||||
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
|
||||||
|
"""默认存储器类型"""
|
||||||
538
nonebot/internal/params.py
Normal file
538
nonebot/internal/params.py
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
from typing_extensions import Self, Annotated, override
|
||||||
|
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Type,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
Literal,
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pydantic.typing import get_args, get_origin
|
||||||
|
from pydantic.fields import Required, FieldInfo, Undefined, ModelField
|
||||||
|
|
||||||
|
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
|
||||||
|
from nonebot.utils import (
|
||||||
|
get_name,
|
||||||
|
run_sync,
|
||||||
|
is_gen_callable,
|
||||||
|
run_sync_ctx_manager,
|
||||||
|
is_async_gen_callable,
|
||||||
|
is_coroutine_callable,
|
||||||
|
generic_check_issubclass,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from nonebot.matcher import Matcher
|
||||||
|
from nonebot.adapters import Bot, Event
|
||||||
|
|
||||||
|
EXTRA_FIELD_INFO = (
|
||||||
|
"gt",
|
||||||
|
"lt",
|
||||||
|
"ge",
|
||||||
|
"le",
|
||||||
|
"multiple_of",
|
||||||
|
"allow_inf_nan",
|
||||||
|
"max_digits",
|
||||||
|
"decimal_places",
|
||||||
|
"min_items",
|
||||||
|
"max_items",
|
||||||
|
"unique_items",
|
||||||
|
"min_length",
|
||||||
|
"max_length",
|
||||||
|
"regex",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DependsInner:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dependency: Optional[T_Handler] = None,
|
||||||
|
*,
|
||||||
|
use_cache: bool = True,
|
||||||
|
validate: Union[bool, FieldInfo] = False,
|
||||||
|
) -> None:
|
||||||
|
self.dependency = dependency
|
||||||
|
self.use_cache = use_cache
|
||||||
|
self.validate = validate
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
dep = get_name(self.dependency)
|
||||||
|
cache = "" if self.use_cache else ", use_cache=False"
|
||||||
|
validate = f", validate={self.validate}" if self.validate else ""
|
||||||
|
return f"DependsInner({dep}{cache}{validate})"
|
||||||
|
|
||||||
|
|
||||||
|
def Depends(
|
||||||
|
dependency: Optional[T_Handler] = None,
|
||||||
|
*,
|
||||||
|
use_cache: bool = True,
|
||||||
|
validate: Union[bool, FieldInfo] = False,
|
||||||
|
) -> Any:
|
||||||
|
"""子依赖装饰器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
dependency: 依赖函数。默认为参数的类型注释。
|
||||||
|
use_cache: 是否使用缓存。默认为 `True`。
|
||||||
|
validate: 是否使用 Pydantic 类型校验。默认为 `False`。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
|
def depend_func() -> Any:
|
||||||
|
return ...
|
||||||
|
|
||||||
|
def depend_gen_func():
|
||||||
|
try:
|
||||||
|
yield ...
|
||||||
|
finally:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def handler(
|
||||||
|
param_name: Any = Depends(depend_func),
|
||||||
|
gen: Any = Depends(depend_gen_func),
|
||||||
|
):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
||||||
|
|
||||||
|
|
||||||
|
class DependParam(Param):
|
||||||
|
"""子依赖注入参数。
|
||||||
|
|
||||||
|
本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。
|
||||||
|
|
||||||
|
本注入应该具有最高优先级,因此应该在其他参数之前检查。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Depends({self.extra['dependent']})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_field(
|
||||||
|
cls, sub_dependent: Dependent, use_cache: bool, validate: Union[bool, FieldInfo]
|
||||||
|
) -> Self:
|
||||||
|
kwargs = {}
|
||||||
|
if isinstance(validate, FieldInfo):
|
||||||
|
kwargs.update((k, getattr(validate, k)) for k in EXTRA_FIELD_INFO)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
Required,
|
||||||
|
validate=bool(validate),
|
||||||
|
**kwargs,
|
||||||
|
dependent=sub_dependent,
|
||||||
|
use_cache=use_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
type_annotation, depends_inner = param.annotation, None
|
||||||
|
# extract type annotation and dependency from Annotated
|
||||||
|
if get_origin(param.annotation) is Annotated:
|
||||||
|
type_annotation, *extra_args = get_args(param.annotation)
|
||||||
|
depends_inner = next(
|
||||||
|
(x for x in reversed(extra_args) if isinstance(x, DependsInner)), None
|
||||||
|
)
|
||||||
|
|
||||||
|
# param default value takes higher priority
|
||||||
|
depends_inner = (
|
||||||
|
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||||
|
)
|
||||||
|
# not a dependent
|
||||||
|
if depends_inner is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
dependency: T_Handler
|
||||||
|
# sub dependency is not specified, use type annotation
|
||||||
|
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
|
||||||
|
# parse sub dependency
|
||||||
|
sub_dependent = Dependent[Any].parse(
|
||||||
|
call=dependency,
|
||||||
|
allow_types=allow_types,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._from_field(
|
||||||
|
sub_dependent, depends_inner.use_cache, depends_inner.validate
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_parameterless(
|
||||||
|
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=allow_types
|
||||||
|
)
|
||||||
|
return cls._from_field(dependent, value.use_cache, value.validate)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(
|
||||||
|
self,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Any:
|
||||||
|
use_cache: bool = self.extra["use_cache"]
|
||||||
|
dependency_cache = {} if dependency_cache is None else dependency_cache
|
||||||
|
|
||||||
|
sub_dependent: Dependent = self.extra["dependent"]
|
||||||
|
call = cast(Callable[..., Any], sub_dependent.call)
|
||||||
|
|
||||||
|
# solve sub dependency with current cache
|
||||||
|
sub_values = await sub_dependent.solve(
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# run dependency function
|
||||||
|
task: asyncio.Task[Any]
|
||||||
|
if use_cache and call in dependency_cache:
|
||||||
|
return await dependency_cache[call]
|
||||||
|
elif is_gen_callable(call) or is_async_gen_callable(call):
|
||||||
|
assert isinstance(
|
||||||
|
stack, AsyncExitStack
|
||||||
|
), "Generator dependency should be called in context"
|
||||||
|
if is_gen_callable(call):
|
||||||
|
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
|
||||||
|
else:
|
||||||
|
cm = asynccontextmanager(call)(**sub_values)
|
||||||
|
task = asyncio.create_task(stack.enter_async_context(cm))
|
||||||
|
dependency_cache[call] = task
|
||||||
|
return await task
|
||||||
|
elif is_coroutine_callable(call):
|
||||||
|
task = asyncio.create_task(call(**sub_values))
|
||||||
|
dependency_cache[call] = task
|
||||||
|
return await task
|
||||||
|
else:
|
||||||
|
task = asyncio.create_task(run_sync(call)(**sub_values))
|
||||||
|
dependency_cache[call] = task
|
||||||
|
return await task
|
||||||
|
|
||||||
|
@override
|
||||||
|
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` 及其子类或 `None` 的参数。
|
||||||
|
|
||||||
|
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
"BotParam("
|
||||||
|
+ (
|
||||||
|
repr(cast(ModelField, checker).type_)
|
||||||
|
if (checker := self.extra.get("checker"))
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
from nonebot.adapters import Bot
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||||
|
return bot
|
||||||
|
|
||||||
|
@override
|
||||||
|
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` 及其子类或 `None` 的参数。
|
||||||
|
|
||||||
|
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
"EventParam("
|
||||||
|
+ (
|
||||||
|
repr(cast(ModelField, checker).type_)
|
||||||
|
if (checker := self.extra.get("checker"))
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||||
|
return event
|
||||||
|
|
||||||
|
@override
|
||||||
|
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
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
@override
|
||||||
|
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
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
from nonebot.matcher import Matcher
|
||||||
|
|
||||||
|
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||||
|
if generic_check_issubclass(param.annotation, Matcher):
|
||||||
|
checker: Optional[ModelField] = None
|
||||||
|
if param.annotation is not Matcher:
|
||||||
|
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 "matcher" and has no type annotation
|
||||||
|
elif param.annotation == param.empty and param.name == "matcher":
|
||||||
|
return cls(Required)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||||
|
return matcher
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||||
|
if checker := self.extra.get("checker", None):
|
||||||
|
check_field_type(checker, matcher)
|
||||||
|
|
||||||
|
|
||||||
|
class ArgInner:
|
||||||
|
def __init__(
|
||||||
|
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
|
||||||
|
) -> None:
|
||||||
|
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:
|
||||||
|
"""Arg 参数消息"""
|
||||||
|
return ArgInner(key, "message")
|
||||||
|
|
||||||
|
|
||||||
|
def ArgStr(key: Optional[str] = None) -> str:
|
||||||
|
"""Arg 参数消息文本"""
|
||||||
|
return ArgInner(key, "str") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def ArgPlainText(key: Optional[str] = None) -> str:
|
||||||
|
"""Arg 参数消息纯文本"""
|
||||||
|
return ArgInner(key, "plaintext") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class ArgParam(Param):
|
||||||
|
"""Arg 注入参数
|
||||||
|
|
||||||
|
本注入解析事件响应器操作 `got` 所获取的参数。
|
||||||
|
|
||||||
|
可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数,
|
||||||
|
留空则会根据参数名称获取。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ArgParam(key={self.extra['key']!r}, type={self.extra['type']!r})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(param.default, ArgInner):
|
||||||
|
return cls(
|
||||||
|
Required, key=param.default.key or param.name, type=param.default.type
|
||||||
|
)
|
||||||
|
elif get_origin(param.annotation) is Annotated:
|
||||||
|
for arg in get_args(param.annotation)[:0:-1]:
|
||||||
|
if isinstance(arg, ArgInner):
|
||||||
|
return cls(Required, key=arg.key or param.name, type=arg.type)
|
||||||
|
|
||||||
|
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||||
|
key: str = self.extra["key"]
|
||||||
|
message = matcher.get_arg(key)
|
||||||
|
if message is None:
|
||||||
|
return message
|
||||||
|
if self.extra["type"] == "message":
|
||||||
|
return message
|
||||||
|
elif self.extra["type"] == "str":
|
||||||
|
return str(message)
|
||||||
|
else:
|
||||||
|
return message.extract_plain_text()
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionParam(Param):
|
||||||
|
"""{ref}`nonebot.message.run_postprocessor` 的异常注入参数
|
||||||
|
|
||||||
|
本注入解析所有类型为 `Exception` 或 `None` 的参数。
|
||||||
|
|
||||||
|
为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "ExceptionParam()"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
||||||
|
return exception
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultParam(Param):
|
||||||
|
"""默认值注入参数
|
||||||
|
|
||||||
|
本注入解析所有剩余未能解析且具有默认值的参数。
|
||||||
|
|
||||||
|
本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"DefaultParam(default={self.default!r})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
def _check_param(
|
||||||
|
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if param.default != param.empty:
|
||||||
|
return cls(param.default)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _solve(self, **kwargs: Any) -> Any:
|
||||||
|
return Undefined
|
||||||
|
|
||||||
|
|
||||||
|
__autodoc__ = {
|
||||||
|
"DependsInner": False,
|
||||||
|
"StateInner": False,
|
||||||
|
"ArgInner": False,
|
||||||
|
}
|
||||||
187
nonebot/internal/permission.py
Normal file
187
nonebot/internal/permission.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing_extensions import Self
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
from typing import Set, Tuple, Union, NoReturn, Optional
|
||||||
|
|
||||||
|
from nonebot.dependencies import Dependent
|
||||||
|
from nonebot.utils import run_coro_with_catch
|
||||||
|
from nonebot.exception import SkippedException
|
||||||
|
from nonebot.typing import T_DependencyCache, T_PermissionChecker
|
||||||
|
|
||||||
|
from .adapter import Bot, Event
|
||||||
|
from .params import BotParam, EventParam, DependParam, DefaultParam
|
||||||
|
|
||||||
|
|
||||||
|
class Permission:
|
||||||
|
"""{ref}`nonebot.matcher.Matcher` 权限类。
|
||||||
|
|
||||||
|
当事件传递时,在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
checkers: PermissionChecker
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
|
Permission(async_function) | sync_function
|
||||||
|
# 等价于
|
||||||
|
Permission(async_function, sync_function)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("checkers",)
|
||||||
|
|
||||||
|
HANDLER_PARAM_TYPES = [
|
||||||
|
DependParam,
|
||||||
|
BotParam,
|
||||||
|
EventParam,
|
||||||
|
DefaultParam,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
|
||||||
|
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,
|
||||||
|
event: Event,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""检查是否满足某个权限。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: Bot 对象
|
||||||
|
event: Event 对象
|
||||||
|
stack: 异步上下文栈
|
||||||
|
dependency_cache: 依赖缓存
|
||||||
|
"""
|
||||||
|
if not self.checkers:
|
||||||
|
return True
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(
|
||||||
|
run_coro_with_catch(
|
||||||
|
checker(
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
),
|
||||||
|
(SkippedException,),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
for checker in self.checkers
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return any(results)
|
||||||
|
|
||||||
|
def __and__(self, other: object) -> NoReturn:
|
||||||
|
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||||
|
|
||||||
|
def __or__(
|
||||||
|
self, other: Optional[Union["Permission", T_PermissionChecker]]
|
||||||
|
) -> "Permission":
|
||||||
|
if other is None:
|
||||||
|
return self
|
||||||
|
elif isinstance(other, Permission):
|
||||||
|
return Permission(*self.checkers, *other.checkers)
|
||||||
|
else:
|
||||||
|
return Permission(*self.checkers, other)
|
||||||
|
|
||||||
|
def __ror__(
|
||||||
|
self, other: Optional[Union["Permission", T_PermissionChecker]]
|
||||||
|
) -> "Permission":
|
||||||
|
if other is None:
|
||||||
|
return self
|
||||||
|
elif isinstance(other, Permission):
|
||||||
|
return Permission(*other.checkers, *self.checkers)
|
||||||
|
else:
|
||||||
|
return Permission(other, *self.checkers)
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""检查当前事件是否属于指定会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
users: 会话 ID 元组
|
||||||
|
perm: 需同时满足的权限
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("users", "perm")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, users: Tuple[str, ...], perm: Optional[Permission] = None
|
||||||
|
) -> None:
|
||||||
|
self.users = users
|
||||||
|
self.perm = perm
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"User(users={self.users}"
|
||||||
|
+ (f", permission={self.perm})" if self.perm else "")
|
||||||
|
+ ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __call__(self, bot: Bot, event: Event) -> bool:
|
||||||
|
try:
|
||||||
|
session = event.get_session_id()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return bool(
|
||||||
|
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.from_permission(*users, perm=perm))
|
||||||
106
nonebot/internal/rule.py
Normal file
106
nonebot/internal/rule.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import asyncio
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
from typing import Set, Union, NoReturn, Optional
|
||||||
|
|
||||||
|
from nonebot.dependencies import Dependent
|
||||||
|
from nonebot.exception import SkippedException
|
||||||
|
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
|
||||||
|
|
||||||
|
from .adapter import Bot, Event
|
||||||
|
from .params import BotParam, EventParam, StateParam, DependParam, DefaultParam
|
||||||
|
|
||||||
|
|
||||||
|
class Rule:
|
||||||
|
"""{ref}`nonebot.matcher.Matcher` 规则类。
|
||||||
|
|
||||||
|
当事件传递时,在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
*checkers: RuleChecker
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
|
Rule(async_function) & sync_function
|
||||||
|
# 等价于
|
||||||
|
Rule(async_function, sync_function)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("checkers",)
|
||||||
|
|
||||||
|
HANDLER_PARAM_TYPES = [
|
||||||
|
DependParam,
|
||||||
|
BotParam,
|
||||||
|
EventParam,
|
||||||
|
StateParam,
|
||||||
|
DefaultParam,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
|
||||||
|
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,
|
||||||
|
event: Event,
|
||||||
|
state: T_State,
|
||||||
|
stack: Optional[AsyncExitStack] = None,
|
||||||
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""检查是否符合所有规则
|
||||||
|
|
||||||
|
参数:
|
||||||
|
bot: Bot 对象
|
||||||
|
event: Event 对象
|
||||||
|
state: 当前 State
|
||||||
|
stack: 异步上下文栈
|
||||||
|
dependency_cache: 依赖缓存
|
||||||
|
"""
|
||||||
|
if not self.checkers:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(
|
||||||
|
checker(
|
||||||
|
bot=bot,
|
||||||
|
event=event,
|
||||||
|
state=state,
|
||||||
|
stack=stack,
|
||||||
|
dependency_cache=dependency_cache,
|
||||||
|
)
|
||||||
|
for checker in self.checkers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except SkippedException:
|
||||||
|
return False
|
||||||
|
return all(results)
|
||||||
|
|
||||||
|
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||||
|
if other is None:
|
||||||
|
return self
|
||||||
|
elif isinstance(other, Rule):
|
||||||
|
return Rule(*self.checkers, *other.checkers)
|
||||||
|
else:
|
||||||
|
return Rule(*self.checkers, other)
|
||||||
|
|
||||||
|
def __rand__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||||
|
if other is None:
|
||||||
|
return self
|
||||||
|
elif isinstance(other, Rule):
|
||||||
|
return Rule(*other.checkers, *self.checkers)
|
||||||
|
else:
|
||||||
|
return Rule(other, *self.checkers)
|
||||||
|
|
||||||
|
def __or__(self, other: object) -> NoReturn:
|
||||||
|
raise RuntimeError("Or operation between rules is not allowed.")
|
||||||
@@ -1,44 +1,42 @@
|
|||||||
"""
|
"""本模块定义了 NoneBot 的日志记录 Logger。
|
||||||
日志
|
|
||||||
====
|
|
||||||
|
|
||||||
NoneBot 使用 `loguru`_ 来记录日志信息。
|
NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||||
|
|
||||||
自定义 logger 请参考 `loguru`_ 文档。
|
自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)
|
||||||
|
以及 [`loguru`][loguru] 文档。
|
||||||
|
|
||||||
.. _loguru:
|
[loguru]: https://github.com/Delgan/loguru
|
||||||
https://github.com/Delgan/loguru
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 7
|
||||||
|
description: nonebot.log 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# avoid sphinx autodoc resolve annotation failed
|
# avoid sphinx autodoc resolve annotation failed
|
||||||
# because loguru module do not have `Logger` class actually
|
# because loguru module do not have `Logger` class actually
|
||||||
from loguru import Logger
|
from loguru import Logger, Record
|
||||||
|
|
||||||
# logger = logging.getLogger("nonebot")
|
# logger = logging.getLogger("nonebot")
|
||||||
logger: "Logger" = loguru.logger
|
logger: "Logger" = loguru.logger
|
||||||
"""
|
"""NoneBot 日志记录器对象。
|
||||||
:说明:
|
|
||||||
|
|
||||||
NoneBot 日志记录器对象。
|
默认信息:
|
||||||
|
|
||||||
:默认信息:
|
- 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`
|
||||||
|
- 等级: `INFO` ,根据 `config.log_level` 配置改变
|
||||||
* 格式: ``[%(asctime)s %(name)s] %(levelname)s: %(message)s``
|
- 输出: 输出至 stdout
|
||||||
* 等级: ``INFO`` ,根据 ``config.log_level`` 配置改变
|
|
||||||
* 输出: 输出至 stdout
|
|
||||||
|
|
||||||
:用法:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# default_handler = logging.StreamHandler(sys.stdout)
|
# default_handler = logging.StreamHandler(sys.stdout)
|
||||||
@@ -47,30 +45,16 @@ logger: "Logger" = loguru.logger
|
|||||||
# logger.addHandler(default_handler)
|
# logger.addHandler(default_handler)
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.level: Union[int, str] = "INFO"
|
|
||||||
|
|
||||||
def __call__(self, record):
|
|
||||||
module_name: str = record["name"]
|
|
||||||
module = sys.modules.get(module_name)
|
|
||||||
if module:
|
|
||||||
module_name = getattr(module, "__module_name__", module_name)
|
|
||||||
record["name"] = module_name.split(".")[0]
|
|
||||||
levelno = (
|
|
||||||
logger.level(self.level).no if isinstance(self.level, str) else self.level
|
|
||||||
)
|
|
||||||
return record["level"].no >= levelno
|
|
||||||
|
|
||||||
|
|
||||||
class LoguruHandler(logging.Handler): # pragma: no cover
|
class LoguruHandler(logging.Handler): # pragma: no cover
|
||||||
def emit(self, record):
|
"""logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。"""
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord):
|
||||||
try:
|
try:
|
||||||
level = logger.level(record.levelname).name
|
level = logger.level(record.levelname).name
|
||||||
except ValueError:
|
except ValueError:
|
||||||
level = record.levelno
|
level = record.levelno
|
||||||
|
|
||||||
frame, depth = logging.currentframe(), 2
|
frame, depth = sys._getframe(6), 6
|
||||||
while frame and frame.f_code.co_filename == logging.__file__:
|
while frame and frame.f_code.co_filename == logging.__file__:
|
||||||
frame = frame.f_back
|
frame = frame.f_back
|
||||||
depth += 1
|
depth += 1
|
||||||
@@ -80,20 +64,30 @@ class LoguruHandler(logging.Handler): # pragma: no cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
logger.remove()
|
def default_filter(record: "Record"):
|
||||||
default_filter = Filter()
|
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
||||||
default_format = (
|
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> "
|
"<g>{time:MM-DD HH:mm:ss}</g> "
|
||||||
"[<lvl>{level}</lvl>] "
|
"[<lvl>{level}</lvl>] "
|
||||||
"<c><u>{name}</u></c> | "
|
"<c><u>{name}</u></c> | "
|
||||||
# "<c>{function}:{line}</c>| "
|
# "<c>{function}:{line}</c>| "
|
||||||
"{message}"
|
"{message}"
|
||||||
)
|
)
|
||||||
|
"""默认日志格式"""
|
||||||
|
|
||||||
|
logger.remove()
|
||||||
logger_id = logger.add(
|
logger_id = logger.add(
|
||||||
sys.stdout,
|
sys.stdout,
|
||||||
level=0,
|
level=0,
|
||||||
colorize=True,
|
|
||||||
diagnose=False,
|
diagnose=False,
|
||||||
filter=default_filter,
|
filter=default_filter,
|
||||||
format=default_format,
|
format=default_format,
|
||||||
)
|
)
|
||||||
|
"""默认日志处理器 id"""
|
||||||
|
|
||||||
|
__autodoc__ = {"logger_id": False}
|
||||||
|
|||||||
@@ -1,784 +1,25 @@
|
|||||||
"""
|
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
|
||||||
事件响应器
|
|
||||||
==========
|
|
||||||
|
|
||||||
该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话 。
|
FrontMatter:
|
||||||
|
sidebar_position: 3
|
||||||
|
description: nonebot.matcher 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from types import ModuleType
|
from nonebot.internal.matcher import Matcher as Matcher
|
||||||
from datetime import datetime
|
from nonebot.internal.matcher import matchers as matchers
|
||||||
from contextvars import ContextVar
|
from nonebot.internal.matcher import current_bot as current_bot
|
||||||
from collections import defaultdict
|
from nonebot.internal.matcher import MatcherSource as MatcherSource
|
||||||
from contextlib import AsyncExitStack
|
from nonebot.internal.matcher import current_event as current_event
|
||||||
from typing import (
|
from nonebot.internal.matcher import MatcherManager as MatcherManager
|
||||||
TYPE_CHECKING,
|
from nonebot.internal.matcher import MatcherProvider as MatcherProvider
|
||||||
Any,
|
from nonebot.internal.matcher import current_handler as current_handler
|
||||||
Dict,
|
from nonebot.internal.matcher import current_matcher as current_matcher
|
||||||
List,
|
from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS
|
||||||
Type,
|
|
||||||
Union,
|
__autodoc__ = {
|
||||||
TypeVar,
|
"Matcher": True,
|
||||||
Callable,
|
"matchers": True,
|
||||||
NoReturn,
|
"MatcherManager": True,
|
||||||
Optional,
|
"MatcherProvider": True,
|
||||||
)
|
"DEFAULT_PROVIDER_CLASS": True,
|
||||||
|
}
|
||||||
from nonebot import params
|
|
||||||
from nonebot.rule import Rule
|
|
||||||
from nonebot.log import logger
|
|
||||||
from nonebot.dependencies import Dependent
|
|
||||||
from nonebot.permission import USER, Permission
|
|
||||||
from nonebot.adapters import (
|
|
||||||
Bot,
|
|
||||||
Event,
|
|
||||||
Message,
|
|
||||||
MessageSegment,
|
|
||||||
MessageTemplate,
|
|
||||||
)
|
|
||||||
from nonebot.consts import (
|
|
||||||
ARG_KEY,
|
|
||||||
RECEIVE_KEY,
|
|
||||||
REJECT_TARGET,
|
|
||||||
LAST_RECEIVE_KEY,
|
|
||||||
REJECT_CACHE_TARGET,
|
|
||||||
)
|
|
||||||
from nonebot.typing import (
|
|
||||||
Any,
|
|
||||||
T_State,
|
|
||||||
T_Handler,
|
|
||||||
T_TypeUpdater,
|
|
||||||
T_DependencyCache,
|
|
||||||
T_PermissionUpdater,
|
|
||||||
)
|
|
||||||
from nonebot.exception import (
|
|
||||||
TypeMisMatch,
|
|
||||||
PausedException,
|
|
||||||
StopPropagation,
|
|
||||||
SkippedException,
|
|
||||||
FinishedException,
|
|
||||||
RejectedException,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from nonebot.plugin import Plugin
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
|
|
||||||
"""
|
|
||||||
:类型: ``Dict[int, List[Type[Matcher]]]``
|
|
||||||
:说明: 用于存储当前所有的事件响应器
|
|
||||||
"""
|
|
||||||
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
|
||||||
current_event: ContextVar[Event] = ContextVar("current_event")
|
|
||||||
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
|
||||||
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}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return repr(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Matcher(metaclass=MatcherMeta):
|
|
||||||
"""事件响应器类"""
|
|
||||||
|
|
||||||
plugin: Optional["Plugin"] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[Plugin]``
|
|
||||||
:说明: 事件响应器所在插件
|
|
||||||
"""
|
|
||||||
module: Optional[ModuleType] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[ModuleType]``
|
|
||||||
:说明: 事件响应器所在插件模块
|
|
||||||
"""
|
|
||||||
plugin_name: Optional[str] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[str]``
|
|
||||||
:说明: 事件响应器所在插件名
|
|
||||||
"""
|
|
||||||
module_name: Optional[str] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[str]``
|
|
||||||
:说明: 事件响应器所在点分割插件模块路径
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: str = ""
|
|
||||||
"""
|
|
||||||
:类型: ``str``
|
|
||||||
:说明: 事件响应器类型
|
|
||||||
"""
|
|
||||||
rule: Rule = Rule()
|
|
||||||
"""
|
|
||||||
:类型: ``Rule``
|
|
||||||
:说明: 事件响应器匹配规则
|
|
||||||
"""
|
|
||||||
permission: Permission = Permission()
|
|
||||||
"""
|
|
||||||
:类型: ``Permission``
|
|
||||||
:说明: 事件响应器触发权限
|
|
||||||
"""
|
|
||||||
handlers: List[Dependent[Any]] = []
|
|
||||||
"""
|
|
||||||
:类型: ``List[Handler]``
|
|
||||||
:说明: 事件响应器拥有的事件处理函数列表
|
|
||||||
"""
|
|
||||||
priority: int = 1
|
|
||||||
"""
|
|
||||||
:类型: ``int``
|
|
||||||
:说明: 事件响应器优先级
|
|
||||||
"""
|
|
||||||
block: bool = False
|
|
||||||
"""
|
|
||||||
:类型: ``bool``
|
|
||||||
:说明: 事件响应器是否阻止事件传播
|
|
||||||
"""
|
|
||||||
temp: bool = False
|
|
||||||
"""
|
|
||||||
:类型: ``bool``
|
|
||||||
:说明: 事件响应器是否为临时
|
|
||||||
"""
|
|
||||||
expire_time: Optional[datetime] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[datetime]``
|
|
||||||
:说明: 事件响应器过期时间点
|
|
||||||
"""
|
|
||||||
|
|
||||||
_default_state: T_State = {}
|
|
||||||
"""
|
|
||||||
:类型: ``T_State``
|
|
||||||
:说明: 事件响应器默认状态
|
|
||||||
"""
|
|
||||||
|
|
||||||
_default_type_updater: Optional[Dependent[str]] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[Dependent]``
|
|
||||||
:说明: 事件响应器类型更新函数
|
|
||||||
"""
|
|
||||||
_default_permission_updater: Optional[Dependent[Permission]] = None
|
|
||||||
"""
|
|
||||||
:类型: ``Optional[Dependent]``
|
|
||||||
:说明: 事件响应器权限更新函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES = [
|
|
||||||
params.DependParam,
|
|
||||||
params.BotParam,
|
|
||||||
params.EventParam,
|
|
||||||
params.StateParam,
|
|
||||||
params.ArgParam,
|
|
||||||
params.MatcherParam,
|
|
||||||
params.DefaultParam,
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""实例化 Matcher 以便运行"""
|
|
||||||
self.handlers = self.handlers.copy()
|
|
||||||
self.state = self._default_state.copy()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
|
|
||||||
f"priority={self.priority}, temp={self.temp}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return repr(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def new(
|
|
||||||
cls,
|
|
||||||
type_: str = "",
|
|
||||||
rule: Optional[Rule] = None,
|
|
||||||
permission: Optional[Permission] = None,
|
|
||||||
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
|
|
||||||
temp: bool = False,
|
|
||||||
priority: int = 1,
|
|
||||||
block: bool = False,
|
|
||||||
*,
|
|
||||||
plugin: Optional["Plugin"] = None,
|
|
||||||
module: Optional[ModuleType] = None,
|
|
||||||
expire_time: Optional[datetime] = None,
|
|
||||||
default_state: Optional[T_State] = None,
|
|
||||||
default_type_updater: Optional[T_TypeUpdater] = None,
|
|
||||||
default_permission_updater: Optional[T_PermissionUpdater] = None,
|
|
||||||
) -> Type["Matcher"]:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``type_: str``: 事件响应器类型,与 ``event.get_type()`` 一致时触发,空字符串表示任意
|
|
||||||
* ``rule: Optional[Rule]``: 匹配规则
|
|
||||||
* ``permission: Optional[Permission]``: 权限
|
|
||||||
* ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表
|
|
||||||
* ``temp: bool``: 是否为临时事件响应器,即触发一次后删除
|
|
||||||
* ``priority: int``: 响应优先级
|
|
||||||
* ``block: bool``: 是否阻止事件向更低优先级的响应器传播
|
|
||||||
* ``plugin: Optional[Plugin]``: 事件响应器所在插件
|
|
||||||
* ``module: Optional[ModuleType]``: 事件响应器所在模块
|
|
||||||
* ``default_state: Optional[T_State]``: 默认状态 ``state``
|
|
||||||
* ``expire_time: Optional[datetime]``: 事件响应器最终有效时间点,过时即被删除
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``Type[Matcher]``: 新的事件响应器类
|
|
||||||
"""
|
|
||||||
NewMatcher = type(
|
|
||||||
"Matcher",
|
|
||||||
(Matcher,),
|
|
||||||
{
|
|
||||||
"plugin": plugin,
|
|
||||||
"module": module,
|
|
||||||
"plugin_name": plugin and plugin.name,
|
|
||||||
"module_name": module and module.__name__,
|
|
||||||
"type": type_,
|
|
||||||
"rule": rule or Rule(),
|
|
||||||
"permission": permission or Permission(),
|
|
||||||
"handlers": [
|
|
||||||
handler
|
|
||||||
if isinstance(handler, Dependent)
|
|
||||||
else Dependent[Any].parse(
|
|
||||||
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
|
|
||||||
)
|
|
||||||
for handler in handlers
|
|
||||||
]
|
|
||||||
if handlers
|
|
||||||
else [],
|
|
||||||
"temp": temp,
|
|
||||||
"expire_time": expire_time,
|
|
||||||
"priority": priority,
|
|
||||||
"block": block,
|
|
||||||
"_default_state": default_state or {},
|
|
||||||
"_default_type_updater": default_type_updater,
|
|
||||||
"_default_permission_updater": default_permission_updater,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.trace(f"Define new matcher {NewMatcher}")
|
|
||||||
|
|
||||||
matchers[priority].append(NewMatcher)
|
|
||||||
|
|
||||||
return NewMatcher
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def check_perm(
|
|
||||||
cls,
|
|
||||||
bot: Bot,
|
|
||||||
event: Event,
|
|
||||||
stack: Optional[AsyncExitStack] = None,
|
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
检查是否满足触发权限
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``bot: Bot``: Bot 对象
|
|
||||||
* ``event: Event``: 上报事件
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``bool``: 是否满足权限
|
|
||||||
"""
|
|
||||||
event_type = event.get_type()
|
|
||||||
return event_type == (cls.type or event_type) and await cls.permission(
|
|
||||||
bot, event, stack, dependency_cache
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def check_rule(
|
|
||||||
cls,
|
|
||||||
bot: Bot,
|
|
||||||
event: Event,
|
|
||||||
state: T_State,
|
|
||||||
stack: Optional[AsyncExitStack] = None,
|
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
检查是否满足匹配规则
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``bot: Bot``: Bot 对象
|
|
||||||
* ``event: Event``: 上报事件
|
|
||||||
* ``state: T_State``: 当前状态
|
|
||||||
|
|
||||||
:返回:
|
|
||||||
|
|
||||||
- ``bool``: 是否满足匹配规则
|
|
||||||
"""
|
|
||||||
event_type = event.get_type()
|
|
||||||
return event_type == (cls.type or event_type) and await cls.rule(
|
|
||||||
bot, event, state, stack, dependency_cache
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``func: T_TypeUpdater``: 响应事件类型更新函数
|
|
||||||
"""
|
|
||||||
cls._default_type_updater = Dependent[str].parse(
|
|
||||||
call=func, allow_types=cls.HANDLER_PARAM_TYPES
|
|
||||||
)
|
|
||||||
return func
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数来更改当前事件响应器的默认会话权限更新函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``func: T_PermissionUpdater``: 会话权限更新函数
|
|
||||||
"""
|
|
||||||
cls._default_permission_updater = Dependent[Permission].parse(
|
|
||||||
call=func, allow_types=cls.HANDLER_PARAM_TYPES
|
|
||||||
)
|
|
||||||
return func
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def append_handler(
|
|
||||||
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
|
|
||||||
) -> Dependent[Any]:
|
|
||||||
handler_ = Dependent[Any].parse(
|
|
||||||
call=handler,
|
|
||||||
parameterless=parameterless,
|
|
||||||
allow_types=cls.HANDLER_PARAM_TYPES,
|
|
||||||
)
|
|
||||||
cls.handlers.append(handler_)
|
|
||||||
return handler_
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def handle(
|
|
||||||
cls, parameterless: Optional[List[Any]] = None
|
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数来向事件响应器直接添加一个处理函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _decorator(func: T_Handler) -> T_Handler:
|
|
||||||
cls.append_handler(func, parameterless=parameterless)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def receive(
|
|
||||||
cls, id: str = "", parameterless: Optional[List[Any]] = None
|
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``id: str``: 消息 ID
|
|
||||||
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
|
|
||||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
|
||||||
if matcher.get_target() == RECEIVE_KEY.format(id=id):
|
|
||||||
matcher.set_receive(id, event)
|
|
||||||
return
|
|
||||||
if matcher.get_receive(id):
|
|
||||||
return
|
|
||||||
await matcher.reject()
|
|
||||||
|
|
||||||
_parameterless = [params.Depends(_receive), *(parameterless or [])]
|
|
||||||
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
cls.append_handler(func, parameterless=_parameterless)
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def got(
|
|
||||||
cls,
|
|
||||||
key: str,
|
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
parameterless: Optional[List[Any]] = None,
|
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
装饰一个函数来指示 NoneBot 当要获取的 ``key`` 不存在时接收用户新的一条消息并经过 ``ArgsParser`` 处理后再运行该函数,如果 ``key`` 已存在则直接继续运行
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``key: str``: 参数名
|
|
||||||
* ``prompt: Optional[Union[str, Message, MessageSegment, MessageFormatter]]``: 在参数不存在时向用户发送的消息
|
|
||||||
* ``args_parser: Optional[T_ArgsParser]``: 可选参数解析函数,空则使用默认解析函数
|
|
||||||
* ``parameterless: Optional[List[Any]]``: 非参数类型依赖列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def _key_getter(event: Event, matcher: "Matcher"):
|
|
||||||
matcher.set_target(ARG_KEY.format(key=key))
|
|
||||||
if matcher.get_target() == ARG_KEY.format(key=key):
|
|
||||||
matcher.set_arg(key, event.get_message())
|
|
||||||
return
|
|
||||||
if matcher.get_arg(key):
|
|
||||||
return
|
|
||||||
await matcher.reject(prompt)
|
|
||||||
|
|
||||||
_parameterless = [
|
|
||||||
params.Depends(_key_getter),
|
|
||||||
*(parameterless or []),
|
|
||||||
]
|
|
||||||
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
cls.append_handler(func, parameterless=_parameterless)
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def send(
|
|
||||||
cls,
|
|
||||||
message: Union[str, Message, MessageSegment, MessageTemplate],
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
发送一条消息给当前交互用户
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``message: Union[str, Message, MessageSegment]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
bot = current_bot.get()
|
|
||||||
event = current_event.get()
|
|
||||||
state = current_matcher.get().state
|
|
||||||
if isinstance(message, MessageTemplate):
|
|
||||||
_message = message.format(**state)
|
|
||||||
else:
|
|
||||||
_message = message
|
|
||||||
return await bot.send(event=event, message=_message, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def finish(
|
|
||||||
cls,
|
|
||||||
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> NoReturn:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
发送一条消息给当前交互用户并结束当前事件响应器
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``message: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
if message is not None:
|
|
||||||
await cls.send(message, **kwargs)
|
|
||||||
raise FinishedException
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def pause(
|
|
||||||
cls,
|
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> NoReturn:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
if prompt is not None:
|
|
||||||
await cls.send(prompt, **kwargs)
|
|
||||||
raise PausedException
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def reject(
|
|
||||||
cls,
|
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> NoReturn:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
最近使用 ``got`` / ``receive`` 接收的消息不符合预期,发送一条消息给当前交互用户并暂停事件响应器,
|
|
||||||
在接收用户新的一条消息后继续当前处理函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
if prompt is not None:
|
|
||||||
await cls.send(prompt, **kwargs)
|
|
||||||
raise RejectedException
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def reject_arg(
|
|
||||||
cls,
|
|
||||||
key: str,
|
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> NoReturn:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
最近使用 ``got`` 接收的消息不符合预期,发送一条消息给当前交互用户并暂停事件响应器,
|
|
||||||
在接收用户新的一条消息后继续当前处理函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``key: str``: 参数名
|
|
||||||
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
matcher = current_matcher.get()
|
|
||||||
matcher.set_target(ARG_KEY.format(key=key))
|
|
||||||
if prompt is not None:
|
|
||||||
await cls.send(prompt, **kwargs)
|
|
||||||
raise RejectedException
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def reject_receive(
|
|
||||||
cls,
|
|
||||||
id: str = "",
|
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> NoReturn:
|
|
||||||
"""
|
|
||||||
:说明:
|
|
||||||
|
|
||||||
最近使用 ``got`` 接收的消息不符合预期,发送一条消息给当前交互用户并暂停事件响应器,
|
|
||||||
在接收用户新的一条消息后继续当前处理函数
|
|
||||||
|
|
||||||
:参数:
|
|
||||||
|
|
||||||
* ``id: str``: 消息 id
|
|
||||||
* ``prompt: Union[str, Message, MessageSegment, MessageTemplate]``: 消息内容
|
|
||||||
* ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api
|
|
||||||
"""
|
|
||||||
matcher = current_matcher.get()
|
|
||||||
matcher.set_target(RECEIVE_KEY.format(id=id))
|
|
||||||
if prompt is not None:
|
|
||||||
await cls.send(prompt, **kwargs)
|
|
||||||
raise RejectedException
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def skip(cls) -> NoReturn:
|
|
||||||
raise SkippedException
|
|
||||||
|
|
||||||
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
|
|
||||||
return self.state.get(RECEIVE_KEY.format(id=id), default)
|
|
||||||
|
|
||||||
def set_receive(self, id: str, event: Event) -> None:
|
|
||||||
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]:
|
|
||||||
return self.state.get(LAST_RECEIVE_KEY, default)
|
|
||||||
|
|
||||||
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
|
|
||||||
return self.state.get(ARG_KEY.format(key=key), default)
|
|
||||||
|
|
||||||
def set_arg(self, key: str, message: Message) -> None:
|
|
||||||
self.state[ARG_KEY.format(key=key)] = message
|
|
||||||
|
|
||||||
def set_target(self, target: str, cache: bool = True) -> None:
|
|
||||||
if cache:
|
|
||||||
self.state[REJECT_CACHE_TARGET] = target
|
|
||||||
else:
|
|
||||||
self.state[REJECT_TARGET] = target
|
|
||||||
|
|
||||||
def get_target(self, default: T = None) -> 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:
|
|
||||||
updater = self.__class__._default_type_updater
|
|
||||||
if not updater:
|
|
||||||
return "message"
|
|
||||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
|
||||||
|
|
||||||
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 resolve_reject(self):
|
|
||||||
handler = current_handler.get()
|
|
||||||
self.handlers.insert(0, handler)
|
|
||||||
if REJECT_CACHE_TARGET in self.state:
|
|
||||||
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
|
|
||||||
|
|
||||||
async def simple_run(
|
|
||||||
self,
|
|
||||||
bot: Bot,
|
|
||||||
event: Event,
|
|
||||||
state: T_State,
|
|
||||||
stack: Optional[AsyncExitStack] = None,
|
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
|
||||||
):
|
|
||||||
logger.trace(
|
|
||||||
f"Matcher {self} run with incoming args: "
|
|
||||||
f"bot={bot}, event={event}, state={state}"
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 运行handlers
|
|
||||||
async def run(
|
|
||||||
self,
|
|
||||||
bot: Bot,
|
|
||||||
event: Event,
|
|
||||||
state: T_State,
|
|
||||||
stack: Optional[AsyncExitStack] = None,
|
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
await self.simple_run(bot, event, state, stack, dependency_cache)
|
|
||||||
|
|
||||||
except RejectedException:
|
|
||||||
await self.resolve_reject()
|
|
||||||
type_ = await self.update_type(bot, event)
|
|
||||||
permission = await self.update_permission(bot, event)
|
|
||||||
|
|
||||||
Matcher.new(
|
|
||||||
type_,
|
|
||||||
Rule(),
|
|
||||||
permission,
|
|
||||||
self.handlers,
|
|
||||||
temp=True,
|
|
||||||
priority=0,
|
|
||||||
block=True,
|
|
||||||
plugin=self.plugin,
|
|
||||||
module=self.module,
|
|
||||||
expire_time=datetime.now() + bot.config.session_expire_timeout,
|
|
||||||
default_state=self.state,
|
|
||||||
default_type_updater=self.__class__._default_type_updater,
|
|
||||||
default_permission_updater=self.__class__._default_permission_updater,
|
|
||||||
)
|
|
||||||
except PausedException:
|
|
||||||
type_ = await self.update_type(bot, event)
|
|
||||||
permission = await self.update_permission(bot, event)
|
|
||||||
|
|
||||||
Matcher.new(
|
|
||||||
type_,
|
|
||||||
Rule(),
|
|
||||||
permission,
|
|
||||||
self.handlers,
|
|
||||||
temp=True,
|
|
||||||
priority=0,
|
|
||||||
block=True,
|
|
||||||
plugin=self.plugin,
|
|
||||||
module=self.module,
|
|
||||||
expire_time=datetime.now() + bot.config.session_expire_timeout,
|
|
||||||
default_state=self.state,
|
|
||||||
default_type_updater=self.__class__._default_type_updater,
|
|
||||||
default_permission_updater=self.__class__._default_permission_updater,
|
|
||||||
)
|
|
||||||
except FinishedException:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
"""
|
"""本模块定义了事件处理主要流程。
|
||||||
事件处理
|
|
||||||
========
|
|
||||||
|
|
||||||
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
||||||
|
|
||||||
|
FrontMatter:
|
||||||
|
sidebar_position: 2
|
||||||
|
description: nonebot.message 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
|
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||||
|
|
||||||
from nonebot import params
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.rule import TrieRule
|
from nonebot.rule import TrieRule
|
||||||
from nonebot.utils import escape_tag
|
|
||||||
from nonebot.dependencies import Dependent
|
from nonebot.dependencies import Dependent
|
||||||
from nonebot.matcher import Matcher, matchers
|
from nonebot.matcher import Matcher, matchers
|
||||||
|
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||||
from nonebot.exception import (
|
from nonebot.exception import (
|
||||||
NoLogException,
|
NoLogException,
|
||||||
StopPropagation,
|
StopPropagation,
|
||||||
@@ -30,132 +32,352 @@ from nonebot.typing import (
|
|||||||
T_EventPreProcessor,
|
T_EventPreProcessor,
|
||||||
T_EventPostProcessor,
|
T_EventPostProcessor,
|
||||||
)
|
)
|
||||||
|
from nonebot.internal.params import (
|
||||||
|
ArgParam,
|
||||||
|
BotParam,
|
||||||
|
EventParam,
|
||||||
|
StateParam,
|
||||||
|
DependParam,
|
||||||
|
DefaultParam,
|
||||||
|
MatcherParam,
|
||||||
|
ExceptionParam,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nonebot.adapters import Bot, Event
|
from nonebot.adapters import Bot, Event
|
||||||
|
|
||||||
_event_preprocessors: Set[Dependent[None]] = set()
|
_event_preprocessors: Set[Dependent[Any]] = set()
|
||||||
_event_postprocessors: Set[Dependent[None]] = set()
|
_event_postprocessors: Set[Dependent[Any]] = set()
|
||||||
_run_preprocessors: Set[Dependent[None]] = set()
|
_run_preprocessors: Set[Dependent[Any]] = set()
|
||||||
_run_postprocessors: Set[Dependent[None]] = set()
|
_run_postprocessors: Set[Dependent[Any]] = set()
|
||||||
|
|
||||||
EVENT_PCS_PARAMS = [
|
EVENT_PCS_PARAMS = (
|
||||||
params.DependParam,
|
DependParam,
|
||||||
params.BotParam,
|
BotParam,
|
||||||
params.EventParam,
|
EventParam,
|
||||||
params.StateParam,
|
StateParam,
|
||||||
params.DefaultParam,
|
DefaultParam,
|
||||||
]
|
)
|
||||||
RUN_PREPCS_PARAMS = [
|
RUN_PREPCS_PARAMS = (
|
||||||
params.DependParam,
|
DependParam,
|
||||||
params.BotParam,
|
BotParam,
|
||||||
params.EventParam,
|
EventParam,
|
||||||
params.StateParam,
|
StateParam,
|
||||||
params.ArgParam,
|
ArgParam,
|
||||||
params.MatcherParam,
|
MatcherParam,
|
||||||
params.DefaultParam,
|
DefaultParam,
|
||||||
]
|
)
|
||||||
RUN_POSTPCS_PARAMS = [
|
RUN_POSTPCS_PARAMS = (
|
||||||
params.DependParam,
|
DependParam,
|
||||||
params.ExceptionParam,
|
ExceptionParam,
|
||||||
params.BotParam,
|
BotParam,
|
||||||
params.EventParam,
|
EventParam,
|
||||||
params.StateParam,
|
StateParam,
|
||||||
params.ArgParam,
|
ArgParam,
|
||||||
params.MatcherParam,
|
MatcherParam,
|
||||||
params.DefaultParam,
|
DefaultParam,
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
|
def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:
|
||||||
"""
|
"""事件预处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
事件预处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。
|
装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。
|
||||||
"""
|
"""
|
||||||
_event_preprocessors.add(
|
_event_preprocessors.add(
|
||||||
Dependent[None].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
||||||
)
|
)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
|
def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:
|
||||||
"""
|
"""事件后处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
事件后处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。
|
装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。
|
||||||
"""
|
"""
|
||||||
_event_postprocessors.add(
|
_event_postprocessors.add(
|
||||||
Dependent[None].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)
|
||||||
)
|
)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
|
def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:
|
||||||
"""
|
"""运行预处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
运行预处理。装饰一个函数,使它在每次事件响应器运行前执行。
|
装饰一个函数,使它在每次事件响应器运行前执行。
|
||||||
"""
|
"""
|
||||||
_run_preprocessors.add(
|
_run_preprocessors.add(
|
||||||
Dependent[None].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
|
Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS)
|
||||||
)
|
)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||||
"""
|
"""运行后处理。
|
||||||
:说明:
|
|
||||||
|
|
||||||
运行后处理。装饰一个函数,使它在每次事件响应器运行后执行。
|
装饰一个函数,使它在每次事件响应器运行后执行。
|
||||||
"""
|
"""
|
||||||
_run_postprocessors.add(
|
_run_postprocessors.add(
|
||||||
Dependent[None].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
|
Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)
|
||||||
)
|
)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]) -> Any:
|
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:
|
try:
|
||||||
return await coro
|
await asyncio.gather(
|
||||||
except SkippedException:
|
*(
|
||||||
pass
|
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:
|
||||||
|
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:
|
||||||
|
"""运行事件响应器运行后处理。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
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(
|
async def _check_matcher(
|
||||||
priority: int,
|
|
||||||
Matcher: Type[Matcher],
|
Matcher: Type[Matcher],
|
||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: Optional[AsyncExitStack] = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = 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:
|
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
matchers[priority].remove(Matcher)
|
Matcher.destroy()
|
||||||
except Exception:
|
return False
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not await Matcher.check_perm(
|
if not await Matcher.check_perm(bot, event, stack, dependency_cache):
|
||||||
bot, event, stack, dependency_cache
|
logger.trace(f"Permission conditions not met for {Matcher}")
|
||||||
) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
return False
|
||||||
return
|
except Exception as e:
|
||||||
|
logger.opt(colors=True, exception=e).error(
|
||||||
|
f"<r><bg #f8bbd0>Permission check failed for {Matcher}.</bg #f8bbd0></r>"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await Matcher.check_rule(bot, event, state, stack, dependency_cache):
|
||||||
|
logger.trace(f"Rule conditions not met for {Matcher}")
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(colors=True, exception=e).error(
|
logger.opt(colors=True, exception=e).error(
|
||||||
f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
|
f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
|
|
||||||
if Matcher.temp:
|
return True
|
||||||
try:
|
|
||||||
matchers[priority].remove(Matcher)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
|
|
||||||
|
|
||||||
|
|
||||||
async def _run_matcher(
|
async def _run_matcher(
|
||||||
@@ -166,100 +388,116 @@ async def _run_matcher(
|
|||||||
stack: Optional[AsyncExitStack] = None,
|
stack: Optional[AsyncExitStack] = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: Optional[T_DependencyCache] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""运行事件响应器。
|
||||||
|
|
||||||
|
临时事件响应器将在运行前被**销毁**。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
Matcher: 事件响应器
|
||||||
|
bot: Bot 对象
|
||||||
|
event: Event 对象
|
||||||
|
state: 会话状态
|
||||||
|
stack: 异步上下文栈
|
||||||
|
dependency_cache: 依赖缓存
|
||||||
|
|
||||||
|
异常:
|
||||||
|
StopPropagation: 阻止事件继续传播
|
||||||
|
"""
|
||||||
logger.info(f"Event will be handled by {Matcher}")
|
logger.info(f"Event will be handled by {Matcher}")
|
||||||
|
|
||||||
|
if Matcher.temp:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
Matcher.destroy()
|
||||||
|
|
||||||
matcher = Matcher()
|
matcher = Matcher()
|
||||||
|
|
||||||
coros = list(
|
if not await _apply_run_preprocessors(
|
||||||
map(
|
bot=bot,
|
||||||
lambda x: _run_coro_with_catch(
|
event=event,
|
||||||
x(
|
state=state,
|
||||||
matcher=matcher,
|
matcher=matcher,
|
||||||
bot=bot,
|
stack=stack,
|
||||||
event=event,
|
dependency_cache=dependency_cache,
|
||||||
state=state,
|
):
|
||||||
stack=stack,
|
return
|
||||||
dependency_cache=dependency_cache,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
_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
|
|
||||||
|
|
||||||
exception = None
|
exception = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Running matcher {matcher}")
|
logger.debug(f"Running {matcher}")
|
||||||
await matcher.run(bot, event, state, stack, dependency_cache)
|
await matcher.run(bot, event, state, stack, dependency_cache)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(colors=True, exception=e).error(
|
logger.opt(colors=True, exception=e).error(
|
||||||
f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
|
f"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>"
|
||||||
)
|
)
|
||||||
exception = e
|
exception = e
|
||||||
|
|
||||||
coros = list(
|
await _apply_run_postprocessors(
|
||||||
map(
|
bot=bot,
|
||||||
lambda x: _run_coro_with_catch(
|
event=event,
|
||||||
x(
|
matcher=matcher,
|
||||||
matcher=matcher,
|
exception=exception,
|
||||||
exception=exception,
|
stack=stack,
|
||||||
bot=bot,
|
dependency_cache=dependency_cache,
|
||||||
event=event,
|
|
||||||
state=state,
|
|
||||||
stack=stack,
|
|
||||||
dependency_cache=dependency_cache,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
_run_postprocessors,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
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:
|
if matcher.block:
|
||||||
raise StopPropagation
|
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:
|
async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||||
"""
|
"""处理一个事件。调用该函数以实现分发事件。
|
||||||
:说明:
|
|
||||||
|
|
||||||
处理一个事件。调用该函数以实现分发事件。
|
参数:
|
||||||
|
bot: Bot 对象
|
||||||
:参数:
|
event: Event 对象
|
||||||
|
|
||||||
* ``bot: Bot``: Bot 对象
|
|
||||||
* ``event: Event``: Event 对象
|
|
||||||
|
|
||||||
:示例:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
用法:
|
||||||
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
asyncio.create_task(handle_event(bot, event))
|
asyncio.create_task(handle_event(bot, event))
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
show_log = True
|
show_log = True
|
||||||
log_msg = f"<m>{escape_tag(bot.type.upper())} {escape_tag(bot.self_id)}</m> | "
|
log_msg = f"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | "
|
||||||
try:
|
try:
|
||||||
log_msg += event.get_log_string()
|
log_msg += event.get_log_string()
|
||||||
except NoLogException:
|
except NoLogException:
|
||||||
@@ -270,37 +508,16 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
state: Dict[Any, Any] = {}
|
state: Dict[Any, Any] = {}
|
||||||
dependency_cache: T_DependencyCache = {}
|
dependency_cache: T_DependencyCache = {}
|
||||||
|
|
||||||
|
# create event scope context
|
||||||
async with AsyncExitStack() as stack:
|
async with AsyncExitStack() as stack:
|
||||||
coros = list(
|
if not await _apply_event_preprocessors(
|
||||||
map(
|
bot=bot,
|
||||||
lambda x: _run_coro_with_catch(
|
event=event,
|
||||||
x(
|
state=state,
|
||||||
bot=bot,
|
stack=stack,
|
||||||
event=event,
|
dependency_cache=dependency_cache,
|
||||||
state=state,
|
):
|
||||||
stack=stack,
|
return
|
||||||
dependency_cache=dependency_cache,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
_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
|
|
||||||
|
|
||||||
# Trie Match
|
# Trie Match
|
||||||
try:
|
try:
|
||||||
@@ -311,6 +528,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
break_flag = False
|
break_flag = False
|
||||||
|
# iterate through all priority until stop propagation
|
||||||
for priority in sorted(matchers.keys()):
|
for priority in sorted(matchers.keys()):
|
||||||
if break_flag:
|
if break_flag:
|
||||||
break
|
break
|
||||||
@@ -319,14 +537,12 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
logger.debug(f"Checking for matchers in priority {priority}...")
|
logger.debug(f"Checking for matchers in priority {priority}...")
|
||||||
|
|
||||||
pending_tasks = [
|
pending_tasks = [
|
||||||
_check_matcher(
|
check_and_run_matcher(
|
||||||
priority, matcher, bot, event, state.copy(), stack, dependency_cache
|
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||||
)
|
)
|
||||||
for matcher in matchers[priority]
|
for matcher in matchers[priority]
|
||||||
]
|
]
|
||||||
|
|
||||||
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
if not isinstance(result, Exception):
|
if not isinstance(result, Exception):
|
||||||
continue
|
continue
|
||||||
@@ -338,26 +554,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
|||||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||||
)
|
)
|
||||||
|
|
||||||
coros = list(
|
if show_log:
|
||||||
map(
|
logger.debug("Checking for matchers completed")
|
||||||
lambda x: _run_coro_with_catch(
|
|
||||||
x(
|
await _apply_event_postprocessors(bot, event, state, stack, dependency_cache)
|
||||||
bot=bot,
|
|
||||||
event=event,
|
|
||||||
state=state,
|
|
||||||
stack=stack,
|
|
||||||
dependency_cache=dependency_cache,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
_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>"
|
|
||||||
)
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user