Compare commits
739 Commits
Daishiky/m
...
Gnome-py/K
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e649f356be | ||
|
|
3260ec6643 | ||
|
|
d16d2d856f | ||
|
|
456d71d228 | ||
|
|
093a38527d | ||
|
|
163d8e6586 | ||
|
|
0637a628ca | ||
|
|
02c6b07834 | ||
|
|
b810848273 | ||
|
|
cd4bb296f3 | ||
|
|
6a63ce2ed7 | ||
|
|
fba7ca420c | ||
|
|
e65415d3c8 | ||
|
|
2ecf755372 | ||
|
|
00ae8bb18c | ||
|
|
0638bda719 | ||
|
|
1957fa6011 | ||
|
|
75a23351c4 | ||
|
|
7513c2138f | ||
|
|
a23dae8604 | ||
|
|
1833e984ce | ||
|
|
53a6b2cb45 | ||
|
|
65640ddfc7 | ||
|
|
14b3188bb8 | ||
|
|
3ffe134895 | ||
|
|
1032728311 | ||
|
|
33470ff196 | ||
|
|
47e42d1648 | ||
|
|
4055bafaa5 | ||
|
|
152b61aabb | ||
|
|
f37be7961a | ||
|
|
0f6db99c59 | ||
|
|
42c0a8d8a5 | ||
|
|
092fbca08f | ||
|
|
13834d1147 | ||
|
|
5d10384576 | ||
|
|
fc0188d7bc | ||
|
|
a62c0ff0d1 | ||
|
|
b75be64044 | ||
|
|
630a842556 | ||
|
|
c485e08ea0 | ||
|
|
dba9a8abb9 | ||
|
|
6f5614373a | ||
|
|
2e12746c70 | ||
|
|
5ef72e4f70 | ||
|
|
7e18d30820 | ||
|
|
923a6a885d | ||
|
|
b28893aa36 | ||
|
|
6e41bd2219 | ||
|
|
773ad6f5bf | ||
|
|
de0e8ef108 | ||
|
|
64ee792391 | ||
|
|
22de755059 | ||
|
|
fa7f8efc8e | ||
|
|
9d1df65af3 | ||
|
|
3ce86f6cde | ||
|
|
31e3e99c2b | ||
|
|
cc90d312f5 | ||
|
|
8cdc1f4ad9 | ||
|
|
75f052b8c9 | ||
|
|
c8cdb275c5 | ||
|
|
406f0ffe04 | ||
|
|
a4acbd2e08 | ||
|
|
6bcc717e63 | ||
|
|
86618f42a6 | ||
|
|
9d474b92f6 | ||
|
|
96ac8c1a4f | ||
|
|
cf4d2e23a2 | ||
|
|
45d498c1b7 | ||
|
|
7b87d2dec0 | ||
|
|
41d22f4312 | ||
|
|
12dcc7c44b | ||
|
|
b12fe223b2 | ||
|
|
a2a7b0f076 | ||
|
|
b2ac327bd8 | ||
|
|
f485f1b612 | ||
|
|
29b808d33f | ||
|
|
516675dd2e | ||
|
|
932efa1edc | ||
|
|
059ec161f8 | ||
|
|
e7821be4aa | ||
|
|
4aafa39e8c | ||
|
|
d2ea33e5e9 | ||
|
|
2f2c39ed22 | ||
|
|
efec816de2 | ||
|
|
dd7d4b8e7f | ||
|
|
2d8f299b6b | ||
|
|
3382d2e9e8 | ||
|
|
539577a2dd | ||
|
|
4f8e67998a | ||
|
|
848d752388 | ||
|
|
78598d59d7 | ||
|
|
ef79305961 | ||
|
|
c6a6c6af85 | ||
|
|
9a61a5a063 | ||
|
|
565b41b0b2 | ||
|
|
835432d161 | ||
|
|
f586f4dfbd | ||
|
|
769db38401 | ||
|
|
c82739a3be | ||
|
|
8306b9f6af | ||
|
|
490bbffc93 | ||
|
|
9d4fa0341e | ||
|
|
cff9ca0288 | ||
|
|
9dd86bbcb3 | ||
|
|
8bbb8f6db9 | ||
|
|
d8b06ca7f2 | ||
|
|
3561ce9d5a | ||
|
|
ae01a96bef | ||
|
|
5ef37923de | ||
|
|
61abb43b69 | ||
|
|
73f953eac5 | ||
|
|
400936df69 | ||
|
|
cdf46127ae | ||
|
|
851dfc3c75 | ||
|
|
b8898c7788 | ||
|
|
d17551f51f | ||
|
|
4a6670c062 | ||
|
|
d7a4230007 | ||
|
|
2e52059555 | ||
|
|
49cf959784 | ||
|
|
91652e3b60 | ||
|
|
9db8698748 | ||
|
|
9727b56503 | ||
|
|
e46d974c8a | ||
|
|
d09993d7e7 | ||
|
|
69f578abdc | ||
|
|
1b5c206279 | ||
|
|
d2dd31de63 | ||
|
|
d0c295b595 | ||
|
|
e1e3e298b5 | ||
|
|
4a72201617 | ||
|
|
ea2d972666 | ||
|
|
6268cad402 | ||
|
|
55f79ed096 | ||
|
|
4065014794 | ||
|
|
8d80259a80 | ||
|
|
311eac97b0 | ||
|
|
d5033b04a2 | ||
|
|
7592300535 | ||
|
|
166152647c | ||
|
|
6c36df6c11 | ||
|
|
fbc4a51c35 | ||
|
|
9246bbc8e3 | ||
|
|
fa5a2188bb | ||
|
|
5390caa67d | ||
|
|
745cf541ea | ||
|
|
ef32f6d882 | ||
|
|
b6590d7f56 | ||
|
|
b5a717fb80 | ||
|
|
f4d5fcc8f9 | ||
|
|
1d2eaf8526 | ||
|
|
f3cb197429 | ||
|
|
d4c683738d | ||
|
|
489e5f3288 | ||
|
|
63434fbfd9 | ||
|
|
68453c7bed | ||
|
|
b73f02b9c3 | ||
|
|
0df3f51a0b | ||
|
|
17f0b59c76 | ||
|
|
28ed599345 | ||
|
|
e79a648987 | ||
|
|
79bae47992 | ||
|
|
8fdd1f0d8f | ||
|
|
3b4c6269be | ||
|
|
27debe18ca | ||
|
|
8ac5cdc314 | ||
|
|
6b6bcb92e6 | ||
|
|
f7a3ea90b8 | ||
|
|
c4ee9dcafa | ||
|
|
529fad6fec | ||
|
|
36b9bc8ee3 | ||
|
|
6587b5c7ea | ||
|
|
feae059c68 | ||
|
|
1e17b7fcea | ||
|
|
fda543c844 | ||
|
|
08a4db3961 | ||
|
|
6e6c8a7b28 | ||
|
|
f631ed22b6 | ||
|
|
a9d9f496f0 | ||
|
|
dc9c224b54 | ||
|
|
1c40d43fd1 | ||
|
|
66871f329e | ||
|
|
1279510194 | ||
|
|
4fca699810 | ||
|
|
5e800a726e | ||
|
|
1418464813 | ||
|
|
1c63816cc0 | ||
|
|
ec32b71ff9 | ||
|
|
c628224403 | ||
|
|
58ca9e9932 | ||
|
|
0cc67e58ed | ||
|
|
035d110837 | ||
|
|
b87d306a70 | ||
|
|
e795d341e7 | ||
|
|
f72350199d | ||
|
|
b640493300 | ||
|
|
2de0398d66 | ||
|
|
e2250d402e | ||
|
|
2cb5ce981e | ||
|
|
41f3998a08 | ||
|
|
13a47dfe6e | ||
|
|
8b148afabd | ||
|
|
3a451c9c65 | ||
|
|
655bf25cc8 | ||
|
|
6beef898c6 | ||
|
|
658b61d468 | ||
|
|
06a371e80a | ||
|
|
608e3b5b6c | ||
|
|
d08725b0f0 | ||
|
|
3ad95f3746 | ||
|
|
56f800de9c | ||
|
|
8db79d2579 | ||
|
|
8851e03a6d | ||
|
|
60d82cf908 | ||
|
|
ecf239d2a2 | ||
|
|
0d3bd3083c | ||
|
|
731a8816bb | ||
|
|
dac0267e28 | ||
|
|
13251da8ce | ||
|
|
9d25bb454b | ||
|
|
0112c2819f | ||
|
|
906c13d4f0 | ||
|
|
a053f77275 | ||
|
|
51b02f2568 | ||
|
|
ca9b371982 | ||
|
|
41e2d3c637 | ||
|
|
154c90ef59 | ||
|
|
1472e9ed7c | ||
|
|
85f3e11ef1 | ||
|
|
67026809a8 | ||
|
|
dd8168f902 | ||
|
|
96b9a0e09d | ||
|
|
c059d43e98 | ||
|
|
78aea51f50 | ||
|
|
b47133dfb2 | ||
|
|
fc51736b34 | ||
|
|
23852404ed | ||
|
|
980d6abbea | ||
|
|
bc75945088 | ||
|
|
dd5fc656d9 | ||
|
|
8675a18185 | ||
|
|
a0e5e062c9 | ||
|
|
07483297ad | ||
|
|
48eb981344 | ||
|
|
feed302269 | ||
|
|
262a50196d | ||
|
|
15eb3d2e5d | ||
|
|
0faf4c8e2b | ||
|
|
5b8be9a772 | ||
|
|
834e23dc00 | ||
|
|
26e68b31ef | ||
|
|
f14e584304 | ||
|
|
e2624b9a31 | ||
|
|
f153154b7a | ||
|
|
1a4e73d599 | ||
|
|
0aa825557d | ||
|
|
8fb998b599 | ||
|
|
826ce101fd | ||
|
|
b4e39668fb | ||
|
|
af8742a911 | ||
|
|
8df35c83a9 | ||
|
|
74e1ab09a0 | ||
|
|
03cd6ff433 | ||
|
|
99b8ae35ba | ||
|
|
cb2363f0fd | ||
|
|
88d825a803 | ||
|
|
e0a9365d61 | ||
|
|
717e723a36 | ||
|
|
1ca5b7b8b2 | ||
|
|
3cb539d91b | ||
|
|
5a7cfb3ce6 | ||
|
|
c1c6457598 | ||
|
|
c2acb0a114 | ||
|
|
feb0f7f29d | ||
|
|
7598865609 | ||
|
|
f40f80c0dc | ||
|
|
49f8073262 | ||
|
|
750ba88f2c | ||
|
|
074f34a5fa | ||
|
|
7d9074db8a | ||
|
|
4152819a3c | ||
|
|
5d798aa5e6 | ||
|
|
828e47d83f | ||
|
|
23a69144b6 | ||
|
|
d047cebc35 | ||
|
|
c748e4bce5 | ||
|
|
d1dc41ec2f | ||
|
|
097b6064f1 | ||
|
|
17268c3368 | ||
|
|
6a553b2347 | ||
|
|
93cc1bdd79 | ||
|
|
8b4dd34328 | ||
|
|
3d0dd5bc1b | ||
|
|
0b577fa209 | ||
|
|
be5603141e | ||
|
|
bba4d6c4e4 | ||
|
|
9f981e718b | ||
|
|
88620d052a | ||
|
|
8760b01e76 | ||
|
|
12e90f9c6d | ||
|
|
a8db8546db | ||
|
|
0fae0b4995 | ||
|
|
7ca90874b9 | ||
|
|
b7b75e2b1f | ||
|
|
ffa0b26b82 | ||
|
|
1059c02df7 | ||
|
|
d7ed884593 | ||
|
|
a3d7e06f25 | ||
|
|
982140b5f7 | ||
|
|
69c400d813 | ||
|
|
9ac459b5d3 | ||
|
|
4f0e907e44 | ||
|
|
812bfbe6f9 | ||
|
|
64b48431b4 | ||
|
|
30605e6f4f | ||
|
|
2d597e310b | ||
|
|
d001b9d0ee | ||
|
|
ed6c061d69 | ||
|
|
12e3eba011 | ||
|
|
c1f1c67eed | ||
|
|
cd4b0904db | ||
|
|
d8075d5412 | ||
|
|
157caaec7c | ||
|
|
62dad0f7bf | ||
|
|
1aeec34f84 | ||
|
|
44d1d29708 | ||
|
|
ea1d423904 | ||
|
|
6f3b8072d6 | ||
|
|
2d7c709235 | ||
|
|
a372aadb2d | ||
|
|
53ef89c29f | ||
|
|
c6a69062a8 | ||
|
|
62b024803a | ||
|
|
b1a355394f | ||
|
|
39a674ddee | ||
|
|
abac04b759 | ||
|
|
7d0bd7ed20 | ||
|
|
7601d6cec3 | ||
|
|
7386a971f8 | ||
|
|
2beee8be14 | ||
|
|
eda02c2e91 | ||
|
|
485542c480 | ||
|
|
a2b10a08b9 | ||
|
|
55c7de82d3 | ||
|
|
f9bccabac5 | ||
|
|
d9adf4d35d | ||
|
|
f7d551953b | ||
|
|
6b1d46a1ea | ||
|
|
e96df33ce0 | ||
|
|
e3a66bcccc | ||
|
|
039bb9f871 | ||
|
|
2ce44f7bd6 | ||
|
|
3c5c5fc274 | ||
|
|
d1a2ee4620 | ||
|
|
a75cd93acc | ||
|
|
5acea453cc | ||
|
|
cd6b453cb3 | ||
|
|
4566b64d77 | ||
|
|
b1836c5577 | ||
|
|
75477b2995 | ||
|
|
2cd2d1d3ee | ||
|
|
f7b0ed7b12 | ||
|
|
b59ec318c0 | ||
|
|
6ce1c537d4 | ||
|
|
caa9512a8a | ||
|
|
47e6a754e4 | ||
|
|
8b7e5a50b4 | ||
|
|
1a3422dccc | ||
|
|
233d10649c | ||
|
|
76c9e390f1 | ||
|
|
cbe7a1b3a2 | ||
|
|
b2c9c26841 | ||
|
|
20dd632722 | ||
|
|
3c2cf06e46 | ||
|
|
dbb135b81a | ||
|
|
d30fea5b0d | ||
|
|
f27e2e073f | ||
|
|
0bc5f276a7 | ||
|
|
1c640ad72b | ||
|
|
f0c76a13d3 | ||
|
|
a0e1d1e25f | ||
|
|
04573c3c06 | ||
|
|
f6ea03230e | ||
|
|
c251c51cb1 | ||
|
|
9181bf046b | ||
|
|
04788d0a06 | ||
|
|
fc66c5b92d | ||
|
|
0dd4c4c08c | ||
|
|
35a9533e8d | ||
|
|
11e23c534a | ||
|
|
ee26b58c6c | ||
|
|
fa6fa6a567 | ||
|
|
2eb0ec07ab | ||
|
|
c2df574b2a | ||
|
|
7dccbace78 | ||
|
|
2247fbb23a | ||
|
|
c693945a46 | ||
|
|
746da7d54c | ||
|
|
1152f67efc | ||
|
|
5ae7940ec8 | ||
|
|
e13cbf4644 | ||
|
|
bd369c76ea | ||
|
|
9adf94e6b1 | ||
|
|
92ee2cd598 | ||
|
|
4b51e3e253 | ||
|
|
d0d2d7ea62 | ||
|
|
5a72391b72 | ||
|
|
3a421a3eb9 | ||
|
|
b2176dc0ef | ||
|
|
40127eb7b5 | ||
|
|
b9d8d3872e | ||
|
|
429c5933d9 | ||
|
|
a16f54afdb | ||
|
|
a09f89cedf | ||
|
|
c6d09a8bfa | ||
|
|
72c66a1706 | ||
|
|
4a4e73ec14 | ||
|
|
ac95b8b85b | ||
|
|
51cc7622a6 | ||
|
|
cb9a506686 | ||
|
|
7c6724fdd7 | ||
|
|
9d3962aa7a | ||
|
|
c1ce3b949f | ||
|
|
68c7c538f5 | ||
|
|
6c79714b42 | ||
|
|
4724943861 | ||
|
|
5c2945bcd4 | ||
|
|
94bbdc154c | ||
|
|
a7ae2eb1bb | ||
|
|
dd727fb6f4 | ||
|
|
ab6d592f8c | ||
|
|
2ea2693bd7 | ||
|
|
fb0c6c56e1 | ||
|
|
81e9d70b7b | ||
|
|
876b1e0f3e | ||
|
|
27556ea0a2 | ||
|
|
dbd9ed2c41 | ||
|
|
9e4bcd3df7 | ||
|
|
4b1059579e | ||
|
|
47f2d04940 | ||
|
|
be5f4ae4ab | ||
|
|
2f0a2b244e | ||
|
|
0847085661 | ||
|
|
369951fd80 | ||
|
|
bac6c2fc7b | ||
|
|
78275023cc | ||
|
|
7c40e83d10 | ||
|
|
c811932ca7 | ||
|
|
09f0ed1fba | ||
|
|
89d24cb0bc | ||
|
|
d0097c4281 | ||
|
|
4a3491cc0a | ||
|
|
8dafe4f544 | ||
|
|
2ed3e049e1 | ||
|
|
61a189c217 | ||
|
|
9f98a9a87f | ||
|
|
90a28d48d5 | ||
|
|
7b1c57ed60 | ||
|
|
2ebd5315f9 | ||
|
|
c9cdb47338 | ||
|
|
db58e628ba | ||
|
|
267fad9180 | ||
|
|
c6f3ed1af4 | ||
|
|
1b15772671 | ||
|
|
02c317d9a4 | ||
|
|
7bd1211b36 | ||
|
|
695662416a | ||
|
|
c21d12be5e | ||
|
|
d78e5d979d | ||
|
|
5a68d3a561 | ||
|
|
5a9cbc967b | ||
|
|
794327cdb4 | ||
|
|
1ae40a11b7 | ||
|
|
06743dd434 | ||
|
|
732c5384fd | ||
|
|
4d7822493f | ||
|
|
7e1f8bf1b4 | ||
|
|
52678b2eb5 | ||
|
|
b48f510e15 | ||
|
|
f321efd4de | ||
|
|
b84c199c70 | ||
|
|
c475218112 | ||
|
|
35bef7af38 | ||
|
|
f4fe247813 | ||
|
|
9ba5745e68 | ||
|
|
ef9f61a933 | ||
|
|
6874aa73c4 | ||
|
|
ff36aedf7b | ||
|
|
8bd17ede47 | ||
|
|
aeb2cfb573 | ||
|
|
263f45d05b | ||
|
|
3f60997630 | ||
|
|
97f308d219 | ||
|
|
3453992ce6 | ||
|
|
6c8f1ccbdf | ||
|
|
65db814d4a | ||
|
|
77ed476129 | ||
|
|
6cc3e572ba | ||
|
|
1bf782fcb5 | ||
|
|
1954861668 | ||
|
|
fc64ffdabd | ||
|
|
fbafe20e51 | ||
|
|
c89882441c | ||
|
|
7584834dd4 | ||
|
|
3b83f60b35 | ||
|
|
85758a75b3 | ||
|
|
d42c63e186 | ||
|
|
2ad2cab50c | ||
|
|
5e96ad9261 | ||
|
|
80fd222ca0 | ||
|
|
eda6680377 | ||
|
|
cc800796a2 | ||
|
|
ed9badcddf | ||
|
|
4c0ebc9221 | ||
|
|
cc56f31bcd | ||
|
|
98570793e4 | ||
|
|
f42e922696 | ||
|
|
f56543df15 | ||
|
|
67aabc3230 | ||
|
|
36cf3c94b4 | ||
|
|
3b55573777 | ||
|
|
ac061c31fb | ||
|
|
3c90f16bf0 | ||
|
|
3cb093c709 | ||
|
|
65439732b3 | ||
|
|
f87eaa613d | ||
|
|
5acb3a62f8 | ||
|
|
8e08bd6af2 | ||
|
|
cc8a86a4bd | ||
|
|
71fe40aafa | ||
|
|
42bab370a7 | ||
|
|
81b259ab36 | ||
|
|
c896563af4 | ||
|
|
5ad88dec72 | ||
|
|
42a538edda | ||
|
|
ef6f5d947a | ||
|
|
fb20c4c3d4 | ||
|
|
ee3e2944ba | ||
|
|
9d114fb066 | ||
|
|
9b4e820bbe | ||
|
|
5fa64e83e0 | ||
|
|
124c4a3919 | ||
|
|
ef22178dee | ||
|
|
f5727ff0d0 | ||
|
|
757cfad38f | ||
|
|
8bc489dba8 | ||
|
|
8b2241916a | ||
|
|
d5e14eb715 | ||
|
|
2a6d79078e | ||
|
|
7ebfface22 | ||
|
|
de965c2bf5 | ||
|
|
2e12f6de9c | ||
|
|
3864fb37a0 | ||
|
|
1bf7aadf94 | ||
|
|
7bad27d215 | ||
|
|
ca92f37f18 | ||
|
|
81004369dc | ||
|
|
598057ee79 | ||
|
|
c31946f29f | ||
|
|
7fde57c89a | ||
|
|
c69b20c52c | ||
|
|
6622be9f46 | ||
|
|
63974ec46d | ||
|
|
b32ad3de37 | ||
|
|
b82a0dc6fd | ||
|
|
1d4e339141 | ||
|
|
dc67d2bd85 | ||
|
|
e3037b32d5 | ||
|
|
2793fc06d5 | ||
|
|
cd7357b93a | ||
|
|
b0ec22065e | ||
|
|
83611edb66 | ||
|
|
135a7e9e5a | ||
|
|
d940486552 | ||
|
|
4c97b8efdd | ||
|
|
60c1240849 | ||
|
|
02e21a8905 | ||
|
|
3381d1e089 | ||
|
|
e762f55847 | ||
|
|
4fcbe75d3b | ||
|
|
51df7496db | ||
|
|
b705173676 | ||
|
|
66b17f5afb | ||
|
|
a8945b5784 | ||
|
|
1a8d63d54f | ||
|
|
58274eafbc | ||
|
|
7b8db49efe | ||
|
|
3b6a2b9e85 | ||
|
|
cbbd31cc9f | ||
|
|
c786a85a9b | ||
|
|
a2df6e81b8 | ||
|
|
56f4ae3a83 | ||
|
|
127b3239e9 | ||
|
|
9f3551926a | ||
|
|
69da87f455 | ||
|
|
b84717fc76 | ||
|
|
f4a861d76e | ||
|
|
686e531624 | ||
|
|
3c2674725a | ||
|
|
d60689a983 | ||
|
|
20c2664a50 | ||
|
|
1765cdffb1 | ||
|
|
368fda7272 | ||
|
|
4fbc78ba81 | ||
|
|
c250b9fc02 | ||
|
|
185b554a56 | ||
|
|
fb024546ff | ||
|
|
1c312a158a | ||
|
|
829c2d4a1a | ||
|
|
91c473db57 | ||
|
|
8e9860077d | ||
|
|
e09f64b7c9 | ||
|
|
e5607822d3 | ||
|
|
275a754abd | ||
|
|
67abfea61a | ||
|
|
f4165755a9 | ||
|
|
a55e817ffe | ||
|
|
157801bc90 | ||
|
|
8457f70477 | ||
|
|
1d7f387122 | ||
|
|
cfe93f19b1 | ||
|
|
42463bae67 | ||
|
|
0c1c9284f6 | ||
|
|
6065329c0e | ||
|
|
15bfdf66b2 | ||
|
|
ac7588f735 | ||
|
|
212d308835 | ||
|
|
7bfb0f8133 | ||
|
|
2e6c28bd60 | ||
|
|
18bf3d3a7d | ||
|
|
ddb71e2aed | ||
|
|
1c64689807 | ||
|
|
c54e43360b | ||
|
|
09f3f2111c | ||
|
|
3c8d3ab078 | ||
|
|
bee6402d84 | ||
|
|
8d74fad474 | ||
|
|
95777230b0 | ||
|
|
631a0b1e13 | ||
|
|
417353da4d | ||
|
|
b35596f7c8 | ||
|
|
cc4dced7c0 | ||
|
|
18badbc60f | ||
|
|
7fb746e6e5 | ||
|
|
aac0374baf | ||
|
|
821b6c61cb | ||
|
|
c2afa984ff | ||
|
|
fdf81089b5 | ||
|
|
e4513f70ad | ||
|
|
86f10f6dd6 | ||
|
|
304229071f | ||
|
|
fed259a83b | ||
|
|
f6fcffbab5 | ||
|
|
ef9bb79e91 | ||
|
|
6ba3d89076 | ||
|
|
9eaf1e85e4 | ||
|
|
57dbb37a52 | ||
|
|
b610998491 | ||
|
|
5dec62f4c0 | ||
|
|
a30ec197c2 | ||
|
|
74f92387ac | ||
|
|
d3ac191a67 | ||
|
|
8f9819eb4c | ||
|
|
ffea48f218 | ||
|
|
90d59bb06c | ||
|
|
0542b129c2 | ||
|
|
1f74b051a8 | ||
|
|
a6f7213c89 | ||
|
|
5ea5f32479 | ||
|
|
d21e65ce47 | ||
|
|
65d48302ad | ||
|
|
ed3c141f5e | ||
|
|
208b16ed1b | ||
|
|
9f1a96ea9b | ||
|
|
930c416ea7 | ||
|
|
da6119e04c | ||
|
|
30310b9ab6 | ||
|
|
dea92a69dc | ||
|
|
23aaa75802 | ||
|
|
496fcf8005 | ||
|
|
a8da7d03b9 | ||
|
|
ef4394f87d | ||
|
|
1209585de5 | ||
|
|
a8b3cfa592 | ||
|
|
9b94fe1ce0 | ||
|
|
7bdaa793f6 | ||
|
|
0f3f2cbeea | ||
|
|
74b07a3218 | ||
|
|
af5964358d | ||
|
|
7cbe942a64 | ||
|
|
f1fac96e33 | ||
|
|
74d8ad2013 | ||
|
|
1ecadf057e | ||
|
|
7d79b4ba55 | ||
|
|
217c2a1cc5 | ||
|
|
40cf397ce6 | ||
|
|
4c565e5299 | ||
|
|
7afcacc9a1 | ||
|
|
d85805ab6d | ||
|
|
7f91ae8b67 | ||
|
|
c54c4cb215 | ||
|
|
3151672cfe | ||
|
|
27886e5aa4 | ||
|
|
d5ad269b35 | ||
|
|
42c3ee6eed | ||
|
|
8e299e80f8 | ||
|
|
296bd069c1 | ||
|
|
b20e92efd8 | ||
|
|
353737239a | ||
|
|
ec71eb2fcb | ||
|
|
bcd3a00eaf | ||
|
|
4fee632526 | ||
|
|
4591705b55 | ||
|
|
f2d5ab6f80 | ||
|
|
ea32147d02 | ||
|
|
d58edd10a7 | ||
|
|
1efdef3ac3 | ||
|
|
3e92196a2b | ||
|
|
68aef92b37 | ||
|
|
1952060e1a | ||
|
|
eb5356cc47 | ||
|
|
d38ef88686 | ||
|
|
ca84d4e2b6 | ||
|
|
4134a17a29 | ||
|
|
9da2f349e7 | ||
|
|
d4df44375b | ||
|
|
05c123f3ab | ||
|
|
99fc950510 | ||
|
|
c3e0b6e123 | ||
|
|
e405bd5f1f | ||
|
|
a31c19563f | ||
|
|
249d94a011 | ||
|
|
d299bfef26 | ||
|
|
0903ea949f | ||
|
|
e895a53713 | ||
|
|
1c553f51fb | ||
|
|
cc6edccc0c | ||
|
|
f8bea3bb05 |
12
.github/CONTRIBUTING.md
vendored
12
.github/CONTRIBUTING.md
vendored
@@ -1,5 +1,7 @@
|
||||
## Contributing to discord.py
|
||||
|
||||
Credits to the `original lib` by Rapptz <https://github.com/Rapptz/discord.py>
|
||||
|
||||
First off, thanks for taking the time to contribute. It makes the library substantially better. :+1:
|
||||
|
||||
The following is a set of guidelines for contributing to the repository. These are guidelines, not hard rules.
|
||||
@@ -8,9 +10,9 @@ The following is a set of guidelines for contributing to the repository. These a
|
||||
|
||||
Generally speaking questions are better suited in our resources below.
|
||||
|
||||
- The official support server: https://discord.gg/r3sSKJJ
|
||||
- The official support server: https://discord.gg/TvqYBrGXEm
|
||||
- The Discord API server under #python_discord-py: https://discord.gg/discord-api
|
||||
- [The FAQ in the documentation](https://discordpy.readthedocs.io/en/latest/faq.html)
|
||||
- [The FAQ in the documentation](https://enhanced-dpy.readthedocs.io/en/latest/faq.html)
|
||||
- [StackOverflow's `discord.py` tag](https://stackoverflow.com/questions/tagged/discord.py)
|
||||
|
||||
Please try your best not to ask questions in our issue tracker. Most of them don't belong there unless they provide value to a larger audience.
|
||||
@@ -32,13 +34,13 @@ If the bug report is missing this information then it'll take us longer to fix t
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows PEP-8 guidelines (mostly) with a column limit of 125.
|
||||
Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep, and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows the black code format, with a line length limit of `120`
|
||||
|
||||
### Git Commit Guidelines
|
||||
|
||||
- Use present tense (e.g. "Add feature" not "Added feature")
|
||||
- Limit all lines to 72 characters or less.
|
||||
- Reference issues or pull requests outside of the first line.
|
||||
- Limit all lines to 120 characters or fewer.
|
||||
- Reference issues or pull requests outside the first line.
|
||||
- Please use the shorthand `#123` and not the full URL.
|
||||
- Commits regarding the commands extension must be prefixed with `[commands]`
|
||||
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,13 +1,12 @@
|
||||
name: Bug Report
|
||||
description: Report broken or incorrect behaviour
|
||||
labels: unconfirmed bug
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for taking the time to fill out a bug.
|
||||
If you want real-time support, consider joining our Discord at https://discord.gg/r3sSKJJ instead.
|
||||
If you want real-time support, consider joining our Discord at https://discord.gg/TvqYBrGXEm instead.
|
||||
|
||||
Please note that this form is for bugs only!
|
||||
- type: input
|
||||
@@ -73,3 +72,7 @@ body:
|
||||
required: true
|
||||
- label: I have removed my token from display, if visible.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: If there is anything else to say, please do so here.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -5,4 +5,4 @@ contact_links:
|
||||
url: https://github.com/Rapptz/discord.py/discussions
|
||||
- name: Discord Server
|
||||
about: Use our official Discord server to ask for help and questions as well.
|
||||
url: https://discord.gg/r3sSKJJ
|
||||
url: https://discord.gg/TvqYBrGXEm
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: Feature Request
|
||||
description: Suggest a feature for this library
|
||||
labels: feature request
|
||||
issue_body: true
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
@@ -44,3 +43,7 @@ body:
|
||||
What is the current solution to the problem, if any?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: If there is anything else to say, please do so here.
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
||||
<!-- Pull requests that do not fill this information in will likely be closed -->
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- What is this pull request for? Does it fix any issues? -->
|
||||
|
||||
38
.github/workflows/ci.yml
vendored
Normal file
38
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup node.js (for pyright)
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "14"
|
||||
|
||||
- name: Run type checking
|
||||
run: |
|
||||
npm install -g pyright
|
||||
pip install .
|
||||
pyright --lib --verifytypes discord --ignoreexternal
|
||||
|
||||
black:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run linter
|
||||
uses: psf/black@stable
|
||||
with:
|
||||
options: "--line-length 120 --check"
|
||||
src: "./discord"
|
||||
1
.python-black
Normal file
1
.python-black
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -2,3 +2,4 @@ include README.rst
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include discord/bin/*.dll
|
||||
include discord/py.typed
|
||||
|
||||
114
README.ja.rst
114
README.ja.rst
@@ -1,114 +0,0 @@
|
||||
discord.py
|
||||
==========
|
||||
|
||||
.. image:: https://discord.com/api/guilds/336642139381301249/embed.png
|
||||
:target: https://discord.gg/nXzj3dg
|
||||
:alt: Discordサーバーの招待
|
||||
.. image:: https://img.shields.io/pypi/v/discord.py.svg
|
||||
:target: https://pypi.python.org/pypi/discord.py
|
||||
:alt: PyPIのバージョン情報
|
||||
.. image:: https://img.shields.io/pypi/pyversions/discord.py.svg
|
||||
:target: https://pypi.python.org/pypi/discord.py
|
||||
:alt: PyPIのサポートしているPythonのバージョン
|
||||
|
||||
discord.py は機能豊富かつモダンで使いやすい、非同期処理にも対応したDiscord用のAPIラッパーです。
|
||||
|
||||
主な特徴
|
||||
-------------
|
||||
|
||||
- ``async`` と ``await`` を使ったモダンなPythonらしいAPI。
|
||||
- 適切なレート制限処理
|
||||
- Discord APIによってサポートされているものを100%カバー。
|
||||
- メモリと速度の両方を最適化。
|
||||
|
||||
インストール
|
||||
-------------
|
||||
|
||||
**Python 3.5.3 以降のバージョンが必須です**
|
||||
|
||||
完全な音声サポートなしでライブラリをインストールする場合は次のコマンドを実行してください:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
# Linux/OS X
|
||||
python3 -m pip install -U discord.py
|
||||
|
||||
# Windows
|
||||
py -3 -m pip install -U discord.py
|
||||
|
||||
音声サポートが必要なら、次のコマンドを実行しましょう:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
# Linux/OS X
|
||||
python3 -m pip install -U discord.py[voice]
|
||||
|
||||
# Windows
|
||||
py -3 -m pip install -U discord.py[voice]
|
||||
|
||||
|
||||
開発版をインストールしたいのならば、次の手順に従ってください:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ git clone https://github.com/Rapptz/discord.py
|
||||
$ cd discord.py
|
||||
$ python3 -m pip install -U .[voice]
|
||||
|
||||
|
||||
オプションパッケージ
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* PyNaCl (音声サポート用)
|
||||
|
||||
Linuxで音声サポートを導入するには、前述のコマンドを実行する前にお気に入りのパッケージマネージャー(例えば ``apt`` や ``dnf`` など)を使って以下のパッケージをインストールする必要があります:
|
||||
|
||||
* libffi-dev (システムによっては ``libffi-devel``)
|
||||
* python-dev (例えばPython 3.6用の ``python3.6-dev``)
|
||||
|
||||
簡単な例
|
||||
--------------
|
||||
|
||||
.. code:: py
|
||||
|
||||
import discord
|
||||
|
||||
class MyClient(discord.Client):
|
||||
async def on_ready(self):
|
||||
print('Logged on as', self.user)
|
||||
|
||||
async def on_message(self, message):
|
||||
# don't respond to ourselves
|
||||
if message.author == self.user:
|
||||
return
|
||||
|
||||
if message.content == 'ping':
|
||||
await message.channel.send('pong')
|
||||
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
||||
Botの例
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. code:: py
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
bot = commands.Bot(command_prefix='>')
|
||||
|
||||
@bot.command()
|
||||
async def ping(ctx):
|
||||
await ctx.send('pong')
|
||||
|
||||
bot.run('token')
|
||||
|
||||
examplesディレクトリに更に多くのサンプルがあります。
|
||||
|
||||
リンク
|
||||
------
|
||||
|
||||
- `ドキュメント <https://discordpy.readthedocs.io/ja/latest/index.html>`_
|
||||
- `公式Discordサーバー <https://discord.gg/nXzj3dg>`_
|
||||
- `Discord API <https://discord.gg/discord-api>`_
|
||||
63
README.rst
63
README.rst
@@ -1,65 +1,72 @@
|
||||
discord.py
|
||||
==========
|
||||
enhanced-discord.py
|
||||
===================
|
||||
|
||||
.. image:: https://discord.com/api/guilds/336642139381301249/embed.png
|
||||
:target: https://discord.gg/r3sSKJJ
|
||||
.. image:: https://discord.com/api/guilds/514232441498763279/embed.png
|
||||
:target: https://discord.gg/TvqYBrGXEm
|
||||
:alt: Discord server invite
|
||||
.. image:: https://img.shields.io/pypi/v/discord.py.svg
|
||||
:target: https://pypi.python.org/pypi/discord.py
|
||||
.. image:: https://img.shields.io/pypi/v/enhanced-dpy.svg
|
||||
:target: https://pypi.python.org/pypi/enhanced-dpy
|
||||
:alt: PyPI version info
|
||||
.. image:: https://img.shields.io/pypi/pyversions/discord.py.svg
|
||||
:target: https://pypi.python.org/pypi/discord.py
|
||||
.. image:: https://img.shields.io/pypi/pyversions/enhanced-dpy.svg
|
||||
:target: https://pypi.python.org/pypi/enhanced-dpy
|
||||
:alt: PyPI supported Python versions
|
||||
|
||||
A modern, easy to use, feature-rich, and async ready API wrapper for Discord written in Python.
|
||||
A modern, maintained, easy to use, feature-rich, and async ready API wrapper for Discord written in Python.
|
||||
|
||||
The Future of enhanced-discord.py
|
||||
--------------------------
|
||||
|
||||
Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_)
|
||||
|
||||
It is currently maintained by (in alphabetical order)
|
||||
|
||||
- Chillymosh#8175
|
||||
- Daggy#9889
|
||||
- dank Had0cK#6081
|
||||
- Dutchy#6127
|
||||
- Eyesofcreeper#0001
|
||||
- Gnome!#6669
|
||||
- IAmTomahawkx#1000
|
||||
- Jadon#2494
|
||||
|
||||
An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_.
|
||||
|
||||
Key Features
|
||||
-------------
|
||||
|
||||
- Modern Pythonic API using ``async`` and ``await``.
|
||||
- Proper rate limit handling.
|
||||
- 100% coverage of the supported Discord API.
|
||||
- Optimised in both speed and memory.
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
**Python 3.5.3 or higher is required**
|
||||
**Python 3.8 or higher is required**
|
||||
|
||||
To install the library without full voice support, you can just run the following command:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
# Linux/macOS
|
||||
python3 -m pip install -U discord.py
|
||||
python3 -m pip install -U enhanced-dpy
|
||||
|
||||
# Windows
|
||||
py -3 -m pip install -U discord.py
|
||||
|
||||
Otherwise to get voice support you should run the following command:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
# Linux/macOS
|
||||
python3 -m pip install -U "discord.py[voice]"
|
||||
|
||||
# Windows
|
||||
py -3 -m pip install -U discord.py[voice]
|
||||
py -3 -m pip install -U enhanced-dpy
|
||||
|
||||
|
||||
To install the development version, do the following:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ git clone https://github.com/Rapptz/discord.py
|
||||
$ cd discord.py
|
||||
$ git clone https://github.com/iDevision/enhanced-discord.py
|
||||
$ cd enhanced-discord.py
|
||||
$ python3 -m pip install -U .[voice]
|
||||
|
||||
|
||||
Optional Packages
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* PyNaCl (for voice support)
|
||||
* `PyNaCl <https://pypi.org/project/PyNaCl/>`__ (for voice support)
|
||||
|
||||
Please note that on Linux installing voice you must install the following packages via your favourite package manager (e.g. ``apt``, ``dnf``, etc) before running the above commands:
|
||||
|
||||
@@ -109,6 +116,6 @@ You can find more examples in the examples directory.
|
||||
Links
|
||||
------
|
||||
|
||||
- `Documentation <https://discordpy.readthedocs.io/en/latest/index.html>`_
|
||||
- `Official Discord Server <https://discord.gg/r3sSKJJ>`_
|
||||
- `Documentation <https://enhanced-dpy.readthedocs.io/en/latest/index.html>`_
|
||||
- `Official Discord Server <https://discord.gg/TvqYBrGXEm>`_
|
||||
- `Discord API <https://discord.gg/discord-api>`_
|
||||
|
||||
@@ -9,16 +9,16 @@ A basic wrapper for the Discord API.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = 'discord'
|
||||
__author__ = 'Rapptz'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2015-present Rapptz'
|
||||
__version__ = '2.0.0a'
|
||||
__title__ = "discord"
|
||||
__author__ = "Rapptz"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2015-present Rapptz"
|
||||
__version__ = "2.0.0a"
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
from typing import NamedTuple, Literal
|
||||
|
||||
from .client import *
|
||||
from .appinfo import *
|
||||
@@ -43,7 +43,7 @@ from .template import *
|
||||
from .widget import *
|
||||
from .object import *
|
||||
from .reaction import *
|
||||
from . import utils, opus, abc
|
||||
from . import utils, opus, abc, ui
|
||||
from .enums import *
|
||||
from .embeds import *
|
||||
from .mentions import *
|
||||
@@ -55,10 +55,20 @@ from .audit_logs import *
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
from .sticker import *
|
||||
from .stage_instance import *
|
||||
from .interactions import *
|
||||
from .components import *
|
||||
from .threads import *
|
||||
|
||||
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
|
||||
|
||||
version_info = VersionInfo(major=2, minor=0, micro=0, releaselevel='alpha', serial=0)
|
||||
class VersionInfo(NamedTuple):
|
||||
major: int
|
||||
minor: int
|
||||
micro: int
|
||||
releaselevel: Literal["alpha", "beta", "candidate", "final"]
|
||||
serial: int
|
||||
|
||||
|
||||
version_info: VersionInfo = VersionInfo(major=2, minor=0, micro=0, releaselevel="alpha", serial=0)
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
|
||||
@@ -31,27 +31,30 @@ import pkg_resources
|
||||
import aiohttp
|
||||
import platform
|
||||
|
||||
|
||||
def show_version():
|
||||
entries = []
|
||||
|
||||
entries.append('- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(sys.version_info))
|
||||
entries.append("- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(sys.version_info))
|
||||
version_info = discord.version_info
|
||||
entries.append('- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(version_info))
|
||||
if version_info.releaselevel != 'final':
|
||||
pkg = pkg_resources.get_distribution('discord.py')
|
||||
entries.append("- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info))
|
||||
if version_info.releaselevel != "final":
|
||||
pkg = pkg_resources.get_distribution("discord.py")
|
||||
if pkg:
|
||||
entries.append(f' - discord.py pkg_resources: v{pkg.version}')
|
||||
entries.append(f" - discord.py pkg_resources: v{pkg.version}")
|
||||
|
||||
entries.append(f'- aiohttp v{aiohttp.__version__}')
|
||||
entries.append(f"- aiohttp v{aiohttp.__version__}")
|
||||
uname = platform.uname()
|
||||
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
|
||||
print('\n'.join(entries))
|
||||
entries.append("- system info: {0.system} {0.release} {0.version}".format(uname))
|
||||
print("\n".join(entries))
|
||||
|
||||
|
||||
def core(parser, args):
|
||||
if args.version:
|
||||
show_version()
|
||||
|
||||
bot_template = """#!/usr/bin/env python3
|
||||
|
||||
_bot_template = """#!/usr/bin/env python3
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
@@ -64,10 +67,10 @@ class Bot(commands.{base}):
|
||||
try:
|
||||
self.load_extension(cog)
|
||||
except Exception as exc:
|
||||
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
|
||||
print(f'Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}')
|
||||
|
||||
async def on_ready(self):
|
||||
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
|
||||
print(f'Logged on as {{self.user}} (ID: {{self.user.id}})')
|
||||
|
||||
|
||||
bot = Bot()
|
||||
@@ -77,7 +80,7 @@ bot = Bot()
|
||||
bot.run(config.token)
|
||||
"""
|
||||
|
||||
gitignore_template = """# Byte-compiled / optimized / DLL files
|
||||
_gitignore_template = """# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
@@ -107,7 +110,7 @@ var/
|
||||
config.py
|
||||
"""
|
||||
|
||||
cog_template = '''from discord.ext import commands
|
||||
_cog_template = '''from discord.ext import commands
|
||||
import discord
|
||||
|
||||
class {name}(commands.Cog{attrs}):
|
||||
@@ -120,7 +123,7 @@ def setup(bot):
|
||||
bot.add_cog({name}(bot))
|
||||
'''
|
||||
|
||||
cog_extras = '''
|
||||
_cog_extras = """
|
||||
def cog_unload(self):
|
||||
# clean up logic goes here
|
||||
pass
|
||||
@@ -149,44 +152,68 @@ cog_extras = '''
|
||||
# called after a command is called here
|
||||
pass
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
# certain file names and directory names are forbidden
|
||||
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
# although some of this doesn't apply to Linux, we might as well be consistent
|
||||
_base_table = {
|
||||
'<': '-',
|
||||
'>': '-',
|
||||
':': '-',
|
||||
'"': '-',
|
||||
"<": "-",
|
||||
">": "-",
|
||||
":": "-",
|
||||
'"': "-",
|
||||
# '/': '-', these are fine
|
||||
# '\\': '-',
|
||||
'|': '-',
|
||||
'?': '-',
|
||||
'*': '-',
|
||||
"|": "-",
|
||||
"?": "-",
|
||||
"*": "-",
|
||||
}
|
||||
|
||||
# NUL (0) and 1-31 are disallowed
|
||||
_base_table.update((chr(i), None) for i in range(32))
|
||||
|
||||
translation_table = str.maketrans(_base_table)
|
||||
_translation_table = str.maketrans(_base_table)
|
||||
|
||||
|
||||
def to_path(parser, name, *, replace_spaces=False):
|
||||
if isinstance(name, Path):
|
||||
return name
|
||||
|
||||
if sys.platform == 'win32':
|
||||
forbidden = ('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', \
|
||||
'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9')
|
||||
if sys.platform == "win32":
|
||||
forbidden = (
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
)
|
||||
if len(name) <= 4 and name.upper() in forbidden:
|
||||
parser.error('invalid directory name given, use a different one')
|
||||
parser.error("invalid directory name given, use a different one")
|
||||
|
||||
name = name.translate(translation_table)
|
||||
name = name.translate(_translation_table)
|
||||
if replace_spaces:
|
||||
name = name.replace(' ', '-')
|
||||
name = name.replace(" ", "-")
|
||||
return Path(name)
|
||||
|
||||
|
||||
def newbot(parser, args):
|
||||
new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
|
||||
|
||||
@@ -195,106 +222,114 @@ def newbot(parser, args):
|
||||
try:
|
||||
new_directory.mkdir(exist_ok=True, parents=True)
|
||||
except OSError as exc:
|
||||
parser.error(f'could not create our bot directory ({exc})')
|
||||
parser.error(f"could not create our bot directory ({exc})")
|
||||
|
||||
cogs = new_directory / 'cogs'
|
||||
cogs = new_directory / "cogs"
|
||||
|
||||
try:
|
||||
cogs.mkdir(exist_ok=True)
|
||||
init = cogs / '__init__.py'
|
||||
init = cogs / "__init__.py"
|
||||
init.touch()
|
||||
except OSError as exc:
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
print(f"warning: could not create cogs directory ({exc})")
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
|
||||
with open(str(new_directory / "config.py"), "w", encoding="utf-8") as fp:
|
||||
fp.write('token = "place your token here"\ncogs = []\n')
|
||||
except OSError as exc:
|
||||
parser.error(f'could not create config file ({exc})')
|
||||
parser.error(f"could not create config file ({exc})")
|
||||
|
||||
try:
|
||||
with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp:
|
||||
base = 'Bot' if not args.sharded else 'AutoShardedBot'
|
||||
fp.write(bot_template.format(base=base, prefix=args.prefix))
|
||||
with open(str(new_directory / "bot.py"), "w", encoding="utf-8") as fp:
|
||||
base = "Bot" if not args.sharded else "AutoShardedBot"
|
||||
fp.write(_bot_template.format(base=base, prefix=args.prefix))
|
||||
except OSError as exc:
|
||||
parser.error(f'could not create bot file ({exc})')
|
||||
parser.error(f"could not create bot file ({exc})")
|
||||
|
||||
if not args.no_git:
|
||||
try:
|
||||
with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
|
||||
fp.write(gitignore_template)
|
||||
with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp:
|
||||
fp.write(_gitignore_template)
|
||||
except OSError as exc:
|
||||
print(f'warning: could not create .gitignore file ({exc})')
|
||||
print(f"warning: could not create .gitignore file ({exc})")
|
||||
|
||||
print("successfully made bot at", new_directory)
|
||||
|
||||
print('successfully made bot at', new_directory)
|
||||
|
||||
def newcog(parser, args):
|
||||
cog_dir = to_path(parser, args.directory)
|
||||
try:
|
||||
cog_dir.mkdir(exist_ok=True)
|
||||
except OSError as exc:
|
||||
print(f'warning: could not create cogs directory ({exc})')
|
||||
print(f"warning: could not create cogs directory ({exc})")
|
||||
|
||||
directory = cog_dir / to_path(parser, args.name)
|
||||
directory = directory.with_suffix('.py')
|
||||
directory = directory.with_suffix(".py")
|
||||
try:
|
||||
with open(str(directory), 'w', encoding='utf-8') as fp:
|
||||
attrs = ''
|
||||
extra = cog_extras if args.full else ''
|
||||
with open(str(directory), "w", encoding="utf-8") as fp:
|
||||
attrs = ""
|
||||
extra = _cog_extras if args.full else ""
|
||||
if args.class_name:
|
||||
name = args.class_name
|
||||
else:
|
||||
name = str(directory.stem)
|
||||
if '-' in name or '_' in name:
|
||||
translation = str.maketrans('-_', ' ')
|
||||
name = name.translate(translation).title().replace(' ', '')
|
||||
if "-" in name or "_" in name:
|
||||
translation = str.maketrans("-_", " ")
|
||||
name = name.translate(translation).title().replace(" ", "")
|
||||
else:
|
||||
name = name.title()
|
||||
|
||||
if args.display_name:
|
||||
attrs += f', name="{args.display_name}"'
|
||||
if args.hide_commands:
|
||||
attrs += ', command_attrs=dict(hidden=True)'
|
||||
fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
|
||||
attrs += ", command_attrs=dict(hidden=True)"
|
||||
fp.write(_cog_template.format(name=name, extra=extra, attrs=attrs))
|
||||
except OSError as exc:
|
||||
parser.error(f'could not create cog file ({exc})')
|
||||
parser.error(f"could not create cog file ({exc})")
|
||||
else:
|
||||
print('successfully made cog at', directory)
|
||||
print("successfully made cog at", directory)
|
||||
|
||||
|
||||
def add_newbot_args(subparser):
|
||||
parser = subparser.add_parser('newbot', help='creates a command bot project quickly')
|
||||
parser = subparser.add_parser("newbot", help="creates a command bot project quickly")
|
||||
parser.set_defaults(func=newbot)
|
||||
|
||||
parser.add_argument('name', help='the bot project name')
|
||||
parser.add_argument('directory', help='the directory to place it in (default: .)', nargs='?', default=Path.cwd())
|
||||
parser.add_argument('--prefix', help='the bot prefix (default: $)', default='$', metavar='<prefix>')
|
||||
parser.add_argument('--sharded', help='whether to use AutoShardedBot', action='store_true')
|
||||
parser.add_argument('--no-git', help='do not create a .gitignore file', action='store_true', dest='no_git')
|
||||
parser.add_argument("name", help="the bot project name")
|
||||
parser.add_argument("directory", help="the directory to place it in (default: .)", nargs="?", default=Path.cwd())
|
||||
parser.add_argument("--prefix", help="the bot prefix (default: $)", default="$", metavar="<prefix>")
|
||||
parser.add_argument("--sharded", help="whether to use AutoShardedBot", action="store_true")
|
||||
parser.add_argument("--no-git", help="do not create a .gitignore file", action="store_true", dest="no_git")
|
||||
|
||||
|
||||
def add_newcog_args(subparser):
|
||||
parser = subparser.add_parser('newcog', help='creates a new cog template quickly')
|
||||
parser = subparser.add_parser("newcog", help="creates a new cog template quickly")
|
||||
parser.set_defaults(func=newcog)
|
||||
|
||||
parser.add_argument('name', help='the cog name')
|
||||
parser.add_argument('directory', help='the directory to place it in (default: cogs)', nargs='?', default=Path('cogs'))
|
||||
parser.add_argument('--class-name', help='the class name of the cog (default: <name>)', dest='class_name')
|
||||
parser.add_argument('--display-name', help='the cog name (default: <name>)')
|
||||
parser.add_argument('--hide-commands', help='whether to hide all commands in the cog', action='store_true')
|
||||
parser.add_argument('--full', help='add all special methods as well', action='store_true')
|
||||
parser.add_argument("name", help="the cog name")
|
||||
parser.add_argument(
|
||||
"directory", help="the directory to place it in (default: cogs)", nargs="?", default=Path("cogs")
|
||||
)
|
||||
parser.add_argument("--class-name", help="the class name of the cog (default: <name>)", dest="class_name")
|
||||
parser.add_argument("--display-name", help="the cog name (default: <name>)")
|
||||
parser.add_argument("--hide-commands", help="whether to hide all commands in the cog", action="store_true")
|
||||
parser.add_argument("--full", help="add all special methods as well", action="store_true")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(prog='discord', description='Tools for helping with discord.py')
|
||||
parser.add_argument('-v', '--version', action='store_true', help='shows the library version')
|
||||
parser = argparse.ArgumentParser(prog="discord", description="Tools for helping with discord.py")
|
||||
parser.add_argument("-v", "--version", action="store_true", help="shows the library version")
|
||||
parser.set_defaults(func=core)
|
||||
|
||||
subparser = parser.add_subparsers(dest='subcommand', title='subcommands')
|
||||
subparser = parser.add_subparsers(dest="subcommand", title="subcommands")
|
||||
add_newbot_args(subparser)
|
||||
add_newcog_args(subparser)
|
||||
return parser, parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
parser, args = parse_args()
|
||||
args.func(parser, args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
784
discord/abc.py
784
discord/abc.py
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union, overload
|
||||
|
||||
from .asset import Asset
|
||||
from .enums import ActivityType, try_enum
|
||||
@@ -31,12 +34,12 @@ from .partial_emoji import PartialEmoji
|
||||
from .utils import _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
'BaseActivity',
|
||||
'Activity',
|
||||
'Streaming',
|
||||
'Game',
|
||||
'Spotify',
|
||||
'CustomActivity',
|
||||
"BaseActivity",
|
||||
"Activity",
|
||||
"Streaming",
|
||||
"Game",
|
||||
"Spotify",
|
||||
"CustomActivity",
|
||||
)
|
||||
|
||||
"""If curious, this is the current schema for an activity.
|
||||
@@ -71,6 +74,9 @@ type: int
|
||||
sync_id: str
|
||||
session_id: str
|
||||
flags: int
|
||||
buttons: list[dict]
|
||||
label: str (max: 32)
|
||||
url: str (max: 512)
|
||||
|
||||
There are also activity flags which are mostly uninteresting for the library atm.
|
||||
|
||||
@@ -84,6 +90,16 @@ t.ActivityFlags = {
|
||||
}
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.activity import (
|
||||
Activity as ActivityPayload,
|
||||
ActivityTimestamps,
|
||||
ActivityParty,
|
||||
ActivityAssets,
|
||||
ActivityButton,
|
||||
)
|
||||
|
||||
|
||||
class BaseActivity:
|
||||
"""The base activity that all user-settable activities inherit from.
|
||||
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
|
||||
@@ -102,19 +118,24 @@ class BaseActivity:
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
__slots__ = ('_created_at',)
|
||||
|
||||
__slots__ = ("_created_at",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._created_at = kwargs.pop('created_at', None)
|
||||
self._created_at: Optional[float] = kwargs.pop("created_at", None)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._created_at is not None:
|
||||
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
|
||||
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
|
||||
|
||||
def to_dict(self) -> ActivityPayload:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Activity(BaseActivity):
|
||||
"""Represents an activity in Discord.
|
||||
@@ -130,17 +151,17 @@ class Activity(BaseActivity):
|
||||
|
||||
Attributes
|
||||
------------
|
||||
application_id: :class:`int`
|
||||
application_id: Optional[:class:`int`]
|
||||
The application ID of the game.
|
||||
name: :class:`str`
|
||||
name: Optional[:class:`str`]
|
||||
The name of the activity.
|
||||
url: :class:`str`
|
||||
url: Optional[:class:`str`]
|
||||
A stream URL that the activity could be doing.
|
||||
type: :class:`ActivityType`
|
||||
The type of activity currently being done.
|
||||
state: :class:`str`
|
||||
state: Optional[:class:`str`]
|
||||
The user's current state. For example, "In Game".
|
||||
details: :class:`str`
|
||||
details: Optional[:class:`str`]
|
||||
The detail of the user's current activity.
|
||||
timestamps: :class:`dict`
|
||||
A dictionary of timestamps. It contains the following optional keys:
|
||||
@@ -164,52 +185,75 @@ class Activity(BaseActivity):
|
||||
|
||||
- ``id``: A string representing the party ID.
|
||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
||||
buttons: List[:class:`dict`]
|
||||
An list of dictionaries representing custom buttons shown in a rich presence.
|
||||
Each dictionary contains the following keys:
|
||||
|
||||
- ``label``: A string representing the text shown on the button.
|
||||
- ``url``: A string representing the URL opened upon clicking the button.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
emoji: Optional[:class:`PartialEmoji`]
|
||||
The emoji that belongs to this activity.
|
||||
"""
|
||||
|
||||
__slots__ = ('state', 'details', '_created_at', 'timestamps', 'assets', 'party',
|
||||
'flags', 'sync_id', 'session_id', 'type', 'name', 'url',
|
||||
'application_id', 'emoji')
|
||||
__slots__ = (
|
||||
"state",
|
||||
"details",
|
||||
"_created_at",
|
||||
"timestamps",
|
||||
"assets",
|
||||
"party",
|
||||
"flags",
|
||||
"sync_id",
|
||||
"session_id",
|
||||
"type",
|
||||
"name",
|
||||
"url",
|
||||
"application_id",
|
||||
"emoji",
|
||||
"buttons",
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.state = kwargs.pop('state', None)
|
||||
self.details = kwargs.pop('details', None)
|
||||
self.timestamps = kwargs.pop('timestamps', {})
|
||||
self.assets = kwargs.pop('assets', {})
|
||||
self.party = kwargs.pop('party', {})
|
||||
self.application_id = _get_as_snowflake(kwargs, 'application_id')
|
||||
self.name = kwargs.pop('name', None)
|
||||
self.url = kwargs.pop('url', None)
|
||||
self.flags = kwargs.pop('flags', 0)
|
||||
self.sync_id = kwargs.pop('sync_id', None)
|
||||
self.session_id = kwargs.pop('session_id', None)
|
||||
self.state: Optional[str] = kwargs.pop("state", None)
|
||||
self.details: Optional[str] = kwargs.pop("details", None)
|
||||
self.timestamps: ActivityTimestamps = kwargs.pop("timestamps", {})
|
||||
self.assets: ActivityAssets = kwargs.pop("assets", {})
|
||||
self.party: ActivityParty = kwargs.pop("party", {})
|
||||
self.application_id: Optional[int] = _get_as_snowflake(kwargs, "application_id")
|
||||
self.name: Optional[str] = kwargs.pop("name", None)
|
||||
self.url: Optional[str] = kwargs.pop("url", None)
|
||||
self.flags: int = kwargs.pop("flags", 0)
|
||||
self.sync_id: Optional[str] = kwargs.pop("sync_id", None)
|
||||
self.session_id: Optional[str] = kwargs.pop("session_id", None)
|
||||
self.buttons: List[ActivityButton] = kwargs.pop("buttons", [])
|
||||
|
||||
activity_type = kwargs.pop('type', -1)
|
||||
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
|
||||
|
||||
emoji = kwargs.pop('emoji', None)
|
||||
if emoji is not None:
|
||||
self.emoji = PartialEmoji.from_dict(emoji)
|
||||
else:
|
||||
self.emoji = None
|
||||
|
||||
def __repr__(self):
|
||||
attrs = (
|
||||
('type', self.type),
|
||||
('name', self.name),
|
||||
('url', self.url),
|
||||
('details', self.details),
|
||||
('application_id', self.application_id),
|
||||
('session_id', self.session_id),
|
||||
('emoji', self.emoji),
|
||||
activity_type = kwargs.pop("type", -1)
|
||||
self.type: ActivityType = (
|
||||
activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
|
||||
)
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<Activity {inner}>'
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
emoji = kwargs.pop("emoji", None)
|
||||
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = (
|
||||
("type", self.type),
|
||||
("name", self.name),
|
||||
("url", self.url),
|
||||
("details", self.details),
|
||||
("application_id", self.application_id),
|
||||
("session_id", self.session_id),
|
||||
("emoji", self.emoji),
|
||||
)
|
||||
inner = " ".join("%s=%r" % t for t in attrs)
|
||||
return f"<Activity {inner}>"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
ret: Dict[str, Any] = {}
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr, None)
|
||||
if value is None:
|
||||
@@ -219,65 +263,66 @@ class Activity(BaseActivity):
|
||||
continue
|
||||
|
||||
ret[attr] = value
|
||||
ret['type'] = int(self.type)
|
||||
ret["type"] = int(self.type)
|
||||
if self.emoji:
|
||||
ret['emoji'] = self.emoji.to_dict()
|
||||
ret["emoji"] = self.emoji.to_dict()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
def start(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps['start'] / 1000
|
||||
timestamp = self.timestamps["start"] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
def end(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
timestamp = self.timestamps['end'] / 1000
|
||||
timestamp = self.timestamps["end"] / 1000
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def large_image_url(self):
|
||||
def large_image_url(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
large_image = self.assets['large_image']
|
||||
large_image = self.assets["large_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{large_image}.png'
|
||||
return Asset.BASE + f"/app-assets/{self.application_id}/{large_image}.png"
|
||||
|
||||
@property
|
||||
def small_image_url(self):
|
||||
def small_image_url(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable."""
|
||||
if self.application_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
small_image = self.assets['small_image']
|
||||
small_image = self.assets["small_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
|
||||
@property
|
||||
def large_image_text(self):
|
||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
||||
return self.assets.get('large_text', None)
|
||||
return Asset.BASE + f"/app-assets/{self.application_id}/{small_image}.png"
|
||||
|
||||
@property
|
||||
def small_image_text(self):
|
||||
def large_image_text(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
||||
return self.assets.get("large_text", None)
|
||||
|
||||
@property
|
||||
def small_image_text(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
|
||||
return self.assets.get('small_text', None)
|
||||
return self.assets.get("small_text", None)
|
||||
|
||||
|
||||
class Game(BaseActivity):
|
||||
@@ -314,23 +359,23 @@ class Game(BaseActivity):
|
||||
The game's name.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', '_end', '_start')
|
||||
__slots__ = ("name", "_end", "_start")
|
||||
|
||||
def __init__(self, name, **extra):
|
||||
def __init__(self, name: str, **extra):
|
||||
super().__init__(**extra)
|
||||
self.name = name
|
||||
self.name: str = name
|
||||
|
||||
try:
|
||||
timestamps = extra['timestamps']
|
||||
timestamps: ActivityTimestamps = extra["timestamps"]
|
||||
except KeyError:
|
||||
self._start = 0
|
||||
self._end = 0
|
||||
else:
|
||||
self._start = timestamps.get('start', 0)
|
||||
self._end = timestamps.get('end', 0)
|
||||
self._start = timestamps.get("start", 0)
|
||||
self._end = timestamps.get("end", 0)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.playing`.
|
||||
@@ -338,48 +383,51 @@ class Game(BaseActivity):
|
||||
return ActivityType.playing
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
def start(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
|
||||
if self._start:
|
||||
return datetime.datetime.utcfromtimestamp(self._start / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
return datetime.datetime.fromtimestamp(self._start / 1000, tz=datetime.timezone.utc)
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
def end(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
|
||||
if self._end:
|
||||
return datetime.datetime.utcfromtimestamp(self._end / 1000).replace(tzinfo=datetime.timezone.utc)
|
||||
return datetime.datetime.fromtimestamp(self._end / 1000, tz=datetime.timezone.utc)
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Game name={self.name!r}>'
|
||||
def __repr__(self) -> str:
|
||||
return f"<Game name={self.name!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
timestamps = {}
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
timestamps: Dict[str, Any] = {}
|
||||
if self._start:
|
||||
timestamps['start'] = self._start
|
||||
timestamps["start"] = self._start
|
||||
|
||||
if self._end:
|
||||
timestamps['end'] = self._end
|
||||
timestamps["end"] = self._end
|
||||
|
||||
# fmt: off
|
||||
return {
|
||||
'type': ActivityType.playing.value,
|
||||
'name': str(self.name),
|
||||
'timestamps': timestamps
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Game) and other.name == self.name
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Streaming(BaseActivity):
|
||||
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
||||
|
||||
@@ -405,7 +453,7 @@ class Streaming(BaseActivity):
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
platform: :class:`str`
|
||||
platform: Optional[:class:`str`]
|
||||
Where the user is streaming from (ie. YouTube, Twitch).
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -425,30 +473,30 @@ class Streaming(BaseActivity):
|
||||
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
|
||||
"""
|
||||
|
||||
__slots__ = ('platform', 'name', 'game', 'url', 'details', 'assets')
|
||||
__slots__ = ("platform", "name", "game", "url", "details", "assets")
|
||||
|
||||
def __init__(self, *, name, url, **extra):
|
||||
def __init__(self, *, name: Optional[str], url: str, **extra: Any):
|
||||
super().__init__(**extra)
|
||||
self.platform = name
|
||||
self.name = extra.pop('details', name)
|
||||
self.game = extra.pop('state', None)
|
||||
self.url = url
|
||||
self.details = extra.pop('details', self.name) # compatibility
|
||||
self.assets = extra.pop('assets', {})
|
||||
self.platform: Optional[str] = name
|
||||
self.name: Optional[str] = extra.pop("details", name)
|
||||
self.game: Optional[str] = extra.pop("state", None)
|
||||
self.url: str = url
|
||||
self.details: Optional[str] = extra.pop("details", self.name) # compatibility
|
||||
self.assets: ActivityAssets = extra.pop("assets", {})
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.streaming`.
|
||||
"""
|
||||
return ActivityType.streaming
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Streaming name={self.name!r}>'
|
||||
def __repr__(self) -> str:
|
||||
return f"<Streaming name={self.name!r}>"
|
||||
|
||||
@property
|
||||
def twitch_name(self):
|
||||
@@ -459,32 +507,35 @@ class Streaming(BaseActivity):
|
||||
"""
|
||||
|
||||
try:
|
||||
name = self.assets['large_image']
|
||||
name = self.assets["large_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return name[7:] if name[:7] == 'twitch:' else None
|
||||
return name[7:] if name[:7] == "twitch:" else None
|
||||
|
||||
def to_dict(self):
|
||||
ret = {
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
# fmt: off
|
||||
ret: Dict[str, Any] = {
|
||||
'type': ActivityType.streaming.value,
|
||||
'name': str(self.name),
|
||||
'url': str(self.url),
|
||||
'assets': self.assets
|
||||
}
|
||||
# fmt: on
|
||||
if self.details:
|
||||
ret['details'] = self.details
|
||||
ret["details"] = self.details
|
||||
return ret
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Spotify:
|
||||
"""Represents a Spotify listening activity from Discord. This is a special case of
|
||||
:class:`Activity` that makes it easier to work with the Spotify integration.
|
||||
@@ -508,21 +559,20 @@ class Spotify:
|
||||
Returns the string 'Spotify'.
|
||||
"""
|
||||
|
||||
__slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id',
|
||||
'_created_at')
|
||||
__slots__ = ("_state", "_details", "_timestamps", "_assets", "_party", "_sync_id", "_session_id", "_created_at")
|
||||
|
||||
def __init__(self, **data):
|
||||
self._state = data.pop('state', None)
|
||||
self._details = data.pop('details', None)
|
||||
self._timestamps = data.pop('timestamps', {})
|
||||
self._assets = data.pop('assets', {})
|
||||
self._party = data.pop('party', {})
|
||||
self._sync_id = data.pop('sync_id')
|
||||
self._session_id = data.pop('session_id')
|
||||
self._created_at = data.pop('created_at', None)
|
||||
self._state: str = data.pop("state", "")
|
||||
self._details: str = data.pop("details", "")
|
||||
self._timestamps: Dict[str, int] = data.pop("timestamps", {})
|
||||
self._assets: ActivityAssets = data.pop("assets", {})
|
||||
self._party: ActivityParty = data.pop("party", {})
|
||||
self._sync_id: str = data.pop("sync_id")
|
||||
self._session_id: str = data.pop("session_id")
|
||||
self._created_at: Optional[float] = data.pop("created_at", None)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.listening`.
|
||||
@@ -530,74 +580,78 @@ class Spotify:
|
||||
return ActivityType.listening
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started listening in UTC.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._created_at is not None:
|
||||
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
|
||||
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
def colour(self) -> Colour:
|
||||
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :attr:`color`"""
|
||||
return Colour(0x1db954)
|
||||
return Colour(0x1DB954)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
def color(self) -> Colour:
|
||||
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :attr:`colour`"""
|
||||
return self.colour
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'flags': 48, # SYNC | PLAY
|
||||
'name': 'Spotify',
|
||||
'assets': self._assets,
|
||||
'party': self._party,
|
||||
'sync_id': self._sync_id,
|
||||
'session_id': self._session_id,
|
||||
'timestamps': self._timestamps,
|
||||
'details': self._details,
|
||||
'state': self._state
|
||||
"flags": 48, # SYNC | PLAY
|
||||
"name": "Spotify",
|
||||
"assets": self._assets,
|
||||
"party": self._party,
|
||||
"sync_id": self._sync_id,
|
||||
"session_id": self._session_id,
|
||||
"timestamps": self._timestamps,
|
||||
"details": self._details,
|
||||
"state": self._state,
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""":class:`str`: The activity's name. This will always return "Spotify"."""
|
||||
return 'Spotify'
|
||||
return "Spotify"
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, Spotify) and other._session_id == self._session_id
|
||||
and other._sync_id == self._sync_id and other.start == self.start)
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
isinstance(other, Spotify)
|
||||
and other._session_id == self._session_id
|
||||
and other._sync_id == self._sync_id
|
||||
and other.start == self.start
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._session_id)
|
||||
|
||||
def __str__(self):
|
||||
return 'Spotify'
|
||||
def __str__(self) -> str:
|
||||
return "Spotify"
|
||||
|
||||
def __repr__(self):
|
||||
return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>"
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
""":class:`str`: The title of the song being played."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
def artists(self) -> List[str]:
|
||||
"""List[:class:`str`]: The artists of the song being played."""
|
||||
return self._state.split('; ')
|
||||
return self._state.split("; ")
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
def artist(self) -> str:
|
||||
""":class:`str`: The artist of the song being played.
|
||||
|
||||
This does not attempt to split the artist information into
|
||||
@@ -606,43 +660,52 @@ class Spotify:
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def album(self):
|
||||
def album(self) -> str:
|
||||
""":class:`str`: The album that the song being played belongs to."""
|
||||
return self._assets.get('large_text', '')
|
||||
return self._assets.get("large_text", "")
|
||||
|
||||
@property
|
||||
def album_cover_url(self):
|
||||
def album_cover_url(self) -> str:
|
||||
""":class:`str`: The album cover image URL from Spotify's CDN."""
|
||||
large_image = self._assets.get('large_image', '')
|
||||
if large_image[:8] != 'spotify:':
|
||||
return ''
|
||||
large_image = self._assets.get("large_image", "")
|
||||
if large_image[:8] != "spotify:":
|
||||
return ""
|
||||
album_image_id = large_image[8:]
|
||||
return 'https://i.scdn.co/image/' + album_image_id
|
||||
return "https://i.scdn.co/image/" + album_image_id
|
||||
|
||||
@property
|
||||
def track_id(self):
|
||||
def track_id(self) -> str:
|
||||
""":class:`str`: The track ID used by Spotify to identify this song."""
|
||||
return self._sync_id
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
def track_url(self) -> str:
|
||||
""":class:`str`: The track URL to listen on Spotify.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return f"https://open.spotify.com/track/{self.track_id}"
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps['start'] / 1000)
|
||||
return datetime.datetime.fromtimestamp(self._timestamps["start"] / 1000, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
def end(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps['end'] / 1000)
|
||||
return datetime.datetime.fromtimestamp(self._timestamps["end"] / 1000, tz=datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
def duration(self) -> datetime.timedelta:
|
||||
""":class:`datetime.timedelta`: The duration of the song being played."""
|
||||
return self.end - self.start
|
||||
|
||||
@property
|
||||
def party_id(self):
|
||||
def party_id(self) -> str:
|
||||
""":class:`str`: The party ID of the listening party."""
|
||||
return self._party.get('id', '')
|
||||
return self._party.get("id", "")
|
||||
|
||||
|
||||
class CustomActivity(BaseActivity):
|
||||
"""Represents a Custom activity from Discord.
|
||||
@@ -675,15 +738,16 @@ class CustomActivity(BaseActivity):
|
||||
The emoji to pass to the activity, if any.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'emoji', 'state')
|
||||
__slots__ = ("name", "emoji", "state")
|
||||
|
||||
def __init__(self, name, *, emoji=None, **extra):
|
||||
def __init__(self, name: Optional[str], *, emoji: Optional[PartialEmoji] = None, **extra: Any):
|
||||
super().__init__(**extra)
|
||||
self.name = name
|
||||
self.state = extra.pop('state', None)
|
||||
if self.name == 'Custom Status':
|
||||
self.name: Optional[str] = name
|
||||
self.state: Optional[str] = extra.pop("state", None)
|
||||
if self.name == "Custom Status":
|
||||
self.name = self.state
|
||||
|
||||
self.emoji: Optional[PartialEmoji]
|
||||
if emoji is None:
|
||||
self.emoji = emoji
|
||||
elif isinstance(emoji, dict):
|
||||
@@ -693,74 +757,89 @@ class CustomActivity(BaseActivity):
|
||||
elif isinstance(emoji, PartialEmoji):
|
||||
self.emoji = emoji
|
||||
else:
|
||||
raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
|
||||
raise TypeError(f"Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.")
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self) -> ActivityType:
|
||||
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.custom`.
|
||||
"""
|
||||
return ActivityType.custom
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
if self.name == self.state:
|
||||
o = {
|
||||
'type': ActivityType.custom.value,
|
||||
'state': self.name,
|
||||
'name': 'Custom Status',
|
||||
"type": ActivityType.custom.value,
|
||||
"state": self.name,
|
||||
"name": "Custom Status",
|
||||
}
|
||||
else:
|
||||
o = {
|
||||
'type': ActivityType.custom.value,
|
||||
'name': self.name,
|
||||
"type": ActivityType.custom.value,
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
if self.emoji:
|
||||
o['emoji'] = self.emoji.to_dict()
|
||||
o["emoji"] = self.emoji.to_dict()
|
||||
return o
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji)
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, str(self.emoji)))
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.emoji:
|
||||
if self.name:
|
||||
return f'{self.emoji} {self.name}'
|
||||
return f"{self.emoji} {self.name}"
|
||||
return str(self.emoji)
|
||||
else:
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<CustomActivity name={0.name!r} emoji={0.emoji!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<CustomActivity name={self.name!r} emoji={self.emoji!r}>"
|
||||
|
||||
|
||||
def create_activity(data):
|
||||
ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify]
|
||||
|
||||
|
||||
@overload
|
||||
def create_activity(data: ActivityPayload) -> ActivityTypes:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def create_activity(data: None) -> None:
|
||||
...
|
||||
|
||||
|
||||
def create_activity(data: Optional[ActivityPayload]) -> Optional[ActivityTypes]:
|
||||
if not data:
|
||||
return None
|
||||
|
||||
game_type = try_enum(ActivityType, data.get('type', -1))
|
||||
game_type = try_enum(ActivityType, data.get("type", -1))
|
||||
if game_type is ActivityType.playing:
|
||||
if 'application_id' in data or 'session_id' in data:
|
||||
if "application_id" in data or "session_id" in data:
|
||||
return Activity(**data)
|
||||
return Game(**data)
|
||||
elif game_type is ActivityType.custom:
|
||||
try:
|
||||
name = data.pop('name')
|
||||
name = data.pop("name")
|
||||
except KeyError:
|
||||
return Activity(**data)
|
||||
else:
|
||||
return CustomActivity(name=name, **data)
|
||||
# we removed the name key from data already
|
||||
return CustomActivity(name=name, **data) # type: ignore
|
||||
elif game_type is ActivityType.streaming:
|
||||
if 'url' in data:
|
||||
return Streaming(**data)
|
||||
if "url" in data:
|
||||
# the url won't be None here
|
||||
return Streaming(**data) # type: ignore
|
||||
return Activity(**data)
|
||||
elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data:
|
||||
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
|
||||
return Spotify(**data)
|
||||
return Activity(**data)
|
||||
|
||||
@@ -22,15 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, TYPE_CHECKING, Optional
|
||||
|
||||
from . import utils
|
||||
from .user import User
|
||||
from .asset import Asset
|
||||
from .team import Team
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .guild import Guild
|
||||
from .types.appinfo import (
|
||||
AppInfo as AppInfoPayload,
|
||||
PartialAppInfo as PartialAppInfoPayload,
|
||||
Team as TeamPayload,
|
||||
)
|
||||
from .user import User
|
||||
from .state import ConnectionState
|
||||
|
||||
__all__ = (
|
||||
'AppInfo',
|
||||
"AppInfo",
|
||||
"PartialAppInfo",
|
||||
)
|
||||
|
||||
|
||||
class AppInfo:
|
||||
"""Represents the application info for the bot provided by Discord.
|
||||
|
||||
@@ -48,9 +62,7 @@ class AppInfo:
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
icon: Optional[:class:`str`]
|
||||
The icon hash, if it exists.
|
||||
description: Optional[:class:`str`]
|
||||
description: :class:`str`
|
||||
The application description.
|
||||
bot_public: :class:`bool`
|
||||
Whether the bot can be invited by anyone or if it is locked
|
||||
@@ -62,155 +74,186 @@ class AppInfo:
|
||||
A list of RPC origin URLs, if RPC is enabled.
|
||||
summary: :class:`str`
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the summary field for the store page of its primary SKU
|
||||
this field will be the summary field for the store page of its primary SKU.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
verify_key: :class:`str`
|
||||
The base64 encoded key for the GameSDK's GetTicket
|
||||
The hex encoded key for verification in interactions and the
|
||||
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
guild_id: Optional[:class:`int`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the guild to which it has been linked
|
||||
this field will be the guild to which it has been linked to.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
primary_sku_id: Optional[:class:`int`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the id of the "Game SKU" that is created, if exists
|
||||
this field will be the id of the "Game SKU" that is created,
|
||||
if it exists.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
slug: Optional[:class:`str`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the URL slug that links to the store page
|
||||
this field will be the URL slug that links to the store page.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
cover_image: Optional[:class:`str`]
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the hash of the image on store embeds
|
||||
terms_of_service_url: Optional[:class:`str`]
|
||||
The application's terms of service URL, if set.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
.. versionadded:: 2.0
|
||||
|
||||
privacy_policy_url: Optional[:class:`str`]
|
||||
The application's privacy policy URL, if set.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
__slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins',
|
||||
'bot_public', 'bot_require_code_grant', 'owner', 'icon',
|
||||
'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id',
|
||||
'slug', 'cover_image')
|
||||
|
||||
def __init__(self, state, data):
|
||||
self._state = state
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"description",
|
||||
"id",
|
||||
"name",
|
||||
"rpc_origins",
|
||||
"bot_public",
|
||||
"bot_require_code_grant",
|
||||
"owner",
|
||||
"_icon",
|
||||
"summary",
|
||||
"verify_key",
|
||||
"team",
|
||||
"guild_id",
|
||||
"primary_sku_id",
|
||||
"slug",
|
||||
"_cover_image",
|
||||
"terms_of_service_url",
|
||||
"privacy_policy_url",
|
||||
)
|
||||
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.description = data['description']
|
||||
self.icon = data['icon']
|
||||
self.rpc_origins = data['rpc_origins']
|
||||
self.bot_public = data['bot_public']
|
||||
self.bot_require_code_grant = data['bot_require_code_grant']
|
||||
self.owner = User(state=self._state, data=data['owner'])
|
||||
def __init__(self, state: ConnectionState, data: AppInfoPayload):
|
||||
from .team import Team
|
||||
|
||||
team = data.get('team')
|
||||
self.team = Team(state, team) if team else None
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self.description: str = data["description"]
|
||||
self._icon: Optional[str] = data["icon"]
|
||||
self.rpc_origins: List[str] = data["rpc_origins"]
|
||||
self.bot_public: bool = data["bot_public"]
|
||||
self.bot_require_code_grant: bool = data["bot_require_code_grant"]
|
||||
self.owner: User = state.create_user(data["owner"])
|
||||
|
||||
self.summary = data['summary']
|
||||
self.verify_key = data['verify_key']
|
||||
team: Optional[TeamPayload] = data.get("team")
|
||||
self.team: Optional[Team] = Team(state, team) if team else None
|
||||
|
||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
self.summary: str = data["summary"]
|
||||
self.verify_key: str = data["verify_key"]
|
||||
|
||||
self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id')
|
||||
self.slug = data.get('slug')
|
||||
self.cover_image = data.get('cover_image')
|
||||
self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id")
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} description={0.description!r} public={0.bot_public} ' \
|
||||
'owner={0.owner!r}>'.format(self)
|
||||
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, "primary_sku_id")
|
||||
self.slug: Optional[str] = data.get("slug")
|
||||
self._cover_image: Optional[str] = data.get("cover_image")
|
||||
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
|
||||
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
|
||||
f"description={self.description!r} public={self.bot_public} "
|
||||
f"owner={self.owner!r}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`.Asset`: Retrieves the application's icon asset.
|
||||
|
||||
This is equivalent to calling :meth:`icon_url_as` with
|
||||
the default parameters ('webp' format and a size of 1024).
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
return self.icon_url_as()
|
||||
|
||||
def icon_url_as(self, *, format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the icon the application has.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: :class:`str`
|
||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_icon(self._state, self, 'app', format=format, size=size)
|
||||
|
||||
def icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_icon(self._state, self.id, self._icon, path="app")
|
||||
|
||||
@property
|
||||
def cover_image_url(self):
|
||||
""":class:`.Asset`: Retrieves the cover image on a store embed.
|
||||
def cover_image(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any.
|
||||
|
||||
This is equivalent to calling :meth:`cover_image_url_as` with
|
||||
the default parameters ('webp' format and a size of 1024).
|
||||
|
||||
.. versionadded:: 1.3
|
||||
This is only available if the application is a game sold on Discord.
|
||||
"""
|
||||
return self.cover_image_url_as()
|
||||
|
||||
def cover_image_url_as(self, *, format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the image on store embeds
|
||||
if this application is a game sold on Discord.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: :class:`str`
|
||||
The format to attempt to convert the image to. Defaults to 'webp'.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_cover_image(self._state, self, format=format, size=size)
|
||||
if self._cover_image is None:
|
||||
return None
|
||||
return Asset._from_cover_image(self._state, self.id, self._cover_image)
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
def guild(self) -> Optional[Guild]:
|
||||
"""Optional[:class:`Guild`]: If this application is a game sold on Discord,
|
||||
this field will be the guild to which it has been linked
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
return self._state._get_guild(int(self.guild_id))
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
|
||||
class PartialAppInfo:
|
||||
"""Represents a partial AppInfo given by :func:`~discord.abc.GuildChannel.create_invite`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-------------
|
||||
id: :class:`int`
|
||||
The application ID.
|
||||
name: :class:`str`
|
||||
The application name.
|
||||
description: :class:`str`
|
||||
The application description.
|
||||
rpc_origins: Optional[List[:class:`str`]]
|
||||
A list of RPC origin URLs, if RPC is enabled.
|
||||
summary: :class:`str`
|
||||
If this application is a game sold on Discord,
|
||||
this field will be the summary field for the store page of its primary SKU.
|
||||
verify_key: :class:`str`
|
||||
The hex encoded key for verification in interactions and the
|
||||
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
|
||||
terms_of_service_url: Optional[:class:`str`]
|
||||
The application's terms of service URL, if set.
|
||||
privacy_policy_url: Optional[:class:`str`]
|
||||
The application's privacy policy URL, if set.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"rpc_origins",
|
||||
"summary",
|
||||
"verify_key",
|
||||
"terms_of_service_url",
|
||||
"privacy_policy_url",
|
||||
"_icon",
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self._icon: Optional[str] = data.get("icon")
|
||||
self.description: str = data["description"]
|
||||
self.rpc_origins: Optional[List[str]] = data.get("rpc_origins")
|
||||
self.summary: str = data["summary"]
|
||||
self.verify_key: str = data["verify_key"]
|
||||
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
|
||||
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>"
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_icon(self._state, self.id, self._icon, path="app")
|
||||
|
||||
479
discord/asset.py
479
discord/asset.py
@@ -22,19 +22,100 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from typing import Any, Literal, Optional, TYPE_CHECKING, Tuple, Union
|
||||
from .errors import DiscordException
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'Asset',
|
||||
)
|
||||
import yarl
|
||||
|
||||
__all__ = ("Asset",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"]
|
||||
ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"]
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
class Asset:
|
||||
|
||||
MISSING = utils.MISSING
|
||||
|
||||
|
||||
class AssetMixin:
|
||||
url: str
|
||||
_state: Optional[Any]
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if self._state is None:
|
||||
raise DiscordException("Invalid state (no ConnectionState provided)")
|
||||
|
||||
return await self._state.http.get_from_cdn(self.url)
|
||||
|
||||
async def save(self, fp: Union[str, bytes, os.PathLike, io.BufferedIOBase], *, seek_begin: bool = True) -> int:
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
|
||||
The file-like object to save this attachment to or the filename
|
||||
to use. If a filename is passed then a file is created with that
|
||||
filename and used instead.
|
||||
seek_begin: :class:`bool`
|
||||
Whether to seek to the beginning of the file after saving is
|
||||
successfully done.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
"""
|
||||
|
||||
data = await self.read()
|
||||
if isinstance(fp, io.BufferedIOBase):
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
else:
|
||||
with open(fp, "wb") as f:
|
||||
return f.write(data)
|
||||
|
||||
|
||||
class Asset(AssetMixin):
|
||||
"""Represents a CDN asset on Discord.
|
||||
|
||||
.. container:: operations
|
||||
@@ -47,10 +128,6 @@ class Asset:
|
||||
|
||||
Returns the length of the CDN asset's URL.
|
||||
|
||||
.. describe:: bool(x)
|
||||
|
||||
Checks if the Asset has a URL.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the asset is equal to another asset.
|
||||
@@ -63,202 +140,276 @@ class Asset:
|
||||
|
||||
Returns the hash of the asset.
|
||||
"""
|
||||
__slots__ = ('_state', '_url')
|
||||
|
||||
BASE = 'https://cdn.discordapp.com'
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"_state",
|
||||
"_url",
|
||||
"_animated",
|
||||
"_key",
|
||||
)
|
||||
|
||||
def __init__(self, state, url=None):
|
||||
BASE = "https://cdn.discordapp.com"
|
||||
|
||||
def __init__(self, state, *, url: str, key: str, animated: bool = False):
|
||||
self._state = state
|
||||
self._url = url
|
||||
self._animated = animated
|
||||
self._key = key
|
||||
|
||||
@classmethod
|
||||
def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not user.is_avatar_animated():
|
||||
raise InvalidArgument("non animated avatars do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if user.avatar is None:
|
||||
return user.default_avatar_url
|
||||
|
||||
if format is None:
|
||||
format = 'gif' if user.is_avatar_animated() else static_format
|
||||
|
||||
return cls(state, '/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size))
|
||||
def _from_default_avatar(cls, state, index: int) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/embed/avatars/{index}.png",
|
||||
key=str(index),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state, object, path, *, format='webp', size=1024):
|
||||
if object.icon is None:
|
||||
return cls(state)
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
|
||||
return cls(state, url)
|
||||
def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
|
||||
animated = avatar.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024",
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_cover_image(cls, state, obj, *, format='webp', size=1024):
|
||||
if obj.cover_image is None:
|
||||
return cls(state)
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
|
||||
return cls(state, url)
|
||||
def _from_guild_avatar(cls, state, guild_id: int, member_id: int, avatar: str) -> Asset:
|
||||
animated = avatar.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024",
|
||||
key=avatar,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if hash is None:
|
||||
return cls(state)
|
||||
|
||||
url = '/{key}/{0}/{1}.{2}?size={3}'
|
||||
return cls(state, url.format(id, hash, format, size, key=key))
|
||||
def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024",
|
||||
key=icon_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not guild.is_icon_animated():
|
||||
raise InvalidArgument("non animated guild icons do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
if guild.icon is None:
|
||||
return cls(state)
|
||||
|
||||
if format is None:
|
||||
format = 'gif' if guild.is_icon_animated() else static_format
|
||||
|
||||
return cls(state, '/icons/{0.id}/{0.icon}.{1}?size={2}'.format(guild, format, size))
|
||||
def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024",
|
||||
key=cover_image_hash,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_sticker_url(cls, state, sticker, *, size=1024):
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
|
||||
return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size))
|
||||
def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024",
|
||||
key=image,
|
||||
animated=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
|
||||
if format == "gif" and not emoji.animated:
|
||||
raise InvalidArgument("non animated emoji's do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
if format is None:
|
||||
format = 'gif' if emoji.animated else static_format
|
||||
def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
|
||||
animated = icon_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024",
|
||||
key=icon_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
return cls(state, f'/emojis/{emoji.id}.{format}')
|
||||
@classmethod
|
||||
def _from_sticker_banner(cls, state, banner: int) -> Asset:
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png",
|
||||
key=str(banner),
|
||||
animated=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.BASE + self._url if self._url is not None else ''
|
||||
@classmethod
|
||||
def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset:
|
||||
animated = banner_hash.startswith("a_")
|
||||
format = "gif" if animated else "png"
|
||||
return cls(
|
||||
state,
|
||||
url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512",
|
||||
key=banner_hash,
|
||||
animated=animated,
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
if self._url:
|
||||
return len(self.BASE + self._url)
|
||||
return 0
|
||||
def __str__(self) -> str:
|
||||
return self._url
|
||||
|
||||
def __bool__(self):
|
||||
return self._url is not None
|
||||
def __len__(self) -> int:
|
||||
return len(self._url)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Asset url={self._url!r}>'
|
||||
shorten = self._url.replace(self.BASE, "")
|
||||
return f"<Asset url={shorten!r}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Asset) and self._url == other._url
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._url)
|
||||
|
||||
async def read(self):
|
||||
"""|coro|
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the underlying URL of the asset."""
|
||||
return self._url
|
||||
|
||||
Retrieves the content of this asset as a :class:`bytes` object.
|
||||
@property
|
||||
def key(self) -> str:
|
||||
""":class:`str`: Returns the identifying key of the asset."""
|
||||
return self._key
|
||||
|
||||
.. warning::
|
||||
def is_animated(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is animated."""
|
||||
return self._animated
|
||||
|
||||
:class:`PartialEmoji` won't have a connection state if user created,
|
||||
and a URL won't be present if a custom image isn't associated with
|
||||
the asset, e.g. a guild with no custom icon.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no valid URL or internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if not self._url:
|
||||
raise DiscordException('Invalid asset (no URL provided)')
|
||||
|
||||
if self._state is None:
|
||||
raise DiscordException('Invalid state (no ConnectionState provided)')
|
||||
|
||||
return await self._state.http.get_from_cdn(self.BASE + self._url)
|
||||
|
||||
async def save(self, fp, *, seek_begin=True):
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
def replace(
|
||||
self,
|
||||
*,
|
||||
size: int = MISSING,
|
||||
format: ValidAssetFormatTypes = MISSING,
|
||||
static_format: ValidStaticFormatTypes = MISSING,
|
||||
) -> Asset:
|
||||
"""Returns a new asset with the passed components replaced.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fp: Union[BinaryIO, :class:`os.PathLike`]
|
||||
Same as in :meth:`Attachment.save`.
|
||||
seek_begin: :class:`bool`
|
||||
Same as in :meth:`Attachment.save`.
|
||||
-----------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
format: :class:`str`
|
||||
The new format to change it to. Must be either
|
||||
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
|
||||
static_format: :class:`str`
|
||||
The new format to change it to if the asset isn't animated.
|
||||
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
|
||||
|
||||
Raises
|
||||
------
|
||||
DiscordException
|
||||
There was no valid URL or internal connection state.
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
-------
|
||||
InvalidArgument
|
||||
An invalid size or format was passed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
:class:`Asset`
|
||||
The newly updated asset.
|
||||
"""
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
|
||||
if format is not MISSING:
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}")
|
||||
url = url.with_path(f"{path}.{format}")
|
||||
elif static_format is MISSING:
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
url = url.with_path(f"{path}.{format}")
|
||||
|
||||
if static_format is not MISSING and not self._animated:
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
|
||||
url = url.with_path(f"{path}.{static_format}")
|
||||
|
||||
if size is not MISSING:
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
url = url.with_query(size=size)
|
||||
else:
|
||||
url = url.with_query(url.raw_query_string)
|
||||
|
||||
url = str(url)
|
||||
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_size(self, size: int, /) -> Asset:
|
||||
"""Returns a new asset with the specified size.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
size: :class:`int`
|
||||
The new size of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
The asset had an invalid size.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
|
||||
url = str(yarl.URL(self._url).with_query(size=size))
|
||||
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_format(self, format: ValidAssetFormatTypes, /) -> Asset:
|
||||
"""Returns a new asset with the specified format.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
format: :class:`str`
|
||||
The new format of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
|
||||
data = await self.read()
|
||||
if isinstance(fp, io.IOBase) and fp.writable():
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}")
|
||||
else:
|
||||
with open(fp, 'wb') as f:
|
||||
return f.write(data)
|
||||
if format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
|
||||
|
||||
url = yarl.URL(self._url)
|
||||
path, _ = os.path.splitext(url.path)
|
||||
url = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string))
|
||||
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
|
||||
|
||||
def with_static_format(self, format: ValidStaticFormatTypes, /) -> Asset:
|
||||
"""Returns a new asset with the specified static format.
|
||||
|
||||
This only changes the format if the underlying asset is
|
||||
not animated. Otherwise, the asset is not changed.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
format: :class:`str`
|
||||
The new static format of the asset.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
The asset had an invalid format.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The new updated asset.
|
||||
"""
|
||||
|
||||
if self._animated:
|
||||
return self
|
||||
return self.with_format(format)
|
||||
|
||||
@@ -22,64 +22,91 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from . import utils, enums
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union
|
||||
|
||||
from . import enums, utils
|
||||
from .asset import Asset
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
from .mixins import Hashable
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
|
||||
__all__ = (
|
||||
'AuditLogDiff',
|
||||
'AuditLogChanges',
|
||||
'AuditLogEntry',
|
||||
"AuditLogDiff",
|
||||
"AuditLogChanges",
|
||||
"AuditLogEntry",
|
||||
)
|
||||
|
||||
def _transform_verification_level(entry, data):
|
||||
return enums.try_enum(enums.VerificationLevel, data)
|
||||
|
||||
def _transform_default_notifications(entry, data):
|
||||
return enums.try_enum(enums.NotificationLevel, data)
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
||||
def _transform_explicit_content_filter(entry, data):
|
||||
return enums.try_enum(enums.ContentFilter, data)
|
||||
from . import abc
|
||||
from .emoji import Emoji
|
||||
from .guild import Guild
|
||||
from .member import Member
|
||||
from .role import Role
|
||||
from .types.audit_log import (
|
||||
AuditLogChange as AuditLogChangePayload,
|
||||
AuditLogEntry as AuditLogEntryPayload,
|
||||
)
|
||||
from .types.channel import PermissionOverwrite as PermissionOverwritePayload
|
||||
from .types.role import Role as RolePayload
|
||||
from .types.snowflake import Snowflake
|
||||
from .user import User
|
||||
from .stage_instance import StageInstance
|
||||
from .sticker import GuildSticker
|
||||
from .threads import Thread
|
||||
|
||||
def _transform_permissions(entry, data):
|
||||
return Permissions(data)
|
||||
|
||||
def _transform_color(entry, data):
|
||||
def _transform_permissions(entry: AuditLogEntry, data: str) -> Permissions:
|
||||
return Permissions(int(data))
|
||||
|
||||
|
||||
def _transform_color(entry: AuditLogEntry, data: int) -> Colour:
|
||||
return Colour(data)
|
||||
|
||||
def _transform_snowflake(entry, data):
|
||||
|
||||
def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int:
|
||||
return int(data)
|
||||
|
||||
def _transform_channel(entry, data):
|
||||
|
||||
def _transform_channel(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Union[abc.GuildChannel, Object]]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
|
||||
def _transform_owner_id(entry, data):
|
||||
|
||||
def _transform_member_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
def _transform_inviter_id(entry, data):
|
||||
|
||||
def _transform_guild_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Guild]:
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
return entry._state._get_guild(data)
|
||||
|
||||
def _transform_overwrites(entry, data):
|
||||
|
||||
def _transform_overwrites(
|
||||
entry: AuditLogEntry, data: List[PermissionOverwritePayload]
|
||||
) -> List[Tuple[Object, PermissionOverwrite]]:
|
||||
overwrites = []
|
||||
for elem in data:
|
||||
allow = Permissions(elem['allow'])
|
||||
deny = Permissions(elem['deny'])
|
||||
allow = Permissions(int(elem["allow"]))
|
||||
deny = Permissions(int(elem["deny"]))
|
||||
ow = PermissionOverwrite.from_pair(allow, deny)
|
||||
|
||||
ow_type = elem['type']
|
||||
ow_id = int(elem['id'])
|
||||
if ow_type == 'role':
|
||||
ow_type = elem["type"]
|
||||
ow_id = int(elem["id"])
|
||||
target = None
|
||||
if ow_type == "0":
|
||||
target = entry.guild.get_role(ow_id)
|
||||
else:
|
||||
elif ow_type == "1":
|
||||
target = entry._get_member(ow_id)
|
||||
|
||||
if target is None:
|
||||
@@ -89,63 +116,132 @@ def _transform_overwrites(entry, data):
|
||||
|
||||
return overwrites
|
||||
|
||||
|
||||
def _transform_icon(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
|
||||
|
||||
|
||||
def _transform_avatar(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore
|
||||
|
||||
|
||||
def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]], Optional[Asset]]:
|
||||
def _transform(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
|
||||
if data is None:
|
||||
return None
|
||||
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
T = TypeVar("T", bound=enums.Enum)
|
||||
|
||||
|
||||
def _enum_transformer(enum: Type[T]) -> Callable[[AuditLogEntry, int], T]:
|
||||
def _transform(entry: AuditLogEntry, data: int) -> T:
|
||||
return enums.try_enum(enum, data)
|
||||
|
||||
return _transform
|
||||
|
||||
|
||||
def _transform_type(entry: AuditLogEntry, data: Union[int]) -> Union[enums.ChannelType, enums.StickerType]:
|
||||
if entry.action.name.startswith("sticker_"):
|
||||
return enums.try_enum(enums.StickerType, data)
|
||||
else:
|
||||
return enums.try_enum(enums.ChannelType, data)
|
||||
|
||||
|
||||
class AuditLogDiff:
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.__dict__)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__dict__.items())
|
||||
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
||||
yield from self.__dict__.items()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
values = " ".join("%s=%r" % item for item in self.__dict__.items())
|
||||
return f"<AuditLogDiff {values}>"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
...
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> Any:
|
||||
...
|
||||
|
||||
|
||||
Transformer = Callable[["AuditLogEntry", Any], Any]
|
||||
|
||||
def __repr__(self):
|
||||
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
|
||||
return f'<AuditLogDiff {values}>'
|
||||
|
||||
class AuditLogChanges:
|
||||
TRANSFORMERS = {
|
||||
'verification_level': (None, _transform_verification_level),
|
||||
'explicit_content_filter': (None, _transform_explicit_content_filter),
|
||||
# fmt: off
|
||||
TRANSFORMERS: ClassVar[Dict[str, Tuple[Optional[str], Optional[Transformer]]]] = {
|
||||
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
|
||||
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
|
||||
'allow': (None, _transform_permissions),
|
||||
'deny': (None, _transform_permissions),
|
||||
'permissions': (None, _transform_permissions),
|
||||
'id': (None, _transform_snowflake),
|
||||
'color': ('colour', _transform_color),
|
||||
'owner_id': ('owner', _transform_owner_id),
|
||||
'inviter_id': ('inviter', _transform_inviter_id),
|
||||
'owner_id': ('owner', _transform_member_id),
|
||||
'inviter_id': ('inviter', _transform_member_id),
|
||||
'channel_id': ('channel', _transform_channel),
|
||||
'afk_channel_id': ('afk_channel', _transform_channel),
|
||||
'system_channel_id': ('system_channel', _transform_channel),
|
||||
'widget_channel_id': ('widget_channel', _transform_channel),
|
||||
'rules_channel_id': ('rules_channel', _transform_channel),
|
||||
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
|
||||
'permission_overwrites': ('overwrites', _transform_overwrites),
|
||||
'splash_hash': ('splash', None),
|
||||
'icon_hash': ('icon', None),
|
||||
'avatar_hash': ('avatar', None),
|
||||
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
|
||||
'banner_hash': ('banner', _guild_hash_transformer('banners')),
|
||||
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
|
||||
'icon_hash': ('icon', _transform_icon),
|
||||
'avatar_hash': ('avatar', _transform_avatar),
|
||||
'rate_limit_per_user': ('slowmode_delay', None),
|
||||
'default_message_notifications': ('default_notifications', _transform_default_notifications),
|
||||
'guild_id': ('guild', _transform_guild_id),
|
||||
'tags': ('emoji', None),
|
||||
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
|
||||
'region': (None, _enum_transformer(enums.VoiceRegion)),
|
||||
'rtc_region': (None, _enum_transformer(enums.VoiceRegion)),
|
||||
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
|
||||
'privacy_level': (None, _enum_transformer(enums.StagePrivacyLevel)),
|
||||
'format_type': (None, _enum_transformer(enums.StickerFormatType)),
|
||||
'type': (None, _transform_type),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, entry, data):
|
||||
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]):
|
||||
self.before = AuditLogDiff()
|
||||
self.after = AuditLogDiff()
|
||||
|
||||
for elem in data:
|
||||
attr = elem['key']
|
||||
attr = elem["key"]
|
||||
|
||||
# special cases for role add/remove
|
||||
if attr == '$add':
|
||||
self._handle_role(self.before, self.after, entry, elem['new_value'])
|
||||
if attr == "$add":
|
||||
self._handle_role(self.before, self.after, entry, elem["new_value"]) # type: ignore
|
||||
continue
|
||||
elif attr == '$remove':
|
||||
self._handle_role(self.after, self.before, entry, elem['new_value'])
|
||||
elif attr == "$remove":
|
||||
self._handle_role(self.after, self.before, entry, elem["new_value"]) # type: ignore
|
||||
continue
|
||||
|
||||
transformer = self.TRANSFORMERS.get(attr)
|
||||
if transformer:
|
||||
key, transformer = transformer
|
||||
try:
|
||||
key, transformer = self.TRANSFORMERS[attr]
|
||||
except (ValueError, KeyError):
|
||||
transformer = None
|
||||
else:
|
||||
if key:
|
||||
attr = key
|
||||
|
||||
transformer: Optional[Transformer]
|
||||
|
||||
try:
|
||||
before = elem['old_value']
|
||||
before = elem["old_value"]
|
||||
except KeyError:
|
||||
before = None
|
||||
else:
|
||||
@@ -155,7 +251,7 @@ class AuditLogChanges:
|
||||
setattr(self.before, attr, before)
|
||||
|
||||
try:
|
||||
after = elem['new_value']
|
||||
after = elem["new_value"]
|
||||
except KeyError:
|
||||
after = None
|
||||
else:
|
||||
@@ -165,31 +261,60 @@ class AuditLogChanges:
|
||||
setattr(self.after, attr, after)
|
||||
|
||||
# add an alias
|
||||
if hasattr(self.after, 'colour'):
|
||||
if hasattr(self.after, "colour"):
|
||||
self.after.color = self.after.colour
|
||||
self.before.color = self.before.colour
|
||||
if hasattr(self.after, "expire_behavior"):
|
||||
self.after.expire_behaviour = self.after.expire_behavior
|
||||
self.before.expire_behaviour = self.before.expire_behavior
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLogChanges before={self.before!r} after={self.after!r}>"
|
||||
|
||||
def _handle_role(self, first, second, entry, elem):
|
||||
if not hasattr(first, 'roles'):
|
||||
setattr(first, 'roles', [])
|
||||
def _handle_role(
|
||||
self, first: AuditLogDiff, second: AuditLogDiff, entry: AuditLogEntry, elem: List[RolePayload]
|
||||
) -> None:
|
||||
if not hasattr(first, "roles"):
|
||||
setattr(first, "roles", [])
|
||||
|
||||
data = []
|
||||
g = entry.guild
|
||||
g: Guild = entry.guild # type: ignore
|
||||
|
||||
for e in elem:
|
||||
role_id = int(e['id'])
|
||||
role_id = int(e["id"])
|
||||
role = g.get_role(role_id)
|
||||
|
||||
if role is None:
|
||||
role = Object(id=role_id)
|
||||
role.name = e['name']
|
||||
role.name = e["name"] # type: ignore
|
||||
|
||||
data.append(role)
|
||||
|
||||
setattr(second, 'roles', data)
|
||||
setattr(second, "roles", data)
|
||||
|
||||
|
||||
class _AuditLogProxyMemberPrune:
|
||||
delete_member_days: int
|
||||
members_removed: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberMoveOrMessageDelete:
|
||||
channel: abc.GuildChannel
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyMemberDisconnect:
|
||||
count: int
|
||||
|
||||
|
||||
class _AuditLogProxyPinAction:
|
||||
channel: abc.GuildChannel
|
||||
message_id: int
|
||||
|
||||
|
||||
class _AuditLogProxyStageInstanceAction:
|
||||
channel: abc.GuildChannel
|
||||
|
||||
|
||||
class AuditLogEntry(Hashable):
|
||||
r"""Represents an Audit Log entry.
|
||||
@@ -210,6 +335,10 @@ class AuditLogEntry(Hashable):
|
||||
|
||||
Returns the entry's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the entry's ID.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Audit log entries are now comparable and hashable.
|
||||
|
||||
@@ -234,153 +363,175 @@ class AuditLogEntry(Hashable):
|
||||
which actions have this field filled out.
|
||||
"""
|
||||
|
||||
def __init__(self, *, users, data, guild):
|
||||
def __init__(self, *, users: Dict[int, User], data: AuditLogEntryPayload, guild: Guild):
|
||||
self._state = guild._state
|
||||
self.guild = guild
|
||||
self._users = users
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
|
||||
self.id = int(data['id'])
|
||||
def _from_data(self, data: AuditLogEntryPayload) -> None:
|
||||
self.action = enums.try_enum(enums.AuditLogAction, data["action_type"])
|
||||
self.id = int(data["id"])
|
||||
|
||||
# this key is technically not usually present
|
||||
self.reason = data.get('reason')
|
||||
self.extra = data.get('options')
|
||||
self.reason = data.get("reason")
|
||||
self.extra = data.get("options")
|
||||
|
||||
if isinstance(self.action, enums.AuditLogAction) and self.extra:
|
||||
if self.action is enums.AuditLogAction.member_prune:
|
||||
# member prune has two keys with useful information
|
||||
self.extra = type('_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()})()
|
||||
self.extra: _AuditLogProxyMemberPrune = type(
|
||||
"_AuditLogProxy", (), {k: int(v) for k, v in self.extra.items()}
|
||||
)()
|
||||
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
|
||||
channel_id = int(self.extra['channel_id'])
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {
|
||||
'count': int(self.extra['count']),
|
||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id)
|
||||
"count": int(self.extra["count"]),
|
||||
"channel": self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
self.extra: _AuditLogProxyMemberMoveOrMessageDelete = type("_AuditLogProxy", (), elems)()
|
||||
elif self.action is enums.AuditLogAction.member_disconnect:
|
||||
# The member disconnect action has a dict with some information
|
||||
elems = {
|
||||
'count': int(self.extra['count']),
|
||||
"count": int(self.extra["count"]),
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
elif self.action.name.endswith('pin'):
|
||||
self.extra: _AuditLogProxyMemberDisconnect = type("_AuditLogProxy", (), elems)()
|
||||
elif self.action.name.endswith("pin"):
|
||||
# the pin actions have a dict with some information
|
||||
channel_id = int(self.extra['channel_id'])
|
||||
message_id = int(self.extra['message_id'])
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {
|
||||
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||
'message_id': message_id
|
||||
"channel": self.guild.get_channel(channel_id) or Object(id=channel_id),
|
||||
"message_id": int(self.extra["message_id"]),
|
||||
}
|
||||
self.extra = type('_AuditLogProxy', (), elems)()
|
||||
elif self.action.name.startswith('overwrite_'):
|
||||
self.extra: _AuditLogProxyPinAction = type("_AuditLogProxy", (), elems)()
|
||||
elif self.action.name.startswith("overwrite_"):
|
||||
# the overwrite_ actions have a dict with some information
|
||||
instance_id = int(self.extra['id'])
|
||||
the_type = self.extra.get('type')
|
||||
if the_type == 'member':
|
||||
instance_id = int(self.extra["id"])
|
||||
the_type = self.extra.get("type")
|
||||
if the_type == "1":
|
||||
self.extra = self._get_member(instance_id)
|
||||
else:
|
||||
elif the_type == "0":
|
||||
role = self.guild.get_role(instance_id)
|
||||
if role is None:
|
||||
role = Object(id=instance_id)
|
||||
role.name = self.extra.get('role_name')
|
||||
self.extra = role
|
||||
role.name = self.extra.get("role_name") # type: ignore
|
||||
self.extra: Role = role
|
||||
elif self.action.name.startswith("stage_instance"):
|
||||
channel_id = int(self.extra["channel_id"])
|
||||
elems = {"channel": self.guild.get_channel(channel_id) or Object(id=channel_id)}
|
||||
self.extra: _AuditLogProxyStageInstanceAction = type("_AuditLogProxy", (), elems)()
|
||||
|
||||
# fmt: off
|
||||
self.extra: Union[
|
||||
_AuditLogProxyMemberPrune,
|
||||
_AuditLogProxyMemberMoveOrMessageDelete,
|
||||
_AuditLogProxyMemberDisconnect,
|
||||
_AuditLogProxyPinAction,
|
||||
_AuditLogProxyStageInstanceAction,
|
||||
Member, User, None,
|
||||
Role,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
# this key is not present when the above is present, typically.
|
||||
# It's a list of { new_value: a, old_value: b, key: c }
|
||||
# where new_value and old_value are not guaranteed to be there depending
|
||||
# on the action type, so let's just fetch it for now and only turn it
|
||||
# into meaningful data when requested
|
||||
self._changes = data.get('changes', [])
|
||||
self._changes = data.get("changes", [])
|
||||
|
||||
self.user = self._get_member(utils._get_as_snowflake(data, 'user_id'))
|
||||
self._target_id = utils._get_as_snowflake(data, 'target_id')
|
||||
self.user = self._get_member(utils._get_as_snowflake(data, "user_id")) # type: ignore
|
||||
self._target_id = utils._get_as_snowflake(data, "target_id")
|
||||
|
||||
def _get_member(self, user_id):
|
||||
def _get_member(self, user_id: int) -> Union[Member, User, None]:
|
||||
return self.guild.get_member(user_id) or self._users.get(user_id)
|
||||
|
||||
def __repr__(self):
|
||||
return '<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"
|
||||
|
||||
@utils.cached_property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@utils.cached_property
|
||||
def target(self):
|
||||
def target(
|
||||
self,
|
||||
) -> Union[
|
||||
Guild, abc.GuildChannel, Member, User, Role, Invite, Emoji, StageInstance, GuildSticker, Thread, Object, None
|
||||
]:
|
||||
try:
|
||||
converter = getattr(self, '_convert_target_' + self.action.target_type)
|
||||
converter = getattr(self, "_convert_target_" + self.action.target_type)
|
||||
except AttributeError:
|
||||
return Object(id=self._target_id)
|
||||
else:
|
||||
return converter(self._target_id)
|
||||
|
||||
@utils.cached_property
|
||||
def category(self):
|
||||
def category(self) -> enums.AuditLogActionCategory:
|
||||
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
||||
return self.action.category
|
||||
|
||||
@utils.cached_property
|
||||
def changes(self):
|
||||
def changes(self) -> AuditLogChanges:
|
||||
""":class:`AuditLogChanges`: The list of changes this entry has."""
|
||||
obj = AuditLogChanges(self, self._changes)
|
||||
del self._changes
|
||||
return obj
|
||||
|
||||
@utils.cached_property
|
||||
def before(self):
|
||||
def before(self) -> AuditLogDiff:
|
||||
""":class:`AuditLogDiff`: The target's prior state."""
|
||||
return self.changes.before
|
||||
|
||||
@utils.cached_property
|
||||
def after(self):
|
||||
def after(self) -> AuditLogDiff:
|
||||
""":class:`AuditLogDiff`: The target's subsequent state."""
|
||||
return self.changes.after
|
||||
|
||||
def _convert_target_guild(self, target_id):
|
||||
def _convert_target_guild(self, target_id: int) -> Guild:
|
||||
return self.guild
|
||||
|
||||
def _convert_target_channel(self, target_id):
|
||||
ch = self.guild.get_channel(target_id)
|
||||
if ch is None:
|
||||
return Object(id=target_id)
|
||||
return ch
|
||||
def _convert_target_channel(self, target_id: int) -> Union[abc.GuildChannel, Object]:
|
||||
return self.guild.get_channel(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_user(self, target_id):
|
||||
def _convert_target_user(self, target_id: int) -> Union[Member, User, None]:
|
||||
return self._get_member(target_id)
|
||||
|
||||
def _convert_target_role(self, target_id):
|
||||
role = self.guild.get_role(target_id)
|
||||
if role is None:
|
||||
return Object(id=target_id)
|
||||
return role
|
||||
def _convert_target_role(self, target_id: int) -> Union[Role, Object]:
|
||||
return self.guild.get_role(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_invite(self, target_id):
|
||||
def _convert_target_invite(self, target_id: int) -> Invite:
|
||||
# invites have target_id set to null
|
||||
# so figure out which change has the full invite data
|
||||
changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after
|
||||
|
||||
fake_payload = {
|
||||
'max_age': changeset.max_age,
|
||||
'max_uses': changeset.max_uses,
|
||||
'code': changeset.code,
|
||||
'temporary': changeset.temporary,
|
||||
'channel': changeset.channel,
|
||||
'uses': changeset.uses,
|
||||
'guild': self.guild,
|
||||
"max_age": changeset.max_age,
|
||||
"max_uses": changeset.max_uses,
|
||||
"code": changeset.code,
|
||||
"temporary": changeset.temporary,
|
||||
"uses": changeset.uses,
|
||||
}
|
||||
|
||||
obj = Invite(state=self._state, data=fake_payload)
|
||||
obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore
|
||||
try:
|
||||
obj.inviter = changeset.inviter
|
||||
except AttributeError:
|
||||
pass
|
||||
return obj
|
||||
|
||||
def _convert_target_emoji(self, target_id):
|
||||
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
|
||||
return self._state.get_emoji(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_message(self, target_id):
|
||||
def _convert_target_message(self, target_id: int) -> Union[Member, User, None]:
|
||||
return self._get_member(target_id)
|
||||
|
||||
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:
|
||||
return self.guild.get_stage_instance(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_sticker(self, target_id: int) -> Union[GuildSticker, Object]:
|
||||
return self._state.get_sticker(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_thread(self, target_id: int) -> Union[Thread, Object]:
|
||||
return self.guild.get_thread(target_id) or Object(id=target_id)
|
||||
|
||||
@@ -22,14 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import time
|
||||
import random
|
||||
from typing import Callable, Generic, Literal, TypeVar, overload, Union
|
||||
|
||||
__all__ = (
|
||||
'ExponentialBackoff',
|
||||
)
|
||||
T = TypeVar("T", bool, Literal[True], Literal[False])
|
||||
|
||||
class ExponentialBackoff:
|
||||
__all__ = ("ExponentialBackoff",)
|
||||
|
||||
|
||||
class ExponentialBackoff(Generic[T]):
|
||||
"""An implementation of the exponential backoff algorithm
|
||||
|
||||
Provides a convenient interface to implement an exponential backoff
|
||||
@@ -51,21 +56,33 @@ class ExponentialBackoff:
|
||||
number in between may be returned.
|
||||
"""
|
||||
|
||||
def __init__(self, base=1, *, integral=False):
|
||||
self._base = base
|
||||
def __init__(self, base: int = 1, *, integral: T = False):
|
||||
self._base: int = base
|
||||
|
||||
self._exp = 0
|
||||
self._max = 10
|
||||
self._reset_time = base * 2 ** 11
|
||||
self._last_invocation = time.monotonic()
|
||||
self._exp: int = 0
|
||||
self._max: int = 10
|
||||
self._reset_time: int = base * 2 ** 11
|
||||
self._last_invocation: float = time.monotonic()
|
||||
|
||||
# Use our own random instance to avoid messing with global one
|
||||
rand = random.Random()
|
||||
rand.seed()
|
||||
|
||||
self._randfunc = rand.randrange if integral else rand.uniform
|
||||
self._randfunc: Callable[..., Union[int, float]] = rand.randrange if integral else rand.uniform # type: ignore
|
||||
|
||||
def delay(self):
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[False]]) -> float:
|
||||
...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[Literal[True]]) -> int:
|
||||
...
|
||||
|
||||
@overload
|
||||
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]:
|
||||
...
|
||||
|
||||
def delay(self) -> Union[int, float]:
|
||||
"""Compute the next delay
|
||||
|
||||
Returns the next delay to wait according to the exponential
|
||||
|
||||
1311
discord/channel.py
1311
discord/channel.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,11 +25,23 @@ DEALINGS IN THE SOFTWARE.
|
||||
import colorsys
|
||||
import random
|
||||
|
||||
__all__ = (
|
||||
'Colour',
|
||||
'Color',
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"Colour",
|
||||
"Color",
|
||||
)
|
||||
|
||||
CT = TypeVar("CT", bound="Colour")
|
||||
|
||||
|
||||
class Colour:
|
||||
"""Represents a Discord role colour. This class is similar
|
||||
to a (red, green, blue) :class:`tuple`.
|
||||
@@ -54,75 +66,82 @@ class Colour:
|
||||
|
||||
Returns the hex format for the colour.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the raw colour value.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
value: :class:`int`
|
||||
The raw integer colour value.
|
||||
"""
|
||||
|
||||
__slots__ = ('value',)
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, value: int):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
|
||||
raise TypeError(f"Expected int parameter, received {value.__class__.__name__} instead.")
|
||||
|
||||
self.value = value
|
||||
self.value: int = value
|
||||
|
||||
def _get_byte(self, byte):
|
||||
return (self.value >> (8 * byte)) & 0xff
|
||||
def _get_byte(self, byte: int) -> int:
|
||||
return (self.value >> (8 * byte)) & 0xFF
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, Colour) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return f'#{self.value:0>6x}'
|
||||
def __str__(self) -> str:
|
||||
return f"#{self.value:0>6x}"
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Colour value={self.value}>'
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
def __hash__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Colour value={self.value}>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
@property
|
||||
def r(self):
|
||||
def r(self) -> int:
|
||||
""":class:`int`: Returns the red component of the colour."""
|
||||
return self._get_byte(2)
|
||||
|
||||
@property
|
||||
def g(self):
|
||||
def g(self) -> int:
|
||||
""":class:`int`: Returns the green component of the colour."""
|
||||
return self._get_byte(1)
|
||||
|
||||
@property
|
||||
def b(self):
|
||||
def b(self) -> int:
|
||||
""":class:`int`: Returns the blue component of the colour."""
|
||||
return self._get_byte(0)
|
||||
|
||||
def to_rgb(self):
|
||||
def to_rgb(self) -> Tuple[int, int, int]:
|
||||
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
|
||||
return (self.r, self.g, self.b)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls, r, g, b):
|
||||
def from_rgb(cls: Type[CT], r: int, g: int, b: int) -> CT:
|
||||
"""Constructs a :class:`Colour` from an RGB tuple."""
|
||||
return cls((r << 16) + (g << 8) + b)
|
||||
|
||||
@classmethod
|
||||
def from_hsv(cls, h, s, v):
|
||||
def from_hsv(cls: Type[CT], h: float, s: float, v: float) -> CT:
|
||||
"""Constructs a :class:`Colour` from an HSV tuple."""
|
||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
def default(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def random(cls, *, seed=None):
|
||||
def random(cls: Type[CT], *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a random hue.
|
||||
|
||||
.. note::
|
||||
@@ -143,125 +162,153 @@ class Colour:
|
||||
return cls.from_hsv(rand.random(), 1, 1)
|
||||
|
||||
@classmethod
|
||||
def teal(cls):
|
||||
def teal(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
||||
return cls(0x1abc9c)
|
||||
return cls(0x1ABC9C)
|
||||
|
||||
@classmethod
|
||||
def dark_teal(cls):
|
||||
def dark_teal(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
|
||||
return cls(0x11806a)
|
||||
return cls(0x11806A)
|
||||
|
||||
@classmethod
|
||||
def green(cls):
|
||||
def brand_green(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x57F287``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0x57F287)
|
||||
|
||||
@classmethod
|
||||
def green(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
|
||||
return cls(0x2ecc71)
|
||||
return cls(0x2ECC71)
|
||||
|
||||
@classmethod
|
||||
def dark_green(cls):
|
||||
def dark_green(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
|
||||
return cls(0x1f8b4c)
|
||||
return cls(0x1F8B4C)
|
||||
|
||||
@classmethod
|
||||
def blue(cls):
|
||||
def blue(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
|
||||
return cls(0x3498db)
|
||||
return cls(0x3498DB)
|
||||
|
||||
@classmethod
|
||||
def dark_blue(cls):
|
||||
def dark_blue(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
|
||||
return cls(0x206694)
|
||||
|
||||
@classmethod
|
||||
def purple(cls):
|
||||
def purple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
|
||||
return cls(0x9b59b6)
|
||||
return cls(0x9B59B6)
|
||||
|
||||
@classmethod
|
||||
def dark_purple(cls):
|
||||
def dark_purple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
|
||||
return cls(0x71368a)
|
||||
return cls(0x71368A)
|
||||
|
||||
@classmethod
|
||||
def magenta(cls):
|
||||
def magenta(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
|
||||
return cls(0xe91e63)
|
||||
return cls(0xE91E63)
|
||||
|
||||
@classmethod
|
||||
def dark_magenta(cls):
|
||||
def dark_magenta(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
|
||||
return cls(0xad1457)
|
||||
return cls(0xAD1457)
|
||||
|
||||
@classmethod
|
||||
def gold(cls):
|
||||
def gold(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
|
||||
return cls(0xf1c40f)
|
||||
return cls(0xF1C40F)
|
||||
|
||||
@classmethod
|
||||
def dark_gold(cls):
|
||||
def dark_gold(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
|
||||
return cls(0xc27c0e)
|
||||
return cls(0xC27C0E)
|
||||
|
||||
@classmethod
|
||||
def orange(cls):
|
||||
def orange(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
|
||||
return cls(0xe67e22)
|
||||
return cls(0xE67E22)
|
||||
|
||||
@classmethod
|
||||
def dark_orange(cls):
|
||||
def dark_orange(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
|
||||
return cls(0xa84300)
|
||||
return cls(0xA84300)
|
||||
|
||||
@classmethod
|
||||
def red(cls):
|
||||
def brand_red(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xED4245``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xED4245)
|
||||
|
||||
@classmethod
|
||||
def red(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
||||
return cls(0xe74c3c)
|
||||
return cls(0xE74C3C)
|
||||
|
||||
@classmethod
|
||||
def dark_red(cls):
|
||||
def nitro_booster(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf47fff``.
|
||||
|
||||
.. versionadded:: 2.0"""
|
||||
return cls(0xF47FFF)
|
||||
|
||||
@classmethod
|
||||
def dark_red(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
|
||||
return cls(0x992d22)
|
||||
return cls(0x992D22)
|
||||
|
||||
@classmethod
|
||||
def lighter_grey(cls):
|
||||
def lighter_grey(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
|
||||
return cls(0x95a5a6)
|
||||
return cls(0x95A5A6)
|
||||
|
||||
lighter_gray = lighter_grey
|
||||
|
||||
@classmethod
|
||||
def dark_grey(cls):
|
||||
def dark_grey(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
|
||||
return cls(0x607d8b)
|
||||
return cls(0x607D8B)
|
||||
|
||||
dark_gray = dark_grey
|
||||
|
||||
@classmethod
|
||||
def light_grey(cls):
|
||||
def light_grey(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
|
||||
return cls(0x979c9f)
|
||||
return cls(0x979C9F)
|
||||
|
||||
light_gray = light_grey
|
||||
|
||||
@classmethod
|
||||
def darker_grey(cls):
|
||||
def darker_grey(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
|
||||
return cls(0x546e7a)
|
||||
return cls(0x546E7A)
|
||||
|
||||
darker_gray = darker_grey
|
||||
|
||||
@classmethod
|
||||
def blurple(cls):
|
||||
def og_blurple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
|
||||
return cls(0x7289da)
|
||||
return cls(0x7289DA)
|
||||
|
||||
@classmethod
|
||||
def greyple(cls):
|
||||
def blurple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x5865F2``."""
|
||||
return cls(0x5865F2)
|
||||
|
||||
@classmethod
|
||||
def greyple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
|
||||
return cls(0x99aab5)
|
||||
return cls(0x99AAB5)
|
||||
|
||||
@classmethod
|
||||
def dark_theme(cls):
|
||||
def dark_theme(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x36393F``.
|
||||
This will appear transparent on Discord's dark theme.
|
||||
|
||||
@@ -269,4 +316,30 @@ class Colour:
|
||||
"""
|
||||
return cls(0x36393F)
|
||||
|
||||
@classmethod
|
||||
def fuchsia(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459E``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xEB459E)
|
||||
|
||||
@classmethod
|
||||
def yellow(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0xFEE75C)
|
||||
|
||||
@classmethod
|
||||
def dark_blurple(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x4E5D94``.
|
||||
This is the original Dark Blurple branding.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return cls(0x4E5D94)
|
||||
|
||||
|
||||
Color = Colour
|
||||
|
||||
383
discord/components.py
Normal file
383
discord/components.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||
from .enums import try_enum, ComponentType, ButtonStyle
|
||||
from .utils import get_slots, MISSING
|
||||
from .partial_emoji import PartialEmoji, _EmojiTag
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.components import (
|
||||
Component as ComponentPayload,
|
||||
ButtonComponent as ButtonComponentPayload,
|
||||
SelectMenu as SelectMenuPayload,
|
||||
SelectOption as SelectOptionPayload,
|
||||
ActionRow as ActionRowPayload,
|
||||
)
|
||||
from .emoji import Emoji
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Component",
|
||||
"ActionRow",
|
||||
"Button",
|
||||
"SelectMenu",
|
||||
"SelectOption",
|
||||
)
|
||||
|
||||
C = TypeVar("C", bound="Component")
|
||||
|
||||
|
||||
class Component:
|
||||
"""Represents a Discord Bot UI Kit Component.
|
||||
|
||||
Currently, the only components supported by Discord are:
|
||||
|
||||
- :class:`ActionRow`
|
||||
- :class:`Button`
|
||||
- :class:`SelectMenu`
|
||||
|
||||
This class is abstract and cannot be instantiated.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
type: :class:`ComponentType`
|
||||
The type of component.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = ("type",)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]]
|
||||
type: ComponentType
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__)
|
||||
return f"<{self.__class__.__name__} {attrs}>"
|
||||
|
||||
@classmethod
|
||||
def _raw_construct(cls: Type[C], **kwargs) -> C:
|
||||
self: C = cls.__new__(cls)
|
||||
for slot in get_slots(cls):
|
||||
try:
|
||||
value = kwargs[slot]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
setattr(self, slot, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ActionRow(Component):
|
||||
"""Represents a Discord Bot UI Kit Action Row.
|
||||
|
||||
This is a component that holds up to 5 children components in a row.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
type: :class:`ComponentType`
|
||||
The type of component.
|
||||
children: List[:class:`Component`]
|
||||
The children components that this holds, if any.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = ("children",)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: ComponentPayload):
|
||||
self.type: ComponentType = try_enum(ComponentType, data["type"])
|
||||
self.children: List[Component] = [_component_factory(d) for d in data.get("components", [])]
|
||||
|
||||
def to_dict(self) -> ActionRowPayload:
|
||||
return {
|
||||
"type": int(self.type),
|
||||
"components": [child.to_dict() for child in self.children],
|
||||
} # type: ignore
|
||||
|
||||
|
||||
class Button(Component):
|
||||
"""Represents a button from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type to create a button is :class:`discord.ui.Button`
|
||||
not this one.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
style: :class:`.ButtonStyle`
|
||||
The style of the button.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the button that gets received during an interaction.
|
||||
If this button is for a URL, it does not have a custom ID.
|
||||
url: Optional[:class:`str`]
|
||||
The URL this button sends you to.
|
||||
disabled: :class:`bool`
|
||||
Whether the button is disabled or not.
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any.
|
||||
emoji: Optional[:class:`PartialEmoji`]
|
||||
The emoji of the button, if available.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"style",
|
||||
"custom_id",
|
||||
"url",
|
||||
"disabled",
|
||||
"label",
|
||||
"emoji",
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: ButtonComponentPayload):
|
||||
self.type: ComponentType = try_enum(ComponentType, data["type"])
|
||||
self.style: ButtonStyle = try_enum(ButtonStyle, data["style"])
|
||||
self.custom_id: Optional[str] = data.get("custom_id")
|
||||
self.url: Optional[str] = data.get("url")
|
||||
self.disabled: bool = data.get("disabled", False)
|
||||
self.label: Optional[str] = data.get("label")
|
||||
self.emoji: Optional[PartialEmoji]
|
||||
try:
|
||||
self.emoji = PartialEmoji.from_dict(data["emoji"])
|
||||
except KeyError:
|
||||
self.emoji = None
|
||||
|
||||
def to_dict(self) -> ButtonComponentPayload:
|
||||
payload = {
|
||||
"type": 2,
|
||||
"style": int(self.style),
|
||||
"label": self.label,
|
||||
"disabled": self.disabled,
|
||||
}
|
||||
if self.custom_id:
|
||||
payload["custom_id"] = self.custom_id
|
||||
|
||||
if self.url:
|
||||
payload["url"] = self.url
|
||||
|
||||
if self.emoji:
|
||||
payload["emoji"] = self.emoji.to_dict()
|
||||
|
||||
return payload # type: ignore
|
||||
|
||||
|
||||
class SelectMenu(Component):
|
||||
"""Represents a select menu from the Discord Bot UI Kit.
|
||||
|
||||
A select menu is functionally the same as a dropdown, however
|
||||
on mobile it renders a bit differently.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type to create a select menu is
|
||||
:class:`discord.ui.Select` not this one.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the select menu that gets received during an interaction.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`SelectOption`]
|
||||
A list of options that can be selected in this menu.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"custom_id",
|
||||
"placeholder",
|
||||
"min_values",
|
||||
"max_values",
|
||||
"options",
|
||||
"disabled",
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: SelectMenuPayload):
|
||||
self.type = ComponentType.select
|
||||
self.custom_id: str = data["custom_id"]
|
||||
self.placeholder: Optional[str] = data.get("placeholder")
|
||||
self.min_values: int = data.get("min_values", 1)
|
||||
self.max_values: int = data.get("max_values", 1)
|
||||
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get("options", [])]
|
||||
self.disabled: bool = data.get("disabled", False)
|
||||
|
||||
def to_dict(self) -> SelectMenuPayload:
|
||||
payload: SelectMenuPayload = {
|
||||
"type": self.type.value,
|
||||
"custom_id": self.custom_id,
|
||||
"min_values": self.min_values,
|
||||
"max_values": self.max_values,
|
||||
"options": [op.to_dict() for op in self.options],
|
||||
"disabled": self.disabled,
|
||||
}
|
||||
|
||||
if self.placeholder:
|
||||
payload["placeholder"] = self.placeholder
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class SelectOption:
|
||||
"""Represents a select menu's option.
|
||||
|
||||
These can be created by users.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
label: :class:`str`
|
||||
The label of the option. This is displayed to users.
|
||||
Can only be up to 100 characters.
|
||||
value: :class:`str`
|
||||
The value of the option. This is not displayed to users.
|
||||
If not provided when constructed then it defaults to the
|
||||
label. Can only be up to 100 characters.
|
||||
description: Optional[:class:`str`]
|
||||
An additional description of the option, if any.
|
||||
Can only be up to 100 characters.
|
||||
emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
|
||||
The emoji of the option, if available.
|
||||
default: :class:`bool`
|
||||
Whether this option is selected by default.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"label",
|
||||
"value",
|
||||
"description",
|
||||
"emoji",
|
||||
"default",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
value: str = MISSING,
|
||||
description: Optional[str] = None,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
default: bool = False,
|
||||
) -> None:
|
||||
self.label = label
|
||||
self.value = label if value is MISSING else value
|
||||
self.description = description
|
||||
|
||||
if emoji is not None:
|
||||
if isinstance(emoji, str):
|
||||
emoji = PartialEmoji.from_str(emoji)
|
||||
elif isinstance(emoji, _EmojiTag):
|
||||
emoji = emoji._to_partial()
|
||||
else:
|
||||
raise TypeError(f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}")
|
||||
|
||||
self.emoji = emoji
|
||||
self.default = default
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<SelectOption label={self.label!r} value={self.value!r} description={self.description!r} "
|
||||
f"emoji={self.emoji!r} default={self.default!r}>"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.emoji:
|
||||
base = f"{self.emoji} {self.label}"
|
||||
else:
|
||||
base = self.label
|
||||
|
||||
if self.description:
|
||||
return f"{base}\n{self.description}"
|
||||
return base
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: SelectOptionPayload) -> SelectOption:
|
||||
try:
|
||||
emoji = PartialEmoji.from_dict(data["emoji"])
|
||||
except KeyError:
|
||||
emoji = None
|
||||
|
||||
return cls(
|
||||
label=data["label"],
|
||||
value=data["value"],
|
||||
description=data.get("description"),
|
||||
emoji=emoji,
|
||||
default=data.get("default", False),
|
||||
)
|
||||
|
||||
def to_dict(self) -> SelectOptionPayload:
|
||||
payload: SelectOptionPayload = {
|
||||
"label": self.label,
|
||||
"value": self.value,
|
||||
"default": self.default,
|
||||
}
|
||||
|
||||
if self.emoji:
|
||||
payload["emoji"] = self.emoji.to_dict() # type: ignore
|
||||
|
||||
if self.description:
|
||||
payload["description"] = self.description
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _component_factory(data: ComponentPayload) -> Component:
|
||||
component_type = data["type"]
|
||||
if component_type == 1:
|
||||
return ActionRow(data)
|
||||
elif component_type == 2:
|
||||
return Button(data) # type: ignore
|
||||
elif component_type == 3:
|
||||
return SelectMenu(data) # type: ignore
|
||||
else:
|
||||
as_enum = try_enum(ComponentType, component_type)
|
||||
return Component._raw_construct(type=as_enum)
|
||||
@@ -22,25 +22,35 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional, Type
|
||||
|
||||
__all__ = (
|
||||
'Typing',
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
from .abc import Messageable
|
||||
|
||||
def _typing_done_callback(fut):
|
||||
from types import TracebackType
|
||||
|
||||
TypingT = TypeVar("TypingT", bound="Typing")
|
||||
|
||||
__all__ = ("Typing",)
|
||||
|
||||
|
||||
def _typing_done_callback(fut: asyncio.Future) -> None:
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
fut.exception()
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
class Typing:
|
||||
def __init__(self, messageable):
|
||||
self.loop = messageable._state.loop
|
||||
self.messageable = messageable
|
||||
|
||||
async def do_typing(self):
|
||||
class Typing:
|
||||
def __init__(self, messageable: Messageable) -> None:
|
||||
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
|
||||
self.messageable: Messageable = messageable
|
||||
|
||||
async def do_typing(self) -> None:
|
||||
try:
|
||||
channel = self._channel
|
||||
except AttributeError:
|
||||
@@ -52,18 +62,28 @@ class Typing:
|
||||
await typing(channel.id)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def __enter__(self):
|
||||
self.task = asyncio.ensure_future(self.do_typing(), loop=self.loop)
|
||||
def __enter__(self: TypingT) -> TypingT:
|
||||
self.task: asyncio.Task = self.loop.create_task(self.do_typing())
|
||||
self.task.add_done_callback(_typing_done_callback)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
self.task.cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
async def __aenter__(self: TypingT) -> TypingT:
|
||||
self._channel = channel = await self.messageable._get_channel()
|
||||
await channel._state.http.send_typing(channel.id)
|
||||
return self.__enter__()
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
self.task.cancel()
|
||||
|
||||
@@ -25,14 +25,12 @@ DEALINGS IN THE SOFTWARE.
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
|
||||
from typing import Any, Dict, Final, List, Mapping, Protocol, TYPE_CHECKING, Type, TypeVar, Union
|
||||
|
||||
from . import utils
|
||||
from .colour import Colour
|
||||
|
||||
__all__ = (
|
||||
'Embed',
|
||||
)
|
||||
__all__ = ("Embed",)
|
||||
|
||||
|
||||
class _EmptyEmbed:
|
||||
@@ -40,7 +38,7 @@ class _EmptyEmbed:
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Embed.Empty'
|
||||
return "Embed.Empty"
|
||||
|
||||
def __len__(self) -> int:
|
||||
return 0
|
||||
@@ -57,19 +55,19 @@ class EmbedProxy:
|
||||
return len(self.__dict__)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
|
||||
return f'EmbedProxy({inner})'
|
||||
inner = ", ".join((f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_")))
|
||||
return f"EmbedProxy({inner})"
|
||||
|
||||
def __getattr__(self, attr: str) -> _EmptyEmbed:
|
||||
return EmptyEmbed
|
||||
|
||||
|
||||
E = TypeVar('E', bound='Embed')
|
||||
E = TypeVar("E", bound="Embed")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from discord.types.embed import Embed as EmbedData, EmbedType
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
MaybeEmpty = Union[T, _EmptyEmbed]
|
||||
|
||||
class _EmbedFooterProxy(Protocol):
|
||||
@@ -157,19 +155,19 @@ class Embed:
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'title',
|
||||
'url',
|
||||
'type',
|
||||
'_timestamp',
|
||||
'_colour',
|
||||
'_footer',
|
||||
'_image',
|
||||
'_thumbnail',
|
||||
'_video',
|
||||
'_provider',
|
||||
'_author',
|
||||
'_fields',
|
||||
'description',
|
||||
"title",
|
||||
"url",
|
||||
"type",
|
||||
"_timestamp",
|
||||
"_colour",
|
||||
"_footer",
|
||||
"_image",
|
||||
"_thumbnail",
|
||||
"_video",
|
||||
"_provider",
|
||||
"_author",
|
||||
"_fields",
|
||||
"description",
|
||||
)
|
||||
|
||||
Empty: Final = EmptyEmbed
|
||||
@@ -179,10 +177,10 @@ class Embed:
|
||||
*,
|
||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
title: MaybeEmpty[str] = EmptyEmbed,
|
||||
type: EmbedType = 'rich',
|
||||
url: MaybeEmpty[str] = EmptyEmbed,
|
||||
description: MaybeEmpty[str] = EmptyEmbed,
|
||||
title: MaybeEmpty[Any] = EmptyEmbed,
|
||||
type: EmbedType = "rich",
|
||||
url: MaybeEmpty[Any] = EmptyEmbed,
|
||||
description: MaybeEmpty[Any] = EmptyEmbed,
|
||||
timestamp: datetime.datetime = None,
|
||||
):
|
||||
|
||||
@@ -202,12 +200,10 @@ class Embed:
|
||||
self.url = str(self.url)
|
||||
|
||||
if timestamp:
|
||||
if timestamp.tzinfo is None:
|
||||
timestamp = timestamp.astimezone()
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: Type[E], data: EmbedData) -> E:
|
||||
def from_dict(cls: Type[E], data: Mapping[str, Any]) -> E:
|
||||
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
|
||||
format that Discord expects it to be in.
|
||||
|
||||
@@ -227,10 +223,10 @@ class Embed:
|
||||
|
||||
# fill in the basic fields
|
||||
|
||||
self.title = data.get('title', EmptyEmbed)
|
||||
self.type = data.get('type', EmptyEmbed)
|
||||
self.description = data.get('description', EmptyEmbed)
|
||||
self.url = data.get('url', EmptyEmbed)
|
||||
self.title = data.get("title", EmptyEmbed)
|
||||
self.type = data.get("type", EmptyEmbed)
|
||||
self.description = data.get("description", EmptyEmbed)
|
||||
self.url = data.get("url", EmptyEmbed)
|
||||
|
||||
if self.title is not EmptyEmbed:
|
||||
self.title = str(self.title)
|
||||
@@ -244,22 +240,22 @@ class Embed:
|
||||
# try to fill in the more rich fields
|
||||
|
||||
try:
|
||||
self._colour = Colour(value=data['color'])
|
||||
self._colour = Colour(value=data["color"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._timestamp = utils.parse_time(data['timestamp'])
|
||||
self._timestamp = utils.parse_time(data["timestamp"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for attr in ('thumbnail', 'video', 'provider', 'author', 'fields', 'image', 'footer'):
|
||||
for attr in ("thumbnail", "video", "provider", "author", "fields", "image", "footer"):
|
||||
try:
|
||||
value = data[attr]
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
setattr(self, '_' + attr, value)
|
||||
setattr(self, "_" + attr, value)
|
||||
|
||||
return self
|
||||
|
||||
@@ -269,22 +265,22 @@ class Embed:
|
||||
|
||||
def __len__(self) -> int:
|
||||
total = len(self.title) + len(self.description)
|
||||
for field in getattr(self, '_fields', []):
|
||||
total += len(field['name']) + len(field['value'])
|
||||
for field in getattr(self, "_fields", []):
|
||||
total += len(field["name"]) + len(field["value"])
|
||||
|
||||
try:
|
||||
footer = self._footer
|
||||
except AttributeError:
|
||||
footer_text = self._footer["text"]
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
else:
|
||||
total += len(footer['text'])
|
||||
total += len(footer_text)
|
||||
|
||||
try:
|
||||
author = self._author
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
total += len(author['name'])
|
||||
total += len(author["name"])
|
||||
|
||||
return total
|
||||
|
||||
@@ -308,7 +304,7 @@ class Embed:
|
||||
|
||||
@property
|
||||
def colour(self) -> MaybeEmpty[Colour]:
|
||||
return getattr(self, '_colour', EmptyEmbed)
|
||||
return getattr(self, "_colour", EmptyEmbed)
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
|
||||
@@ -317,17 +313,23 @@ class Embed:
|
||||
elif isinstance(value, int):
|
||||
self._colour = Colour(value=value)
|
||||
else:
|
||||
raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.')
|
||||
raise TypeError(
|
||||
f"Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead."
|
||||
)
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def timestamp(self) -> MaybeEmpty[datetime.datetime]:
|
||||
return getattr(self, '_timestamp', EmptyEmbed)
|
||||
return getattr(self, "_timestamp", EmptyEmbed)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, value: MaybeEmpty[datetime.datetime]):
|
||||
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
|
||||
if isinstance(value, datetime.datetime):
|
||||
if value.tzinfo is None:
|
||||
value = value.astimezone()
|
||||
self._timestamp = value
|
||||
elif isinstance(value, _EmptyEmbed):
|
||||
self._timestamp = value
|
||||
else:
|
||||
raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
|
||||
@@ -340,9 +342,9 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_footer", {})) # type: ignore
|
||||
|
||||
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
def set_footer(self: E, *, text: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> E:
|
||||
"""Sets the footer for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -358,10 +360,25 @@ class Embed:
|
||||
|
||||
self._footer = {}
|
||||
if text is not EmptyEmbed:
|
||||
self._footer['text'] = str(text)
|
||||
self._footer["text"] = str(text)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._footer['icon_url'] = str(icon_url)
|
||||
self._footer["icon_url"] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
def remove_footer(self: E) -> E:
|
||||
"""Clears embed's footer information.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
try:
|
||||
del self._footer
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return self
|
||||
|
||||
@@ -378,9 +395,23 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_image", {})) # type: ignore
|
||||
|
||||
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
@image.setter
|
||||
def image(self, url: Any):
|
||||
if url is EmptyEmbed:
|
||||
del self.image
|
||||
else:
|
||||
self._image = {"url": str(url)}
|
||||
|
||||
@image.deleter
|
||||
def image(self):
|
||||
try:
|
||||
del self._image
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -395,16 +426,7 @@ class Embed:
|
||||
The source URL for the image. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
if url is EmptyEmbed:
|
||||
try:
|
||||
del self._image
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._image = {
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
self.image = url
|
||||
return self
|
||||
|
||||
@property
|
||||
@@ -420,9 +442,23 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_thumbnail", {})) # type: ignore
|
||||
|
||||
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
|
||||
@thumbnail.setter
|
||||
def thumbnail(self, url: Any):
|
||||
if url is EmptyEmbed:
|
||||
del self.thumbnail
|
||||
else:
|
||||
self._thumbnail = {"url": str(url)}
|
||||
|
||||
@thumbnail.deleter
|
||||
def thumbnail(self):
|
||||
try:
|
||||
del self._thumbnail
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def set_thumbnail(self, *, url: MaybeEmpty[Any]):
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -437,16 +473,7 @@ class Embed:
|
||||
The source URL for the thumbnail. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
if url is EmptyEmbed:
|
||||
try:
|
||||
del self._thumbnail
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._thumbnail = {
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
self.thumbnail = url
|
||||
return self
|
||||
|
||||
@property
|
||||
@@ -461,7 +488,7 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_video', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_video", {})) # type: ignore
|
||||
|
||||
@property
|
||||
def provider(self) -> _EmbedProviderProxy:
|
||||
@@ -471,7 +498,7 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_provider", {})) # type: ignore
|
||||
|
||||
@property
|
||||
def author(self) -> _EmbedAuthorProxy:
|
||||
@@ -481,9 +508,11 @@ class Embed:
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
|
||||
return EmbedProxy(getattr(self, "_author", {})) # type: ignore
|
||||
|
||||
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
|
||||
def set_author(
|
||||
self: E, *, name: Any, url: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed
|
||||
) -> E:
|
||||
"""Sets the author for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -500,14 +529,14 @@ class Embed:
|
||||
"""
|
||||
|
||||
self._author = {
|
||||
'name': str(name),
|
||||
"name": str(name),
|
||||
}
|
||||
|
||||
if url is not EmptyEmbed:
|
||||
self._author['url'] = str(url)
|
||||
self._author["url"] = str(url)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._author['icon_url'] = str(icon_url)
|
||||
self._author["icon_url"] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
@@ -528,15 +557,15 @@ class Embed:
|
||||
|
||||
@property
|
||||
def fields(self) -> List[_EmbedFieldProxy]:
|
||||
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
||||
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
|
||||
|
||||
See :meth:`add_field` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
|
||||
return [EmbedProxy(d) for d in getattr(self, "_fields", [])] # type: ignore
|
||||
|
||||
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
|
||||
def add_field(self: E, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||
"""Adds a field to the embed object.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -553,9 +582,9 @@ class Embed:
|
||||
"""
|
||||
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value),
|
||||
"inline": inline,
|
||||
"name": str(name),
|
||||
"value": str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -565,7 +594,7 @@ class Embed:
|
||||
|
||||
return self
|
||||
|
||||
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
def insert_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||
"""Inserts a field before a specified index to the embed.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
@@ -586,9 +615,9 @@ class Embed:
|
||||
"""
|
||||
|
||||
field = {
|
||||
'inline': inline,
|
||||
'name': str(name),
|
||||
'value': str(value),
|
||||
"inline": inline,
|
||||
"name": str(name),
|
||||
"value": str(value),
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -626,7 +655,7 @@ class Embed:
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
|
||||
def set_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
|
||||
"""Modifies a field to the embed object.
|
||||
|
||||
The index must point to a valid pre-existing field.
|
||||
@@ -654,11 +683,11 @@ class Embed:
|
||||
try:
|
||||
field = self._fields[index]
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
raise IndexError('field index out of range')
|
||||
raise IndexError("field index out of range")
|
||||
|
||||
field['name'] = str(name)
|
||||
field['value'] = str(value)
|
||||
field['inline'] = inline
|
||||
field["name"] = str(name)
|
||||
field["value"] = str(value)
|
||||
field["inline"] = inline
|
||||
return self
|
||||
|
||||
def to_dict(self) -> EmbedData:
|
||||
@@ -676,35 +705,35 @@ class Embed:
|
||||
# deal with basic convenience wrappers
|
||||
|
||||
try:
|
||||
colour = result.pop('colour')
|
||||
colour = result.pop("colour")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if colour:
|
||||
result['color'] = colour.value
|
||||
result["color"] = colour.value
|
||||
|
||||
try:
|
||||
timestamp = result.pop('timestamp')
|
||||
timestamp = result.pop("timestamp")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if timestamp:
|
||||
if timestamp.tzinfo:
|
||||
result['timestamp'] = timestamp.astimezone(tz=datetime.timezone.utc).isoformat()
|
||||
result["timestamp"] = timestamp.astimezone(tz=datetime.timezone.utc).isoformat()
|
||||
else:
|
||||
result['timestamp'] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()
|
||||
result["timestamp"] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
# add in the non raw attribute ones
|
||||
if self.type:
|
||||
result['type'] = self.type
|
||||
result["type"] = self.type
|
||||
|
||||
if self.description:
|
||||
result['description'] = self.description
|
||||
result["description"] = self.description
|
||||
|
||||
if self.url:
|
||||
result['url'] = self.url
|
||||
result["url"] = self.url
|
||||
|
||||
if self.title:
|
||||
result['title'] = self.title
|
||||
result["title"] = self.title
|
||||
|
||||
return result # type: ignore
|
||||
|
||||
182
discord/emoji.py
182
discord/emoji.py
@@ -22,16 +22,26 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .asset import Asset
|
||||
from . import utils
|
||||
from .partial_emoji import _EmojiTag
|
||||
from __future__ import annotations
|
||||
from typing import Any, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from .asset import Asset, AssetMixin
|
||||
from .utils import SnowflakeList, snowflake_time, MISSING
|
||||
from .partial_emoji import _EmojiTag, PartialEmoji
|
||||
from .user import User
|
||||
|
||||
__all__ = (
|
||||
'Emoji',
|
||||
)
|
||||
__all__ = ("Emoji",)
|
||||
|
||||
class Emoji(_EmojiTag):
|
||||
if TYPE_CHECKING:
|
||||
from .types.emoji import Emoji as EmojiPayload
|
||||
from .guild import Guild
|
||||
from .state import ConnectionState
|
||||
from .abc import Snowflake
|
||||
from .role import Role
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Emoji(_EmojiTag, AssetMixin):
|
||||
"""Represents a custom emoji.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
@@ -60,6 +70,10 @@ class Emoji(_EmojiTag):
|
||||
|
||||
Returns the emoji rendered for discord.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the emoji ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@@ -80,68 +94,79 @@ class Emoji(_EmojiTag):
|
||||
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
|
||||
having the :attr:`~Permissions.manage_emojis` permission.
|
||||
"""
|
||||
__slots__ = ('require_colons', 'animated', 'managed', 'id', 'name', '_roles', 'guild_id',
|
||||
'_state', 'user', 'available')
|
||||
|
||||
def __init__(self, *, guild, state, data):
|
||||
self.guild_id = guild.id
|
||||
self._state = state
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"require_colons",
|
||||
"animated",
|
||||
"managed",
|
||||
"id",
|
||||
"name",
|
||||
"_roles",
|
||||
"guild_id",
|
||||
"_state",
|
||||
"user",
|
||||
"available",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload):
|
||||
self.guild_id: int = guild.id
|
||||
self._state: ConnectionState = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, emoji):
|
||||
self.require_colons = emoji['require_colons']
|
||||
self.managed = emoji['managed']
|
||||
self.id = int(emoji['id'])
|
||||
self.name = emoji['name']
|
||||
self.animated = emoji.get('animated', False)
|
||||
self.available = emoji.get('available', True)
|
||||
self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))
|
||||
user = emoji.get('user')
|
||||
self.user = User(state=self._state, data=user) if user else None
|
||||
def _from_data(self, emoji: EmojiPayload):
|
||||
self.require_colons: bool = emoji.get("require_colons", False)
|
||||
self.managed: bool = emoji.get("managed", False)
|
||||
self.id: int = int(emoji["id"]) # type: ignore
|
||||
self.name: str = emoji["name"] # type: ignore
|
||||
self.animated: bool = emoji.get("animated", False)
|
||||
self.available: bool = emoji.get("available", True)
|
||||
self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get("roles", [])))
|
||||
user = emoji.get("user")
|
||||
self.user: Optional[User] = User(state=self._state, data=user) if user else None
|
||||
|
||||
def _iterator(self):
|
||||
def _to_partial(self) -> PartialEmoji:
|
||||
return PartialEmoji(name=self.name, animated=self.animated, id=self.id)
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
for attr in self.__slots__:
|
||||
if attr[0] != '_':
|
||||
if attr[0] != "_":
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None:
|
||||
yield (attr, value)
|
||||
|
||||
def __iter__(self):
|
||||
return self._iterator()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.animated:
|
||||
return '<a:{0.name}:{0.id}>'.format(self)
|
||||
return "<:{0.name}:{0.id}>".format(self)
|
||||
return f"<a:{self.name}:{self.id}>"
|
||||
return f"<:{self.name}:{self.id}>"
|
||||
|
||||
def __repr__(self):
|
||||
return '<Emoji id={0.id} name={0.name!r} animated={0.animated} managed={0.managed}>'.format(self)
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>"
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, _EmojiTag) and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return self.id >> 22
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime:
|
||||
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`Asset`: Returns the asset of the emoji.
|
||||
|
||||
This is equivalent to calling :meth:`url_as` with
|
||||
the default parameters (i.e. png/gif detection).
|
||||
"""
|
||||
return self.url_as(format=None)
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the URL of the emoji."""
|
||||
fmt = "gif" if self.animated else "png"
|
||||
return f"{Asset.BASE}/emojis/{self.id}.{fmt}"
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
def roles(self) -> List[Role]:
|
||||
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
|
||||
|
||||
If roles is empty, the emoji is unrestricted.
|
||||
@@ -153,44 +178,11 @@ class Emoji(_EmojiTag):
|
||||
return [role for role in guild.roles if self._roles.has(role.id)]
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
def guild(self) -> Guild:
|
||||
""":class:`Guild`: The guild this emoji belongs to."""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
|
||||
def url_as(self, *, format=None, static_format="png"):
|
||||
"""Returns an :class:`Asset` for the emoji's url.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
||||
'gif' is only valid for animated emojis.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the emojis to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected as either 'gif' or static_format, depending on whether the
|
||||
emoji is animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated emoji's to.
|
||||
Defaults to 'png'
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or ``static_format``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
||||
|
||||
|
||||
def is_usable(self):
|
||||
def is_usable(self) -> bool:
|
||||
""":class:`bool`: Whether the bot can use this emoji.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -202,7 +194,7 @@ class Emoji(_EmojiTag):
|
||||
emoji_roles, my_roles = self._roles, self.guild.me._roles
|
||||
return any(my_roles.has(role_id) for role_id in emoji_roles)
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the custom emoji.
|
||||
@@ -225,7 +217,9 @@ class Emoji(_EmojiTag):
|
||||
|
||||
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
||||
|
||||
async def edit(self, *, name=None, roles=None, reason=None):
|
||||
async def edit(
|
||||
self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None
|
||||
) -> Emoji:
|
||||
r"""|coro|
|
||||
|
||||
Edits the custom emoji.
|
||||
@@ -233,12 +227,15 @@ class Emoji(_EmojiTag):
|
||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
||||
do this.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The newly updated emoji is returned.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The new emoji name.
|
||||
roles: Optional[list[:class:`Role`]]
|
||||
A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone.
|
||||
roles: Optional[List[:class:`~discord.abc.Snowflake`]]
|
||||
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this emoji. Shows up on the audit log.
|
||||
|
||||
@@ -248,9 +245,18 @@ class Emoji(_EmojiTag):
|
||||
You are not allowed to edit emojis.
|
||||
HTTPException
|
||||
An error occurred editing the emoji.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Emoji`
|
||||
The newly updated emoji.
|
||||
"""
|
||||
|
||||
name = name or self.name
|
||||
if roles:
|
||||
roles = [role.id for role in roles]
|
||||
await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason)
|
||||
payload = {}
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
if roles is not MISSING:
|
||||
payload["roles"] = [role.id for role in roles]
|
||||
|
||||
data = await self._state.http.edit_custom_emoji(self.guild.id, self.id, payload=payload, reason=reason)
|
||||
return Emoji(guild=self.guild, data=data, state=self._state)
|
||||
|
||||
490
discord/enums.py
490
discord/enums.py
@@ -24,49 +24,72 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import types
|
||||
from collections import namedtuple
|
||||
from typing import Any, TYPE_CHECKING, Type, TypeVar
|
||||
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Type, TypeVar
|
||||
|
||||
__all__ = (
|
||||
'Enum',
|
||||
'ChannelType',
|
||||
'MessageType',
|
||||
'VoiceRegion',
|
||||
'SpeakingState',
|
||||
'VerificationLevel',
|
||||
'ContentFilter',
|
||||
'Status',
|
||||
'DefaultAvatar',
|
||||
'AuditLogAction',
|
||||
'AuditLogActionCategory',
|
||||
'UserFlags',
|
||||
'ActivityType',
|
||||
'NotificationLevel',
|
||||
'TeamMembershipState',
|
||||
'WebhookType',
|
||||
'ExpireBehaviour',
|
||||
'ExpireBehavior',
|
||||
'StickerType',
|
||||
"Enum",
|
||||
"ChannelType",
|
||||
"MessageType",
|
||||
"VoiceRegion",
|
||||
"SpeakingState",
|
||||
"VerificationLevel",
|
||||
"ContentFilter",
|
||||
"Status",
|
||||
"DefaultAvatar",
|
||||
"AuditLogAction",
|
||||
"AuditLogActionCategory",
|
||||
"UserFlags",
|
||||
"ActivityType",
|
||||
"NotificationLevel",
|
||||
"TeamMembershipState",
|
||||
"WebhookType",
|
||||
"ExpireBehaviour",
|
||||
"ExpireBehavior",
|
||||
"StickerType",
|
||||
"StickerFormatType",
|
||||
"InviteTarget",
|
||||
"VideoQualityMode",
|
||||
"ComponentType",
|
||||
"ButtonStyle",
|
||||
"StagePrivacyLevel",
|
||||
"InteractionType",
|
||||
"InteractionResponseType",
|
||||
"NSFWLevel",
|
||||
)
|
||||
|
||||
def _create_value_cls(name):
|
||||
cls = namedtuple('_EnumValue_' + name, 'name value')
|
||||
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
|
||||
cls.__str__ = lambda self: f'{name}.{self.name}'
|
||||
|
||||
def _create_value_cls(name, comparable):
|
||||
cls = namedtuple("_EnumValue_" + name, "name value")
|
||||
cls.__repr__ = lambda self: f"<{name}.{self.name}: {self.value!r}>"
|
||||
cls.__str__ = lambda self: f"{name}.{self.name}"
|
||||
if comparable:
|
||||
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value
|
||||
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value
|
||||
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value
|
||||
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value
|
||||
return cls
|
||||
|
||||
|
||||
def _is_descriptor(obj):
|
||||
return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')
|
||||
return hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__")
|
||||
|
||||
|
||||
class EnumMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
if TYPE_CHECKING:
|
||||
__name__: ClassVar[str]
|
||||
_enum_member_names_: ClassVar[List[str]]
|
||||
_enum_member_map_: ClassVar[Dict[str, Any]]
|
||||
_enum_value_map_: ClassVar[Dict[Any, Any]]
|
||||
|
||||
def __new__(cls, name, bases, attrs, *, comparable: bool = False):
|
||||
value_mapping = {}
|
||||
member_mapping = {}
|
||||
member_names = []
|
||||
|
||||
value_cls = _create_value_cls(name)
|
||||
value_cls = _create_value_cls(name, comparable)
|
||||
for key, value in list(attrs.items()):
|
||||
is_descriptor = _is_descriptor(value)
|
||||
if key[0] == '_' and not is_descriptor:
|
||||
if key[0] == "_" and not is_descriptor:
|
||||
continue
|
||||
|
||||
# Special case classmethod to just pass through
|
||||
@@ -88,12 +111,12 @@ class EnumMeta(type):
|
||||
member_mapping[key] = new_value
|
||||
attrs[key] = new_value
|
||||
|
||||
attrs['_enum_value_map_'] = value_mapping
|
||||
attrs['_enum_member_map_'] = member_mapping
|
||||
attrs['_enum_member_names_'] = member_names
|
||||
attrs['_enum_value_cls_'] = value_cls
|
||||
attrs["_enum_value_map_"] = value_mapping
|
||||
attrs["_enum_member_map_"] = member_mapping
|
||||
attrs["_enum_member_names_"] = member_names
|
||||
attrs["_enum_value_cls_"] = value_cls
|
||||
actual_cls = super().__new__(cls, name, bases, attrs)
|
||||
value_cls._actual_enum_cls_ = actual_cls
|
||||
value_cls._actual_enum_cls_ = actual_cls # type: ignore
|
||||
return actual_cls
|
||||
|
||||
def __iter__(cls):
|
||||
@@ -106,7 +129,7 @@ class EnumMeta(type):
|
||||
return len(cls._enum_member_names_)
|
||||
|
||||
def __repr__(cls):
|
||||
return f'<enum {cls.__name__}>'
|
||||
return f"<enum {cls.__name__}>"
|
||||
|
||||
@property
|
||||
def __members__(cls):
|
||||
@@ -122,10 +145,10 @@ class EnumMeta(type):
|
||||
return cls._enum_member_map_[key]
|
||||
|
||||
def __setattr__(cls, name, value):
|
||||
raise TypeError('Enums are immutable.')
|
||||
raise TypeError("Enums are immutable.")
|
||||
|
||||
def __delattr__(cls, attr):
|
||||
raise TypeError('Enums are immutable')
|
||||
raise TypeError("Enums are immutable")
|
||||
|
||||
def __instancecheck__(self, instance):
|
||||
# isinstance(x, Y)
|
||||
@@ -135,9 +158,11 @@ class EnumMeta(type):
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enum import Enum
|
||||
else:
|
||||
|
||||
class Enum(metaclass=EnumMeta):
|
||||
@classmethod
|
||||
def try_value(cls, value):
|
||||
@@ -146,72 +171,84 @@ else:
|
||||
except (KeyError, TypeError):
|
||||
return value
|
||||
|
||||
|
||||
class ChannelType(Enum):
|
||||
text = 0
|
||||
private = 1
|
||||
voice = 2
|
||||
group = 3
|
||||
text = 0
|
||||
private = 1
|
||||
voice = 2
|
||||
group = 3
|
||||
category = 4
|
||||
news = 5
|
||||
store = 6
|
||||
news = 5
|
||||
store = 6
|
||||
news_thread = 10
|
||||
public_thread = 11
|
||||
private_thread = 12
|
||||
stage_voice = 13
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
default = 0
|
||||
recipient_add = 1
|
||||
recipient_remove = 2
|
||||
call = 3
|
||||
channel_name_change = 4
|
||||
channel_icon_change = 5
|
||||
pins_add = 6
|
||||
new_member = 7
|
||||
premium_guild_subscription = 8
|
||||
premium_guild_tier_1 = 9
|
||||
premium_guild_tier_2 = 10
|
||||
premium_guild_tier_3 = 11
|
||||
channel_follow_add = 12
|
||||
guild_stream = 13
|
||||
guild_discovery_disqualified = 14
|
||||
guild_discovery_requalified = 15
|
||||
default = 0
|
||||
recipient_add = 1
|
||||
recipient_remove = 2
|
||||
call = 3
|
||||
channel_name_change = 4
|
||||
channel_icon_change = 5
|
||||
pins_add = 6
|
||||
new_member = 7
|
||||
premium_guild_subscription = 8
|
||||
premium_guild_tier_1 = 9
|
||||
premium_guild_tier_2 = 10
|
||||
premium_guild_tier_3 = 11
|
||||
channel_follow_add = 12
|
||||
guild_stream = 13
|
||||
guild_discovery_disqualified = 14
|
||||
guild_discovery_requalified = 15
|
||||
guild_discovery_grace_period_initial_warning = 16
|
||||
guild_discovery_grace_period_final_warning = 17
|
||||
guild_discovery_grace_period_final_warning = 17
|
||||
thread_created = 18
|
||||
reply = 19
|
||||
application_command = 20
|
||||
thread_starter_message = 21
|
||||
guild_invite_reminder = 22
|
||||
|
||||
|
||||
class VoiceRegion(Enum):
|
||||
us_west = 'us-west'
|
||||
us_east = 'us-east'
|
||||
us_south = 'us-south'
|
||||
us_central = 'us-central'
|
||||
eu_west = 'eu-west'
|
||||
eu_central = 'eu-central'
|
||||
singapore = 'singapore'
|
||||
london = 'london'
|
||||
sydney = 'sydney'
|
||||
amsterdam = 'amsterdam'
|
||||
frankfurt = 'frankfurt'
|
||||
brazil = 'brazil'
|
||||
hongkong = 'hongkong'
|
||||
russia = 'russia'
|
||||
japan = 'japan'
|
||||
southafrica = 'southafrica'
|
||||
south_korea = 'south-korea'
|
||||
india = 'india'
|
||||
europe = 'europe'
|
||||
dubai = 'dubai'
|
||||
vip_us_east = 'vip-us-east'
|
||||
vip_us_west = 'vip-us-west'
|
||||
vip_amsterdam = 'vip-amsterdam'
|
||||
us_west = "us-west"
|
||||
us_east = "us-east"
|
||||
us_south = "us-south"
|
||||
us_central = "us-central"
|
||||
eu_west = "eu-west"
|
||||
eu_central = "eu-central"
|
||||
singapore = "singapore"
|
||||
london = "london"
|
||||
sydney = "sydney"
|
||||
amsterdam = "amsterdam"
|
||||
frankfurt = "frankfurt"
|
||||
brazil = "brazil"
|
||||
hongkong = "hongkong"
|
||||
russia = "russia"
|
||||
japan = "japan"
|
||||
southafrica = "southafrica"
|
||||
south_korea = "south-korea"
|
||||
india = "india"
|
||||
europe = "europe"
|
||||
dubai = "dubai"
|
||||
vip_us_east = "vip-us-east"
|
||||
vip_us_west = "vip-us-west"
|
||||
vip_amsterdam = "vip-amsterdam"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class SpeakingState(Enum):
|
||||
none = 0
|
||||
voice = 1
|
||||
none = 0
|
||||
voice = 1
|
||||
soundshare = 2
|
||||
priority = 4
|
||||
priority = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -219,59 +256,64 @@ class SpeakingState(Enum):
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
class VerificationLevel(Enum):
|
||||
none = 0
|
||||
low = 1
|
||||
medium = 2
|
||||
high = 3
|
||||
table_flip = 3
|
||||
extreme = 4
|
||||
double_table_flip = 4
|
||||
very_high = 4
|
||||
|
||||
class VerificationLevel(Enum, comparable=True):
|
||||
none = 0
|
||||
low = 1
|
||||
medium = 2
|
||||
high = 3
|
||||
highest = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class ContentFilter(Enum):
|
||||
disabled = 0
|
||||
no_role = 1
|
||||
|
||||
class ContentFilter(Enum, comparable=True):
|
||||
disabled = 0
|
||||
no_role = 1
|
||||
all_members = 2
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
online = 'online'
|
||||
offline = 'offline'
|
||||
idle = 'idle'
|
||||
dnd = 'dnd'
|
||||
do_not_disturb = 'dnd'
|
||||
invisible = 'invisible'
|
||||
online = "online"
|
||||
offline = "offline"
|
||||
idle = "idle"
|
||||
dnd = "dnd"
|
||||
do_not_disturb = "dnd"
|
||||
invisible = "invisible"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class DefaultAvatar(Enum):
|
||||
blurple = 0
|
||||
grey = 1
|
||||
gray = 1
|
||||
green = 2
|
||||
orange = 3
|
||||
red = 4
|
||||
grey = 1
|
||||
gray = 1
|
||||
green = 2
|
||||
orange = 3
|
||||
red = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class NotificationLevel(Enum):
|
||||
all_messages = 0
|
||||
|
||||
class NotificationLevel(Enum, comparable=True):
|
||||
all_messages = 0
|
||||
only_mentions = 1
|
||||
|
||||
|
||||
class AuditLogActionCategory(Enum):
|
||||
create = 1
|
||||
delete = 2
|
||||
update = 3
|
||||
|
||||
|
||||
class AuditLogAction(Enum):
|
||||
# fmt: off
|
||||
guild_update = 1
|
||||
channel_create = 10
|
||||
channel_update = 11
|
||||
@@ -307,71 +349,101 @@ class AuditLogAction(Enum):
|
||||
integration_create = 80
|
||||
integration_update = 81
|
||||
integration_delete = 82
|
||||
stage_instance_create = 83
|
||||
stage_instance_update = 84
|
||||
stage_instance_delete = 85
|
||||
sticker_create = 90
|
||||
sticker_update = 91
|
||||
sticker_delete = 92
|
||||
thread_create = 110
|
||||
thread_update = 111
|
||||
thread_delete = 112
|
||||
# fmt: on
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
lookup = {
|
||||
AuditLogAction.guild_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.channel_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.kick: None,
|
||||
AuditLogAction.member_prune: None,
|
||||
AuditLogAction.ban: None,
|
||||
AuditLogAction.unban: None,
|
||||
AuditLogAction.member_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_move: None,
|
||||
AuditLogAction.member_disconnect: None,
|
||||
AuditLogAction.bot_add: None,
|
||||
AuditLogAction.role_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.role_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.invite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.invite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.webhook_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.webhook_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.emoji_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.emoji_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_pin: None,
|
||||
AuditLogAction.message_unpin: None,
|
||||
AuditLogAction.integration_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.integration_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
|
||||
def category(self) -> Optional[AuditLogActionCategory]:
|
||||
# fmt: off
|
||||
lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = {
|
||||
AuditLogAction.guild_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.channel_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.kick: None,
|
||||
AuditLogAction.member_prune: None,
|
||||
AuditLogAction.ban: None,
|
||||
AuditLogAction.unban: None,
|
||||
AuditLogAction.member_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.member_move: None,
|
||||
AuditLogAction.member_disconnect: None,
|
||||
AuditLogAction.bot_add: None,
|
||||
AuditLogAction.role_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.role_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.role_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.invite_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.invite_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.webhook_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.webhook_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.emoji_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.emoji_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.message_pin: None,
|
||||
AuditLogAction.message_unpin: None,
|
||||
AuditLogAction.integration_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.integration_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.sticker_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.sticker_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.sticker_delete: AuditLogActionCategory.delete,
|
||||
AuditLogAction.thread_create: AuditLogActionCategory.create,
|
||||
AuditLogAction.thread_update: AuditLogActionCategory.update,
|
||||
AuditLogAction.thread_delete: AuditLogActionCategory.delete,
|
||||
}
|
||||
# fmt: on
|
||||
return lookup[self]
|
||||
|
||||
@property
|
||||
def target_type(self):
|
||||
def target_type(self) -> Optional[str]:
|
||||
v = self.value
|
||||
if v == -1:
|
||||
return 'all'
|
||||
return "all"
|
||||
elif v < 10:
|
||||
return 'guild'
|
||||
return "guild"
|
||||
elif v < 20:
|
||||
return 'channel'
|
||||
return "channel"
|
||||
elif v < 30:
|
||||
return 'user'
|
||||
return "user"
|
||||
elif v < 40:
|
||||
return 'role'
|
||||
return "role"
|
||||
elif v < 50:
|
||||
return 'invite'
|
||||
return "invite"
|
||||
elif v < 60:
|
||||
return 'webhook'
|
||||
return "webhook"
|
||||
elif v < 70:
|
||||
return 'emoji'
|
||||
return "emoji"
|
||||
elif v == 73:
|
||||
return "channel"
|
||||
elif v < 80:
|
||||
return 'message'
|
||||
return "message"
|
||||
elif v < 83:
|
||||
return "integration"
|
||||
elif v < 90:
|
||||
return 'integration'
|
||||
return "stage_instance"
|
||||
elif v < 93:
|
||||
return "sticker"
|
||||
elif v < 113:
|
||||
return "thread"
|
||||
|
||||
|
||||
class UserFlags(Enum):
|
||||
staff = 1
|
||||
@@ -390,6 +462,8 @@ class UserFlags(Enum):
|
||||
bug_hunter_level_2 = 16384
|
||||
verified_bot = 65536
|
||||
verified_bot_developer = 131072
|
||||
discord_certified_moderator = 262144
|
||||
|
||||
|
||||
class ActivityType(Enum):
|
||||
unknown = -1
|
||||
@@ -403,36 +477,128 @@ class ActivityType(Enum):
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class TeamMembershipState(Enum):
|
||||
invited = 1
|
||||
accepted = 2
|
||||
|
||||
|
||||
class WebhookType(Enum):
|
||||
incoming = 1
|
||||
channel_follower = 2
|
||||
application = 3
|
||||
|
||||
|
||||
class ExpireBehaviour(Enum):
|
||||
remove_role = 0
|
||||
kick = 1
|
||||
|
||||
|
||||
ExpireBehavior = ExpireBehaviour
|
||||
|
||||
|
||||
class StickerType(Enum):
|
||||
standard = 1
|
||||
guild = 2
|
||||
|
||||
|
||||
class StickerFormatType(Enum):
|
||||
png = 1
|
||||
apng = 2
|
||||
lottie = 3
|
||||
|
||||
@property
|
||||
def file_extension(self) -> str:
|
||||
# fmt: off
|
||||
lookup: Dict[StickerFormatType, str] = {
|
||||
StickerFormatType.png: 'png',
|
||||
StickerFormatType.apng: 'png',
|
||||
StickerFormatType.lottie: 'json',
|
||||
}
|
||||
# fmt: on
|
||||
return lookup[self]
|
||||
|
||||
|
||||
class InviteTarget(Enum):
|
||||
unknown = 0
|
||||
stream = 1
|
||||
embedded_application = 2
|
||||
|
||||
|
||||
class InteractionType(Enum):
|
||||
ping = 1
|
||||
application_command = 2
|
||||
component = 3
|
||||
|
||||
|
||||
class InteractionResponseType(Enum):
|
||||
pong = 1
|
||||
# ack = 2 (deprecated)
|
||||
# channel_message = 3 (deprecated)
|
||||
channel_message = 4 # (with source)
|
||||
deferred_channel_message = 5 # (with source)
|
||||
deferred_message_update = 6 # for components
|
||||
message_update = 7 # for components
|
||||
|
||||
|
||||
class VideoQualityMode(Enum):
|
||||
auto = 1
|
||||
full = 2
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class ComponentType(Enum):
|
||||
action_row = 1
|
||||
button = 2
|
||||
select = 3
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class ButtonStyle(Enum):
|
||||
primary = 1
|
||||
secondary = 2
|
||||
success = 3
|
||||
danger = 4
|
||||
link = 5
|
||||
|
||||
# Aliases
|
||||
blurple = 1
|
||||
grey = 2
|
||||
gray = 2
|
||||
green = 3
|
||||
red = 4
|
||||
url = 5
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class StagePrivacyLevel(Enum):
|
||||
public = 1
|
||||
closed = 2
|
||||
guild_only = 2
|
||||
|
||||
|
||||
class NSFWLevel(Enum, comparable=True):
|
||||
default = 0
|
||||
explicit = 1
|
||||
safe = 2
|
||||
age_restricted = 3
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def create_unknown_value(cls: Type[T], val: Any) -> T:
|
||||
value_cls = cls._enum_value_cls_ # type: ignore
|
||||
name = f'unknown_{val}'
|
||||
value_cls = cls._enum_value_cls_ # type: ignore
|
||||
name = f"unknown_{val}"
|
||||
return value_cls(name=name, value=val)
|
||||
|
||||
|
||||
def try_enum(cls: Type[T], val: Any) -> T:
|
||||
"""A function that tries to turn the value into enum ``cls``.
|
||||
|
||||
@@ -440,6 +606,6 @@ def try_enum(cls: Type[T], val: Any) -> T:
|
||||
"""
|
||||
|
||||
try:
|
||||
return cls._enum_value_map_[val] # type: ignore
|
||||
return cls._enum_value_map_[val] # type: ignore
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
return create_unknown_value(cls, val)
|
||||
|
||||
@@ -22,67 +22,91 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Any, Tuple, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohttp import ClientResponse, ClientWebSocketResponse
|
||||
|
||||
try:
|
||||
from requests import Response
|
||||
|
||||
_ResponseType = Union[ClientResponse, Response]
|
||||
except ModuleNotFoundError:
|
||||
_ResponseType = ClientResponse
|
||||
|
||||
from .interactions import Interaction
|
||||
|
||||
__all__ = (
|
||||
'DiscordException',
|
||||
'ClientException',
|
||||
'NoMoreItems',
|
||||
'GatewayNotFound',
|
||||
'HTTPException',
|
||||
'Forbidden',
|
||||
'NotFound',
|
||||
'DiscordServerError',
|
||||
'InvalidData',
|
||||
'InvalidArgument',
|
||||
'LoginFailure',
|
||||
'ConnectionClosed',
|
||||
'PrivilegedIntentsRequired',
|
||||
"DiscordException",
|
||||
"ClientException",
|
||||
"NoMoreItems",
|
||||
"GatewayNotFound",
|
||||
"HTTPException",
|
||||
"Forbidden",
|
||||
"NotFound",
|
||||
"DiscordServerError",
|
||||
"InvalidData",
|
||||
"InvalidArgument",
|
||||
"LoginFailure",
|
||||
"ConnectionClosed",
|
||||
"PrivilegedIntentsRequired",
|
||||
"InteractionResponded",
|
||||
)
|
||||
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for discord.py
|
||||
|
||||
Ideally speaking, this could be caught to handle any exceptions thrown from this library.
|
||||
Ideally speaking, this could be caught to handle any exceptions raised from this library.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ClientException(DiscordException):
|
||||
"""Exception that's thrown when an operation in the :class:`Client` fails.
|
||||
"""Exception that's raised when an operation in the :class:`Client` fails.
|
||||
|
||||
These are usually for exceptions that happened due to user input.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoMoreItems(DiscordException):
|
||||
"""Exception that is thrown when an async iteration operation has no more
|
||||
items."""
|
||||
"""Exception that is raised when an async iteration operation has no more items."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GatewayNotFound(DiscordException):
|
||||
"""An exception that is usually thrown when the gateway hub
|
||||
for the :class:`Client` websocket is not found."""
|
||||
"""An exception that is raised when the gateway for Discord could not be found"""
|
||||
|
||||
def __init__(self):
|
||||
message = 'The gateway to connect to discord was not found.'
|
||||
message = "The gateway to connect to discord was not found."
|
||||
super().__init__(message)
|
||||
|
||||
def flatten_error_dict(d, key=''):
|
||||
items = []
|
||||
|
||||
def _flatten_error_dict(d: Dict[str, Any], key: str = "") -> Dict[str, str]:
|
||||
items: List[Tuple[str, str]] = []
|
||||
for k, v in d.items():
|
||||
new_key = key + '.' + k if key else k
|
||||
new_key = key + "." + k if key else k
|
||||
|
||||
if isinstance(v, dict):
|
||||
try:
|
||||
_errors = v['_errors']
|
||||
_errors: List[Dict[str, Any]] = v["_errors"]
|
||||
except KeyError:
|
||||
items.extend(flatten_error_dict(v, new_key).items())
|
||||
items.extend(_flatten_error_dict(v, new_key).items())
|
||||
else:
|
||||
items.append((new_key, ' '.join(x.get('message', '') for x in _errors)))
|
||||
items.append((new_key, " ".join(x.get("message", "") for x in _errors)))
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
|
||||
return dict(items)
|
||||
|
||||
|
||||
class HTTPException(DiscordException):
|
||||
"""Exception that's thrown when an HTTP request operation fails.
|
||||
"""Exception that's raised when an HTTP request operation fails.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
@@ -99,77 +123,92 @@ class HTTPException(DiscordException):
|
||||
The Discord specific error code for the failure.
|
||||
"""
|
||||
|
||||
def __init__(self, response, message):
|
||||
self.response = response
|
||||
self.status = response.status
|
||||
def __init__(self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]):
|
||||
self.response: _ResponseType = response
|
||||
self.status: int = response.status # type: ignore
|
||||
self.code: int
|
||||
self.text: str
|
||||
if isinstance(message, dict):
|
||||
self.code = message.get('code', 0)
|
||||
base = message.get('message', '')
|
||||
errors = message.get('errors')
|
||||
self.code = message.get("code", 0)
|
||||
base = message.get("message", "")
|
||||
errors = message.get("errors")
|
||||
if errors:
|
||||
errors = flatten_error_dict(errors)
|
||||
helpful = '\n'.join('In %s: %s' % t for t in errors.items())
|
||||
self.text = base + '\n' + helpful
|
||||
errors = _flatten_error_dict(errors)
|
||||
helpful = "\n".join("In %s: %s" % t for t in errors.items())
|
||||
self.text = base + "\n" + helpful
|
||||
else:
|
||||
self.text = base
|
||||
else:
|
||||
self.text = message
|
||||
self.text = message or ""
|
||||
self.code = 0
|
||||
|
||||
fmt = '{0.status} {0.reason} (error code: {1})'
|
||||
fmt = "{0.status} {0.reason} (error code: {1})"
|
||||
if len(self.text):
|
||||
fmt += ': {2}'
|
||||
fmt += ": {2}"
|
||||
|
||||
super().__init__(fmt.format(self.response, self.code, self.text))
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""Exception that's thrown for when status code 403 occurs.
|
||||
"""Exception that's raised for when status code 403 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""Exception that's thrown for when status code 404 occurs.
|
||||
"""Exception that's raised for when status code 404 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DiscordServerError(HTTPException):
|
||||
"""Exception that's thrown for when a 500 range status code occurs.
|
||||
"""Exception that's raised for when a 500 range status code occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidData(ClientException):
|
||||
"""Exception that's raised when the library encounters unknown
|
||||
or invalid data from Discord.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidArgument(ClientException):
|
||||
"""Exception that's thrown when an argument to a function
|
||||
"""Exception that's raised when an argument to a function
|
||||
is invalid some way (e.g. wrong value or wrong type).
|
||||
|
||||
This could be considered the analogous of ``ValueError`` and
|
||||
``TypeError`` except inherited from :exc:`ClientException` and thus
|
||||
:exc:`DiscordException`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LoginFailure(ClientException):
|
||||
"""Exception that's thrown when the :meth:`Client.login` function
|
||||
"""Exception that's raised when the :meth:`Client.login` function
|
||||
fails to log you in from improper credentials or some other misc.
|
||||
failure.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionClosed(ClientException):
|
||||
"""Exception that's thrown when the gateway connection is
|
||||
"""Exception that's raised when the gateway connection is
|
||||
closed for reasons that could not be handled internally.
|
||||
|
||||
Attributes
|
||||
@@ -181,17 +220,19 @@ class ConnectionClosed(ClientException):
|
||||
shard_id: Optional[:class:`int`]
|
||||
The shard ID that got closed if applicable.
|
||||
"""
|
||||
def __init__(self, socket, *, shard_id, code=None):
|
||||
|
||||
def __init__(self, socket: ClientWebSocketResponse, *, shard_id: Optional[int], code: Optional[int] = None):
|
||||
# This exception is just the same exception except
|
||||
# reconfigured to subclass ClientException for users
|
||||
self.code = code or socket.close_code
|
||||
self.code: int = code or socket.close_code or -1
|
||||
# aiohttp doesn't seem to consistently provide close reason
|
||||
self.reason = ''
|
||||
self.shard_id = shard_id
|
||||
super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}')
|
||||
self.reason: str = ""
|
||||
self.shard_id: Optional[int] = shard_id
|
||||
super().__init__(f"Shard ID {self.shard_id} WebSocket closed with {self.code}")
|
||||
|
||||
|
||||
class PrivilegedIntentsRequired(ClientException):
|
||||
"""Exception that's thrown when the gateway is requesting privileged intents
|
||||
"""Exception that's raised when the gateway is requesting privileged intents
|
||||
but they're not ticked in the developer page yet.
|
||||
|
||||
Go to https://discord.com/developers/applications/ and enable the intents
|
||||
@@ -206,10 +247,31 @@ class PrivilegedIntentsRequired(ClientException):
|
||||
The shard ID that got closed if applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, shard_id):
|
||||
self.shard_id = shard_id
|
||||
msg = 'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the ' \
|
||||
'developer portal. It is recommended to go to https://discord.com/developers/applications/ ' \
|
||||
'and explicitly enable the privileged intents within your application\'s page. If this is not ' \
|
||||
'possible, then consider disabling the privileged intents instead.'
|
||||
def __init__(self, shard_id: Optional[int]):
|
||||
self.shard_id: Optional[int] = shard_id
|
||||
msg = (
|
||||
"Shard ID %s is requesting privileged intents that have not been explicitly enabled in the "
|
||||
"developer portal. It is recommended to go to https://discord.com/developers/applications/ "
|
||||
"and explicitly enable the privileged intents within your application's page. If this is not "
|
||||
"possible, then consider disabling the privileged intents instead."
|
||||
)
|
||||
super().__init__(msg % shard_id)
|
||||
|
||||
|
||||
class InteractionResponded(ClientException):
|
||||
"""Exception that's raised when sending another interaction response using
|
||||
:class:`InteractionResponse` when one has already been done before.
|
||||
|
||||
An interaction can only respond once.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
interaction: :class:`Interaction`
|
||||
The interaction that's already been responded to.
|
||||
"""
|
||||
|
||||
def __init__(self, interaction: Interaction):
|
||||
self.interaction: Interaction = interaction
|
||||
super().__init__("This interaction has already been responded to before")
|
||||
|
||||
@@ -16,3 +16,4 @@ from .help import *
|
||||
from .converter import *
|
||||
from .cooldowns import *
|
||||
from .cog import *
|
||||
from .flags import *
|
||||
|
||||
@@ -22,6 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
from typing import Any, Callable, Coroutine, TYPE_CHECKING, TypeVar, Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
from .cog import Cog
|
||||
from .errors import CommandError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
Coro = Coroutine[Any, Any, T]
|
||||
MaybeCoro = Union[T, Coro[T]]
|
||||
CoroFunc = Callable[..., Coro[Any]]
|
||||
|
||||
Check = Union[Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], Callable[["Context[Any]"], MaybeCoro[bool]]]
|
||||
Hook = Union[Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]]]
|
||||
Error = Union[
|
||||
Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]], Callable[["Context[Any]", "CommandError"], Coro[Any]]
|
||||
]
|
||||
|
||||
|
||||
# This is merely a tag type to avoid circular import issues.
|
||||
# Yes, this is a terrible solution but ultimately it is the only solution.
|
||||
class _BaseCommand:
|
||||
|
||||
@@ -22,38 +22,103 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
import inspect
|
||||
import importlib.util
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from discord.http import HTTPClient
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Iterable,
|
||||
Tuple,
|
||||
cast,
|
||||
Mapping,
|
||||
List,
|
||||
Dict,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
import discord
|
||||
from discord.types.interactions import (
|
||||
ApplicationCommandInteractionData,
|
||||
ApplicationCommandInteractionDataOption,
|
||||
EditApplicationCommand,
|
||||
_ApplicationCommandInteractionDataOptionString,
|
||||
)
|
||||
|
||||
from .core import GroupMixin
|
||||
from .view import StringView
|
||||
from .converter import Greedy
|
||||
from .view import StringView, supported_quotes
|
||||
from .context import Context
|
||||
from .flags import FlagConverter
|
||||
from . import errors
|
||||
from .help import HelpCommand, DefaultHelpCommand
|
||||
from .cog import Cog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import importlib.machinery
|
||||
|
||||
from discord.message import Message
|
||||
from ._types import (
|
||||
Check,
|
||||
CoroFunc,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'when_mentioned',
|
||||
'when_mentioned_or',
|
||||
'Bot',
|
||||
'AutoShardedBot',
|
||||
"when_mentioned",
|
||||
"when_mentioned_or",
|
||||
"Bot",
|
||||
"AutoShardedBot",
|
||||
)
|
||||
|
||||
def when_mentioned(bot, msg):
|
||||
MISSING: Any = discord.utils.MISSING
|
||||
|
||||
T = TypeVar("T")
|
||||
CFT = TypeVar("CFT", bound="CoroFunc")
|
||||
CXT = TypeVar("CXT", bound="Context")
|
||||
|
||||
|
||||
class _FakeSlashMessage(discord.PartialMessage):
|
||||
activity = application = edited_at = reference = webhook_id = None
|
||||
attachments = components = reactions = stickers = mentions = []
|
||||
author: Union[discord.User, discord.Member]
|
||||
tts = False
|
||||
|
||||
@classmethod
|
||||
def from_interaction(
|
||||
cls, interaction: discord.Interaction, channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread]
|
||||
):
|
||||
self = cls(channel=channel, id=interaction.id)
|
||||
assert interaction.user is not None
|
||||
self.author = interaction.user
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def when_mentioned(bot: Union[Bot, AutoShardedBot], msg: Message) -> List[str]:
|
||||
"""A callable that implements a command prefix equivalent to being mentioned.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
"""
|
||||
return [f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
|
||||
# bot.user will never be None when this is called
|
||||
return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "] # type: ignore
|
||||
|
||||
def when_mentioned_or(*prefixes):
|
||||
|
||||
def when_mentioned_or(*prefixes: str) -> Callable[[Union[Bot, AutoShardedBot], Message], List[str]]:
|
||||
"""A callable that implements when mentioned or other prefixes provided.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
@@ -82,6 +147,7 @@ def when_mentioned_or(*prefixes):
|
||||
----------
|
||||
:func:`.when_mentioned`
|
||||
"""
|
||||
|
||||
def inner(bot, msg):
|
||||
r = list(prefixes)
|
||||
r = when_mentioned(bot, msg) + r
|
||||
@@ -89,42 +155,87 @@ def when_mentioned_or(*prefixes):
|
||||
|
||||
return inner
|
||||
|
||||
def _is_submodule(parent, child):
|
||||
|
||||
def _is_submodule(parent: str, child: str) -> bool:
|
||||
return parent == child or child.startswith(parent + ".")
|
||||
|
||||
|
||||
def _unwrap_slash_groups(
|
||||
data: ApplicationCommandInteractionData,
|
||||
) -> Tuple[str, List[ApplicationCommandInteractionDataOption]]:
|
||||
command_name = data["name"]
|
||||
command_options = data.get("options") or []
|
||||
while any(o["type"] in {1, 2} for o in command_options): # type: ignore
|
||||
for option in command_options: # type: ignore
|
||||
if option["type"] in {1, 2}: # type: ignore
|
||||
command_name += f' {option["name"]}' # type: ignore
|
||||
command_options = option.get("options") or []
|
||||
|
||||
return command_name, command_options
|
||||
|
||||
|
||||
def _quote_string_safe(string: str) -> str:
|
||||
# we need to quote this string otherwise we may spill into
|
||||
# other parameters and cause all kinds of trouble, as many
|
||||
# quotes are supported and some may be in the option, we
|
||||
# loop through all supported quotes and if neither open or
|
||||
# close are in the string, we add them
|
||||
for open, close in supported_quotes.items():
|
||||
if open not in string and close not in string:
|
||||
return f"{open}{string}{close}"
|
||||
|
||||
# all supported quotes are in the message and we cannot add any
|
||||
# safely, very unlikely but still got to be covered
|
||||
raise errors.UnexpectedQuoteError(string)
|
||||
|
||||
|
||||
class _DefaultRepr:
|
||||
def __repr__(self):
|
||||
return '<default-help-command>'
|
||||
return "<default-help-command>"
|
||||
|
||||
|
||||
_default = _DefaultRepr()
|
||||
|
||||
|
||||
class BotBase(GroupMixin):
|
||||
def __init__(self, command_prefix, help_command=_default, description=None, **options):
|
||||
super().__init__(**options)
|
||||
def __init__(
|
||||
self,
|
||||
command_prefix,
|
||||
help_command=_default,
|
||||
description=None,
|
||||
*,
|
||||
intents: discord.Intents,
|
||||
message_commands: bool = True,
|
||||
slash_commands: bool = False,
|
||||
**options,
|
||||
):
|
||||
super().__init__(**options, intents=intents)
|
||||
|
||||
self.command_prefix = command_prefix
|
||||
self.extra_events = {}
|
||||
self.__cogs = {}
|
||||
self.__extensions = {}
|
||||
self._checks = []
|
||||
self.slash_commands = slash_commands
|
||||
self.message_commands = message_commands
|
||||
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
||||
self.__cogs: Dict[str, Cog] = {}
|
||||
self.__extensions: Dict[str, types.ModuleType] = {}
|
||||
self._checks: List[Check] = []
|
||||
self._check_once = []
|
||||
self._before_invoke = None
|
||||
self._after_invoke = None
|
||||
self._help_command = None
|
||||
self.description = inspect.cleandoc(description) if description else ''
|
||||
self.owner_id = options.get('owner_id')
|
||||
self.owner_ids = options.get('owner_ids', set())
|
||||
self.strip_after_prefix = options.get('strip_after_prefix', False)
|
||||
self.description = inspect.cleandoc(description) if description else ""
|
||||
self.owner_id = options.get("owner_id")
|
||||
self.owner_ids = options.get("owner_ids", set())
|
||||
self.strip_after_prefix = options.get("strip_after_prefix", False)
|
||||
self.slash_command_guilds: Optional[Iterable[int]] = options.get("slash_command_guilds", None)
|
||||
|
||||
if self.owner_id and self.owner_ids:
|
||||
raise TypeError('Both owner_id and owner_ids are set.')
|
||||
raise TypeError("Both owner_id and owner_ids are set.")
|
||||
|
||||
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
|
||||
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
|
||||
raise TypeError(f"owner_ids must be a collection not {self.owner_ids.__class__!r}")
|
||||
|
||||
if options.pop('self_bot', False):
|
||||
self._skip_check = lambda x, y: x != y
|
||||
else:
|
||||
self._skip_check = lambda x, y: x == y
|
||||
if not (message_commands or slash_commands):
|
||||
raise ValueError("Both message_commands and slash_commands are disabled.")
|
||||
|
||||
if help_command is _default:
|
||||
self.help_command = DefaultHelpCommand()
|
||||
@@ -133,13 +244,64 @@ class BotBase(GroupMixin):
|
||||
|
||||
# internal helpers
|
||||
|
||||
def dispatch(self, event_name, *args, **kwargs):
|
||||
super().dispatch(event_name, *args, **kwargs)
|
||||
ev = 'on_' + event_name
|
||||
def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None:
|
||||
# super() will resolve to Client
|
||||
super().dispatch(event_name, *args, **kwargs) # type: ignore
|
||||
ev = "on_" + event_name
|
||||
for event in self.extra_events.get(ev, []):
|
||||
self._schedule_event(event, ev, *args, **kwargs)
|
||||
self._schedule_event(event, ev, *args, **kwargs) # type: ignore
|
||||
|
||||
async def close(self):
|
||||
async def setup(self):
|
||||
await self.create_slash_commands()
|
||||
|
||||
async def create_slash_commands(self):
|
||||
commands: defaultdict[Optional[int], List[EditApplicationCommand]] = defaultdict(list)
|
||||
for command in self.commands:
|
||||
if command.hidden or (command.slash_command is None and not self.slash_commands):
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = command.to_application_command()
|
||||
except Exception:
|
||||
raise errors.ApplicationCommandRegistrationError(command)
|
||||
|
||||
if payload is None:
|
||||
continue
|
||||
|
||||
guilds = command.slash_command_guilds or self.slash_command_guilds
|
||||
if guilds is None:
|
||||
commands[None].append(payload)
|
||||
else:
|
||||
for guild in guilds:
|
||||
commands[guild].append(payload)
|
||||
|
||||
http: HTTPClient = self.http # type: ignore
|
||||
global_commands = commands.pop(None, None)
|
||||
application_id = self.application_id or (await self.application_info()).id # type: ignore
|
||||
if global_commands is not None:
|
||||
if self.slash_command_guilds is None:
|
||||
await http.bulk_upsert_global_commands(
|
||||
payload=global_commands,
|
||||
application_id=application_id,
|
||||
)
|
||||
else:
|
||||
for guild in self.slash_command_guilds:
|
||||
await http.bulk_upsert_guild_commands(
|
||||
guild_id=guild,
|
||||
payload=global_commands,
|
||||
application_id=application_id,
|
||||
)
|
||||
|
||||
for guild, guild_commands in commands.items():
|
||||
assert guild is not None
|
||||
await http.bulk_upsert_guild_commands(
|
||||
guild_id=guild,
|
||||
payload=guild_commands,
|
||||
application_id=application_id,
|
||||
)
|
||||
|
||||
@discord.utils.copy_doc(discord.Client.close)
|
||||
async def close(self) -> None:
|
||||
for extension in tuple(self.__extensions):
|
||||
try:
|
||||
self.unload_extension(extension)
|
||||
@@ -152,9 +314,9 @@ class BotBase(GroupMixin):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await super().close()
|
||||
await super().close() # type: ignore
|
||||
|
||||
async def on_command_error(self, context, exception):
|
||||
async def on_command_error(self, context: Context, exception: errors.CommandError) -> None:
|
||||
"""|coro|
|
||||
|
||||
The default command error handler provided by the bot.
|
||||
@@ -164,22 +326,23 @@ class BotBase(GroupMixin):
|
||||
|
||||
This only fires if you do not specify any listeners for command error.
|
||||
"""
|
||||
if self.extra_events.get('on_command_error', None):
|
||||
if self.extra_events.get("on_command_error", None):
|
||||
return
|
||||
|
||||
if hasattr(context.command, 'on_error'):
|
||||
command = context.command
|
||||
if command and command.has_error_handler():
|
||||
return
|
||||
|
||||
cog = context.cog
|
||||
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
if cog and cog.has_error_handler():
|
||||
return
|
||||
|
||||
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
|
||||
print(f"Ignoring exception in command {context.command}:", file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
# global check registration
|
||||
|
||||
def check(self, func):
|
||||
def check(self, func: T) -> T:
|
||||
r"""A decorator that adds a global check to the bot.
|
||||
|
||||
A global check is similar to a :func:`.check` that is applied
|
||||
@@ -204,10 +367,11 @@ class BotBase(GroupMixin):
|
||||
return ctx.command.qualified_name in allowed_commands
|
||||
|
||||
"""
|
||||
self.add_check(func)
|
||||
# T was used instead of Check to ensure the type matches on return
|
||||
self.add_check(func) # type: ignore
|
||||
return func
|
||||
|
||||
def add_check(self, func, *, call_once=False):
|
||||
def add_check(self, func: Check, *, call_once: bool = False) -> None:
|
||||
"""Adds a global check to the bot.
|
||||
|
||||
This is the non-decorator interface to :meth:`.check`
|
||||
@@ -219,7 +383,7 @@ class BotBase(GroupMixin):
|
||||
The function that was used as a global check.
|
||||
call_once: :class:`bool`
|
||||
If the function should only be called once per
|
||||
:meth:`.Command.invoke` call.
|
||||
:meth:`.invoke` call.
|
||||
"""
|
||||
|
||||
if call_once:
|
||||
@@ -227,7 +391,7 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self._checks.append(func)
|
||||
|
||||
def remove_check(self, func, *, call_once=False):
|
||||
def remove_check(self, func: Check, *, call_once: bool = False) -> None:
|
||||
"""Removes a global check from the bot.
|
||||
|
||||
This function is idempotent and will not raise an exception
|
||||
@@ -248,11 +412,11 @@ class BotBase(GroupMixin):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def check_once(self, func):
|
||||
def check_once(self, func: CFT) -> CFT:
|
||||
r"""A decorator that adds a "call once" global check to the bot.
|
||||
|
||||
Unlike regular global checks, this one is called only once
|
||||
per :meth:`.Command.invoke` call.
|
||||
per :meth:`.invoke` call.
|
||||
|
||||
Regular global checks are called whenever a command is called
|
||||
or :meth:`.Command.can_run` is called. This type of check
|
||||
@@ -286,15 +450,16 @@ class BotBase(GroupMixin):
|
||||
self.add_check(func, call_once=True)
|
||||
return func
|
||||
|
||||
async def can_run(self, ctx, *, call_once=False):
|
||||
async def can_run(self, ctx: Context, *, call_once: bool = False) -> bool:
|
||||
data = self._check_once if call_once else self._checks
|
||||
|
||||
if len(data) == 0:
|
||||
return True
|
||||
|
||||
return await discord.utils.async_all(f(ctx) for f in data)
|
||||
# type-checker doesn't distinguish between functions and methods
|
||||
return await discord.utils.async_all(f(ctx) for f in data) # type: ignore
|
||||
|
||||
async def is_owner(self, user):
|
||||
async def is_owner(self, user: discord.User) -> bool:
|
||||
"""|coro|
|
||||
|
||||
Checks if a :class:`~discord.User` or :class:`~discord.Member` is the owner of
|
||||
@@ -323,15 +488,61 @@ class BotBase(GroupMixin):
|
||||
elif self.owner_ids:
|
||||
return user.id in self.owner_ids
|
||||
else:
|
||||
app = await self.application_info()
|
||||
if app.team:
|
||||
self.owner_ids = ids = {m.id for m in app.team.members}
|
||||
return user.id in ids
|
||||
else:
|
||||
self.owner_id = owner_id = app.owner.id
|
||||
return user.id == owner_id
|
||||
# Populate the used fields, then retry the check. This is only done at-most once in the bot lifetime.
|
||||
await self.populate_owners()
|
||||
return await self.is_owner(user)
|
||||
|
||||
def before_invoke(self, coro):
|
||||
async def try_owners(self) -> List[discord.User]:
|
||||
"""|coro|
|
||||
|
||||
Returns a list of :class:`~discord.User` representing the owners of the bot.
|
||||
It uses the :attr:`owner_id` and :attr:`owner_ids`, if set.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
The function also checks if the application is team-owned if
|
||||
:attr:`owner_ids` is not set.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`~discord.User`]
|
||||
List of owners of the bot.
|
||||
"""
|
||||
if self.owner_id:
|
||||
owner = await self.try_user(self.owner_id)
|
||||
|
||||
if owner:
|
||||
return [owner]
|
||||
else:
|
||||
return []
|
||||
|
||||
elif self.owner_ids:
|
||||
owners = []
|
||||
|
||||
for owner_id in self.owner_ids:
|
||||
owner = await self.try_user(owner_id)
|
||||
if owner:
|
||||
owners.append(owner)
|
||||
|
||||
return owners
|
||||
else:
|
||||
# We didn't have owners cached yet, cache them and retry.
|
||||
await self.populate_owners()
|
||||
return await self.try_owners()
|
||||
|
||||
async def populate_owners(self):
|
||||
"""|coro|
|
||||
|
||||
Populate the :attr:`owner_id` and :attr:`owner_ids` through the use of :meth:`~.Bot.application_info`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
app = await self.application_info() # type: ignore
|
||||
if app.team:
|
||||
self.owner_ids = {m.id for m in app.team.members}
|
||||
else:
|
||||
self.owner_id = app.owner.id
|
||||
|
||||
def before_invoke(self, coro: CFT) -> CFT:
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
|
||||
A pre-invoke hook is called directly before the command is
|
||||
@@ -358,12 +569,12 @@ class BotBase(GroupMixin):
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise TypeError('The pre-invoke hook must be a coroutine.')
|
||||
raise TypeError("The pre-invoke hook must be a coroutine.")
|
||||
|
||||
self._before_invoke = coro
|
||||
return coro
|
||||
|
||||
def after_invoke(self, coro):
|
||||
def after_invoke(self, coro: CFT) -> CFT:
|
||||
r"""A decorator that registers a coroutine as a post-invoke hook.
|
||||
|
||||
A post-invoke hook is called directly after the command is
|
||||
@@ -391,21 +602,21 @@ class BotBase(GroupMixin):
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise TypeError('The post-invoke hook must be a coroutine.')
|
||||
raise TypeError("The post-invoke hook must be a coroutine.")
|
||||
|
||||
self._after_invoke = coro
|
||||
return coro
|
||||
|
||||
# listener registration
|
||||
|
||||
def add_listener(self, func, name=None):
|
||||
def add_listener(self, func: CoroFunc, name: str = MISSING) -> None:
|
||||
"""The non decorator alternative to :meth:`.listen`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func: :ref:`coroutine <coroutine>`
|
||||
The function to call.
|
||||
name: Optional[:class:`str`]
|
||||
name: :class:`str`
|
||||
The name of the event to listen for. Defaults to ``func.__name__``.
|
||||
|
||||
Example
|
||||
@@ -420,17 +631,17 @@ class BotBase(GroupMixin):
|
||||
bot.add_listener(my_message, 'on_message')
|
||||
|
||||
"""
|
||||
name = func.__name__ if name is None else name
|
||||
name = func.__name__ if name is MISSING else name
|
||||
|
||||
if not asyncio.iscoroutinefunction(func):
|
||||
raise TypeError('Listeners must be coroutines')
|
||||
raise TypeError("Listeners must be coroutines")
|
||||
|
||||
if name in self.extra_events:
|
||||
self.extra_events[name].append(func)
|
||||
else:
|
||||
self.extra_events[name] = [func]
|
||||
|
||||
def remove_listener(self, func, name=None):
|
||||
def remove_listener(self, func: CoroFunc, name: str = MISSING) -> None:
|
||||
"""Removes a listener from the pool of listeners.
|
||||
|
||||
Parameters
|
||||
@@ -442,7 +653,7 @@ class BotBase(GroupMixin):
|
||||
``func.__name__``.
|
||||
"""
|
||||
|
||||
name = func.__name__ if name is None else name
|
||||
name = func.__name__ if name is MISSING else name
|
||||
|
||||
if name in self.extra_events:
|
||||
try:
|
||||
@@ -450,7 +661,7 @@ class BotBase(GroupMixin):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def listen(self, name=None):
|
||||
def listen(self, name: str = MISSING) -> Callable[[CFT], CFT]:
|
||||
"""A decorator that registers another function as an external
|
||||
event listener. Basically this allows you to listen to multiple
|
||||
events from different places e.g. such as :func:`.on_ready`
|
||||
@@ -480,7 +691,7 @@ class BotBase(GroupMixin):
|
||||
The function being listened to is not a coroutine.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
def decorator(func: CFT) -> CFT:
|
||||
self.add_listener(func, name)
|
||||
return func
|
||||
|
||||
@@ -488,15 +699,25 @@ class BotBase(GroupMixin):
|
||||
|
||||
# cogs
|
||||
|
||||
def add_cog(self, cog):
|
||||
def add_cog(self, cog: Cog, *, override: bool = False) -> None:
|
||||
"""Adds a "cog" to the bot.
|
||||
|
||||
A cog is a class that has its own event listeners and commands.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
:exc:`.ClientException` is raised when a cog with the same name
|
||||
is already loaded.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
cog: :class:`.Cog`
|
||||
The cog to register to the bot.
|
||||
override: :class:`bool`
|
||||
If a previously loaded cog with the same name should be ejected
|
||||
instead of raising an error.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Raises
|
||||
-------
|
||||
@@ -504,15 +725,25 @@ class BotBase(GroupMixin):
|
||||
The cog does not inherit from :class:`.Cog`.
|
||||
CommandError
|
||||
An error happened during loading.
|
||||
.ClientException
|
||||
A cog with the same name is already loaded.
|
||||
"""
|
||||
|
||||
if not isinstance(cog, Cog):
|
||||
raise TypeError('cogs must derive from Cog')
|
||||
raise TypeError("cogs must derive from Cog")
|
||||
|
||||
cog_name = cog.__cog_name__
|
||||
existing = self.__cogs.get(cog_name)
|
||||
|
||||
if existing is not None:
|
||||
if not override:
|
||||
raise discord.ClientException(f"Cog named {cog_name!r} already loaded")
|
||||
self.remove_cog(cog_name)
|
||||
|
||||
cog = cog._inject(self)
|
||||
self.__cogs[cog.__cog_name__] = cog
|
||||
self.__cogs[cog_name] = cog
|
||||
|
||||
def get_cog(self, name):
|
||||
def get_cog(self, name: str) -> Optional[Cog]:
|
||||
"""Gets the cog instance requested.
|
||||
|
||||
If the cog is not found, ``None`` is returned instead.
|
||||
@@ -531,8 +762,8 @@ class BotBase(GroupMixin):
|
||||
"""
|
||||
return self.__cogs.get(name)
|
||||
|
||||
def remove_cog(self, name):
|
||||
"""Removes a cog from the bot.
|
||||
def remove_cog(self, name: str) -> Optional[Cog]:
|
||||
"""Removes a cog from the bot and returns it.
|
||||
|
||||
All registered commands and event listeners that the
|
||||
cog has registered will be removed as well.
|
||||
@@ -543,6 +774,11 @@ class BotBase(GroupMixin):
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the cog to remove.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`.Cog`]
|
||||
The cog that was removed. ``None`` if not found.
|
||||
"""
|
||||
|
||||
cog = self.__cogs.pop(name, None)
|
||||
@@ -554,14 +790,16 @@ class BotBase(GroupMixin):
|
||||
help_command.cog = None
|
||||
cog._eject(self)
|
||||
|
||||
return cog
|
||||
|
||||
@property
|
||||
def cogs(self):
|
||||
def cogs(self) -> Mapping[str, Cog]:
|
||||
"""Mapping[:class:`str`, :class:`Cog`]: A read-only mapping of cog name to cog."""
|
||||
return types.MappingProxyType(self.__cogs)
|
||||
|
||||
# extensions
|
||||
|
||||
def _remove_module_references(self, name):
|
||||
def _remove_module_references(self, name: str) -> None:
|
||||
# find all references to the module
|
||||
# remove the cogs registered from the module
|
||||
for cogname, cog in self.__cogs.copy().items():
|
||||
@@ -585,9 +823,9 @@ class BotBase(GroupMixin):
|
||||
for index in reversed(remove):
|
||||
del event_list[index]
|
||||
|
||||
def _call_module_finalizers(self, lib, key):
|
||||
def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None:
|
||||
try:
|
||||
func = getattr(lib, 'teardown')
|
||||
func = getattr(lib, "teardown")
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
@@ -603,18 +841,18 @@ class BotBase(GroupMixin):
|
||||
if _is_submodule(name, module):
|
||||
del sys.modules[module]
|
||||
|
||||
def _load_from_module_spec(self, spec, key):
|
||||
def _load_from_module_spec(self, spec: importlib.machinery.ModuleSpec, key: str) -> None:
|
||||
# precondition: key not in self.__extensions
|
||||
lib = importlib.util.module_from_spec(spec)
|
||||
sys.modules[key] = lib
|
||||
try:
|
||||
spec.loader.exec_module(lib)
|
||||
spec.loader.exec_module(lib) # type: ignore
|
||||
except Exception as e:
|
||||
del sys.modules[key]
|
||||
raise errors.ExtensionFailed(key, e) from e
|
||||
|
||||
try:
|
||||
setup = getattr(lib, 'setup')
|
||||
setup = getattr(lib, "setup")
|
||||
except AttributeError:
|
||||
del sys.modules[key]
|
||||
raise errors.NoEntryPointError(key)
|
||||
@@ -629,13 +867,13 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.__extensions[key] = lib
|
||||
|
||||
def _resolve_name(self, name, package):
|
||||
def _resolve_name(self, name: str, package: Optional[str]) -> str:
|
||||
try:
|
||||
return importlib.util.resolve_name(name, package)
|
||||
except ImportError:
|
||||
raise errors.ExtensionNotFound(name)
|
||||
|
||||
def load_extension(self, name, *, package=None):
|
||||
def load_extension(self, name: str, *, package: Optional[str] = None) -> None:
|
||||
"""Loads an extension.
|
||||
|
||||
An extension is a python module that contains commands, cogs, or
|
||||
@@ -682,7 +920,7 @@ class BotBase(GroupMixin):
|
||||
|
||||
self._load_from_module_spec(spec, name)
|
||||
|
||||
def unload_extension(self, name, *, package=None):
|
||||
def unload_extension(self, name: str, *, package: Optional[str] = None) -> None:
|
||||
"""Unloads an extension.
|
||||
|
||||
When the extension is unloaded, all commands, listeners, and cogs are
|
||||
@@ -723,7 +961,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
|
||||
def reload_extension(self, name, *, package=None):
|
||||
def reload_extension(self, name: str, *, package: Optional[str] = None) -> None:
|
||||
"""Atomically reloads an extension.
|
||||
|
||||
This replaces the extension with the same extension, only refreshed. This is
|
||||
@@ -764,11 +1002,7 @@ class BotBase(GroupMixin):
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
|
||||
# get the previous module states from sys modules
|
||||
modules = {
|
||||
name: module
|
||||
for name, module in sys.modules.items()
|
||||
if _is_submodule(lib.__name__, name)
|
||||
}
|
||||
modules = {name: module for name, module in sys.modules.items() if _is_submodule(lib.__name__, name)}
|
||||
|
||||
try:
|
||||
# Unload and then load the module...
|
||||
@@ -779,7 +1013,7 @@ class BotBase(GroupMixin):
|
||||
# if the load failed, the remnants should have been
|
||||
# cleaned from the load_extension function call
|
||||
# so let's load it from our old compiled library.
|
||||
lib.setup(self)
|
||||
lib.setup(self) # type: ignore
|
||||
self.__extensions[name] = lib
|
||||
|
||||
# revert sys.modules back to normal and raise back to caller
|
||||
@@ -787,21 +1021,21 @@ class BotBase(GroupMixin):
|
||||
raise
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
def extensions(self) -> Mapping[str, types.ModuleType]:
|
||||
"""Mapping[:class:`str`, :class:`py:types.ModuleType`]: A read-only mapping of extension name to extension."""
|
||||
return types.MappingProxyType(self.__extensions)
|
||||
|
||||
# help command stuff
|
||||
|
||||
@property
|
||||
def help_command(self):
|
||||
def help_command(self) -> Optional[HelpCommand]:
|
||||
return self._help_command
|
||||
|
||||
@help_command.setter
|
||||
def help_command(self, value):
|
||||
def help_command(self, value: Optional[HelpCommand]) -> None:
|
||||
if value is not None:
|
||||
if not isinstance(value, HelpCommand):
|
||||
raise TypeError('help_command must be a subclass of HelpCommand')
|
||||
raise TypeError("help_command must be a subclass of HelpCommand")
|
||||
if self._help_command is not None:
|
||||
self._help_command._remove_from_bot(self)
|
||||
self._help_command = value
|
||||
@@ -814,7 +1048,7 @@ class BotBase(GroupMixin):
|
||||
|
||||
# command processing
|
||||
|
||||
async def get_prefix(self, message):
|
||||
async def get_prefix(self, message: Message) -> Union[List[str], str]:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the prefix the bot is listening to
|
||||
@@ -831,6 +1065,9 @@ class BotBase(GroupMixin):
|
||||
A list of prefixes or a single prefix that the bot is
|
||||
listening for.
|
||||
"""
|
||||
if isinstance(message, _FakeSlashMessage):
|
||||
return "/"
|
||||
|
||||
prefix = ret = self.command_prefix
|
||||
if callable(prefix):
|
||||
ret = await discord.utils.maybe_coroutine(prefix, self, message)
|
||||
@@ -844,15 +1081,17 @@ class BotBase(GroupMixin):
|
||||
if isinstance(ret, collections.abc.Iterable):
|
||||
raise
|
||||
|
||||
raise TypeError("command_prefix must be plain string, iterable of strings, or callable "
|
||||
"returning either of these, not {}".format(ret.__class__.__name__))
|
||||
raise TypeError(
|
||||
"command_prefix must be plain string, iterable of strings, or callable "
|
||||
f"returning either of these, not {ret.__class__.__name__}"
|
||||
)
|
||||
|
||||
if not ret:
|
||||
raise ValueError("Iterable command_prefix must contain at least one prefix")
|
||||
|
||||
return ret
|
||||
|
||||
async def get_context(self, message, *, cls=Context):
|
||||
async def get_context(self, message: Message, *, cls: Type[CXT] = Context) -> CXT:
|
||||
r"""|coro|
|
||||
|
||||
Returns the invocation context from the message.
|
||||
@@ -885,7 +1124,7 @@ class BotBase(GroupMixin):
|
||||
view = StringView(message.content)
|
||||
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
||||
|
||||
if self._skip_check(message.author.id, self.user.id):
|
||||
if message.author.id == self.user.id: # type: ignore
|
||||
return ctx
|
||||
|
||||
prefix = await self.get_prefix(message)
|
||||
@@ -905,14 +1144,18 @@ class BotBase(GroupMixin):
|
||||
|
||||
except TypeError:
|
||||
if not isinstance(prefix, list):
|
||||
raise TypeError("get_prefix must return either a string or a list of string, "
|
||||
"not {}".format(prefix.__class__.__name__))
|
||||
raise TypeError(
|
||||
"get_prefix must return either a string or a list of string, "
|
||||
f"not {prefix.__class__.__name__}"
|
||||
)
|
||||
|
||||
# It's possible a bad command_prefix got us here.
|
||||
for value in prefix:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError("Iterable command_prefix or list returned from get_prefix must "
|
||||
"contain only strings, not {}".format(value.__class__.__name__))
|
||||
raise TypeError(
|
||||
"Iterable command_prefix or list returned from get_prefix must "
|
||||
f"contain only strings, not {value.__class__.__name__}"
|
||||
)
|
||||
|
||||
# Getting here shouldn't happen
|
||||
raise
|
||||
@@ -922,11 +1165,12 @@ class BotBase(GroupMixin):
|
||||
|
||||
invoker = view.get_word()
|
||||
ctx.invoked_with = invoker
|
||||
ctx.prefix = invoked_prefix
|
||||
# type-checker fails to narrow invoked_prefix type.
|
||||
ctx.prefix = invoked_prefix # type: ignore
|
||||
ctx.command = self.all_commands.get(invoker)
|
||||
return ctx
|
||||
|
||||
async def invoke(self, ctx):
|
||||
async def invoke(self, ctx: Context) -> None:
|
||||
"""|coro|
|
||||
|
||||
Invokes the command given under the invocation context and
|
||||
@@ -938,21 +1182,21 @@ class BotBase(GroupMixin):
|
||||
The invocation context to invoke.
|
||||
"""
|
||||
if ctx.command is not None:
|
||||
self.dispatch('command', ctx)
|
||||
self.dispatch("command", ctx)
|
||||
try:
|
||||
if await self.can_run(ctx, call_once=True):
|
||||
await ctx.command.invoke(ctx)
|
||||
else:
|
||||
raise errors.CheckFailure('The global check once functions failed.')
|
||||
raise errors.CheckFailure("The global check once functions failed.")
|
||||
except errors.CommandError as exc:
|
||||
await ctx.command.dispatch_error(ctx, exc)
|
||||
else:
|
||||
self.dispatch('command_completion', ctx)
|
||||
self.dispatch("command_completion", ctx)
|
||||
elif ctx.invoked_with:
|
||||
exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found')
|
||||
self.dispatch('command_error', ctx, exc)
|
||||
self.dispatch("command_error", ctx, exc)
|
||||
|
||||
async def process_commands(self, message):
|
||||
async def process_commands(self, message: Message) -> None:
|
||||
"""|coro|
|
||||
|
||||
This function processes the commands that have been registered
|
||||
@@ -980,9 +1224,95 @@ class BotBase(GroupMixin):
|
||||
ctx = await self.get_context(message)
|
||||
await self.invoke(ctx)
|
||||
|
||||
async def process_slash_commands(self, interaction: discord.Interaction):
|
||||
"""|coro|
|
||||
|
||||
This function processes a slash command interaction into a usable
|
||||
message and calls :meth:`.process_commands` based on it. Without this
|
||||
coroutine slash commands will not be triggered.
|
||||
|
||||
By default, this coroutine is called inside the :func:`.on_interaction`
|
||||
event. If you choose to override the :func:`.on_interaction` event,
|
||||
then you should invoke this coroutine as well.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`discord.Interaction`
|
||||
The interaction to process slash commands for.
|
||||
|
||||
"""
|
||||
if interaction.type != discord.InteractionType.application_command:
|
||||
return
|
||||
|
||||
interaction.data = cast(ApplicationCommandInteractionData, interaction.data)
|
||||
command_name, command_options = _unwrap_slash_groups(interaction.data)
|
||||
|
||||
command = self.get_command(command_name)
|
||||
if command is None:
|
||||
raise errors.CommandNotFound(f'Command "{command_name}" is not found')
|
||||
|
||||
# Ensure the interaction channel is usable
|
||||
channel = interaction.channel
|
||||
if channel is None or isinstance(channel, discord.PartialMessageable):
|
||||
if interaction.guild is None:
|
||||
assert interaction.user is not None
|
||||
channel = await interaction.user.create_dm()
|
||||
elif interaction.channel_id is not None:
|
||||
channel = await interaction.guild.fetch_channel(interaction.channel_id)
|
||||
else:
|
||||
return # cannot do anything without stable channel
|
||||
|
||||
# Make our fake message so we can pass it to ext.commands
|
||||
message: discord.Message = _FakeSlashMessage.from_interaction(interaction, channel) # type: ignore
|
||||
message.content = f"/{command_name} "
|
||||
|
||||
# Add arguments to fake message content, in the right order
|
||||
ignore_params: List[inspect.Parameter] = []
|
||||
for name, param in command.clean_params.items():
|
||||
if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter):
|
||||
for name, flag in param.annotation.get_flags().items():
|
||||
option = next((o for o in command_options if o["name"] == name), None)
|
||||
|
||||
if option is None:
|
||||
if flag.required:
|
||||
raise errors.MissingRequiredFlag(flag)
|
||||
else:
|
||||
prefix = param.annotation.__commands_flag_prefix__
|
||||
delimiter = param.annotation.__commands_flag_delimiter__
|
||||
message.content += f"{prefix}{name} {option['value']}{delimiter}" # type: ignore
|
||||
continue
|
||||
|
||||
option = next((o for o in command_options if o["name"] == name), None)
|
||||
if option is None:
|
||||
if param.default is param.empty and not command._is_typing_optional(param.annotation):
|
||||
raise errors.MissingRequiredArgument(param)
|
||||
else:
|
||||
ignore_params.append(param)
|
||||
elif (
|
||||
option["type"] == 3
|
||||
and not isinstance(param.annotation, Greedy)
|
||||
and param.kind in {param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY}
|
||||
):
|
||||
# String with space in without "consume rest"
|
||||
option = cast(_ApplicationCommandInteractionDataOptionString, option)
|
||||
message.content += f"{_quote_string_safe(option['value'])} "
|
||||
else:
|
||||
message.content += f'{option.get("value", "")} '
|
||||
|
||||
ctx = await self.get_context(message)
|
||||
ctx._ignored_params = ignore_params
|
||||
ctx.interaction = interaction
|
||||
await self.invoke(ctx)
|
||||
|
||||
async def on_message(self, message):
|
||||
await self.process_commands(message)
|
||||
|
||||
async def on_interaction(self, interaction: discord.Interaction):
|
||||
await self.process_slash_commands(interaction)
|
||||
|
||||
|
||||
class Bot(BotBase, discord.Client):
|
||||
"""Represents a discord bot.
|
||||
|
||||
@@ -1025,15 +1355,11 @@ class Bot(BotBase, discord.Client):
|
||||
when passing an empty string, it should always be last as no prefix
|
||||
after it will be matched.
|
||||
case_insensitive: :class:`bool`
|
||||
Whether the commands should be case insensitive. Defaults to ``False``. This
|
||||
Whether the commands should be case insensitive. Defaults to ``True``. This
|
||||
attribute does not carry over to groups. You must set it to every group if
|
||||
you require group commands to be case insensitive as well.
|
||||
description: :class:`str`
|
||||
The content prefixed into the default help message.
|
||||
self_bot: :class:`bool`
|
||||
If ``True``, the bot will only listen to commands invoked by itself rather
|
||||
than ignoring itself. If ``False`` (the default) then the bot will ignore
|
||||
itself. This cannot be changed once initialised.
|
||||
help_command: Optional[:class:`.HelpCommand`]
|
||||
The help command implementation to use. This can be dynamically
|
||||
set at runtime. To remove the help command pass ``None``. For more
|
||||
@@ -1056,11 +1382,36 @@ class Bot(BotBase, discord.Client):
|
||||
the ``command_prefix`` is set to ``!``. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
message_commands: Optional[:class:`bool`]
|
||||
Whether to process commands based on messages.
|
||||
|
||||
Can be overwritten per command in the command decorators or when making
|
||||
a :class:`Command` object via the ``message_command`` parameter
|
||||
|
||||
.. versionadded:: 2.0
|
||||
slash_commands: Optional[:class:`bool`]
|
||||
Whether to upload and process slash commands.
|
||||
|
||||
Can be overwritten per command in the command decorators or when making
|
||||
a :class:`Command` object via the ``slash_command`` parameter
|
||||
|
||||
.. versionadded:: 2.0
|
||||
slash_command_guilds: Optional[:class:`List[int]`]
|
||||
If this is set, only upload slash commands to these guild IDs.
|
||||
|
||||
Can be overwritten per command in the command decorators or when making
|
||||
a :class:`Command` object via the ``slash_command_guilds`` parameter
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AutoShardedBot(BotBase, discord.AutoShardedClient):
|
||||
"""This is similar to :class:`.Bot` except that it is inherited from
|
||||
:class:`discord.AutoShardedClient` instead.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -21,16 +21,31 @@ LIABILITY, WHETHER 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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import copy
|
||||
import discord.utils
|
||||
|
||||
from typing import Any, Callable, ClassVar, Dict, Generator, List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type
|
||||
|
||||
from ._types import _BaseCommand
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .bot import BotBase
|
||||
from .context import Context
|
||||
from .core import Command
|
||||
|
||||
__all__ = (
|
||||
'CogMeta',
|
||||
'Cog',
|
||||
"CogMeta",
|
||||
"Cog",
|
||||
)
|
||||
|
||||
CogT = TypeVar("CogT", bound="Cog")
|
||||
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
|
||||
|
||||
MISSING: Any = discord.utils.MISSING
|
||||
|
||||
|
||||
class CogMeta(type):
|
||||
"""A metaclass for defining a cog.
|
||||
|
||||
@@ -91,19 +106,24 @@ class CogMeta(type):
|
||||
pass # hidden -> False
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, bases, attrs = args
|
||||
attrs['__cog_name__'] = kwargs.pop('name', name)
|
||||
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
|
||||
__cog_name__: str
|
||||
__cog_settings__: Dict[str, Any]
|
||||
__cog_commands__: List[Command]
|
||||
__cog_listeners__: List[Tuple[str, str]]
|
||||
|
||||
description = kwargs.pop('description', None)
|
||||
def __new__(cls: Type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta:
|
||||
name, bases, attrs = args
|
||||
attrs["__cog_name__"] = kwargs.pop("name", name)
|
||||
attrs["__cog_settings__"] = kwargs.pop("command_attrs", {})
|
||||
|
||||
description = kwargs.pop("description", None)
|
||||
if description is None:
|
||||
description = inspect.cleandoc(attrs.get('__doc__', ''))
|
||||
attrs['__cog_description__'] = description
|
||||
description = inspect.cleandoc(attrs.get("__doc__", ""))
|
||||
attrs["__cog_description__"] = description
|
||||
|
||||
commands = {}
|
||||
listeners = {}
|
||||
no_bot_cog = 'Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})'
|
||||
no_bot_cog = "Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})"
|
||||
|
||||
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||
for base in reversed(new_cls.__mro__):
|
||||
@@ -118,21 +138,21 @@ class CogMeta(type):
|
||||
value = value.__func__
|
||||
if isinstance(value, _BaseCommand):
|
||||
if is_static_method:
|
||||
raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
raise TypeError(f"Command in method {base}.{elem!r} must not be staticmethod.")
|
||||
if elem.startswith(("cog_", "bot_")):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
commands[elem] = value
|
||||
elif inspect.iscoroutinefunction(value):
|
||||
try:
|
||||
getattr(value, '__cog_listener__')
|
||||
getattr(value, "__cog_listener__")
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
if elem.startswith(('cog_', 'bot_')):
|
||||
if elem.startswith(("cog_", "bot_")):
|
||||
raise TypeError(no_bot_cog.format(base, elem))
|
||||
listeners[elem] = value
|
||||
|
||||
new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
|
||||
new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
|
||||
|
||||
listeners_as_list = []
|
||||
for listener in listeners.values():
|
||||
@@ -144,17 +164,19 @@ class CogMeta(type):
|
||||
new_cls.__cog_listeners__ = listeners_as_list
|
||||
return new_cls
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args)
|
||||
|
||||
@classmethod
|
||||
def qualified_name(cls):
|
||||
def qualified_name(cls) -> str:
|
||||
return cls.__cog_name__
|
||||
|
||||
def _cog_special_method(func):
|
||||
|
||||
def _cog_special_method(func: FuncT) -> FuncT:
|
||||
func.__cog_special_method__ = None
|
||||
return func
|
||||
|
||||
|
||||
class Cog(metaclass=CogMeta):
|
||||
"""The base class that all cogs must inherit from.
|
||||
|
||||
@@ -166,7 +188,12 @@ class Cog(metaclass=CogMeta):
|
||||
are equally valid here.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
__cog_name__: ClassVar[str]
|
||||
__cog_settings__: ClassVar[Dict[str, Any]]
|
||||
__cog_commands__: ClassVar[List[Command]]
|
||||
__cog_listeners__: ClassVar[List[Tuple[str, str]]]
|
||||
|
||||
def __new__(cls: Type[CogT], *args: Any, **kwargs: Any) -> CogT:
|
||||
# For issue 426, we need to store a copy of the command objects
|
||||
# since we modify them to inject `self` to them.
|
||||
# To do this, we need to interfere with the Cog creation process.
|
||||
@@ -174,12 +201,10 @@ class Cog(metaclass=CogMeta):
|
||||
cmd_attrs = cls.__cog_settings__
|
||||
|
||||
# Either update the command with the cog provided defaults or copy it.
|
||||
self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__)
|
||||
# r.e type ignore, type-checker complains about overriding a ClassVar
|
||||
self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__) # type: ignore
|
||||
|
||||
lookup = {
|
||||
cmd.qualified_name: cmd
|
||||
for cmd in self.__cog_commands__
|
||||
}
|
||||
lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__}
|
||||
|
||||
# Update the Command instances dynamically as well
|
||||
for command in self.__cog_commands__:
|
||||
@@ -187,15 +212,15 @@ class Cog(metaclass=CogMeta):
|
||||
parent = command.parent
|
||||
if parent is not None:
|
||||
# Get the latest parent reference
|
||||
parent = lookup[parent.qualified_name]
|
||||
parent = lookup[parent.qualified_name] # type: ignore
|
||||
|
||||
# Update our parent's reference to our self
|
||||
parent.remove_command(command.name)
|
||||
parent.add_command(command)
|
||||
parent.remove_command(command.name) # type: ignore
|
||||
parent.add_command(command) # type: ignore
|
||||
|
||||
return self
|
||||
|
||||
def get_commands(self):
|
||||
def get_commands(self) -> List[Command]:
|
||||
r"""
|
||||
Returns
|
||||
--------
|
||||
@@ -210,20 +235,20 @@ class Cog(metaclass=CogMeta):
|
||||
return [c for c in self.__cog_commands__ if c.parent is None]
|
||||
|
||||
@property
|
||||
def qualified_name(self):
|
||||
def qualified_name(self) -> str:
|
||||
""":class:`str`: Returns the cog's specified name, not the class name."""
|
||||
return self.__cog_name__
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
def description(self) -> str:
|
||||
""":class:`str`: Returns the cog's description, typically the cleaned docstring."""
|
||||
return self.__cog_description__
|
||||
|
||||
@description.setter
|
||||
def description(self, description):
|
||||
def description(self, description: str) -> None:
|
||||
self.__cog_description__ = description
|
||||
|
||||
def walk_commands(self):
|
||||
def walk_commands(self) -> Generator[Command, None, None]:
|
||||
"""An iterator that recursively walks through this cog's commands and subcommands.
|
||||
|
||||
Yields
|
||||
@@ -232,13 +257,14 @@ class Cog(metaclass=CogMeta):
|
||||
A command or group from the cog.
|
||||
"""
|
||||
from .core import GroupMixin
|
||||
|
||||
for command in self.__cog_commands__:
|
||||
if command.parent is None:
|
||||
yield command
|
||||
if isinstance(command, GroupMixin):
|
||||
yield from command.walk_commands()
|
||||
|
||||
def get_listeners(self):
|
||||
def get_listeners(self) -> List[Tuple[str, Callable[..., Any]]]:
|
||||
"""Returns a :class:`list` of (name, function) listener pairs that are defined in this cog.
|
||||
|
||||
Returns
|
||||
@@ -249,12 +275,12 @@ class Cog(metaclass=CogMeta):
|
||||
return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__]
|
||||
|
||||
@classmethod
|
||||
def _get_overridden_method(cls, method):
|
||||
def _get_overridden_method(cls, method: FuncT) -> Optional[FuncT]:
|
||||
"""Return None if the method is not overridden. Otherwise returns the overridden method."""
|
||||
return getattr(method.__func__, '__cog_special_method__', method)
|
||||
return getattr(method.__func__, "__cog_special_method__", method)
|
||||
|
||||
@classmethod
|
||||
def listener(cls, name=None):
|
||||
def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]:
|
||||
"""A decorator that marks a function as a listener.
|
||||
|
||||
This is the cog equivalent of :meth:`.Bot.listen`.
|
||||
@@ -272,15 +298,15 @@ class Cog(metaclass=CogMeta):
|
||||
the name.
|
||||
"""
|
||||
|
||||
if name is not None and not isinstance(name, str):
|
||||
raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
|
||||
if name is not MISSING and not isinstance(name, str):
|
||||
raise TypeError(f"Cog.listener expected str but received {name.__class__.__name__!r} instead.")
|
||||
|
||||
def decorator(func):
|
||||
def decorator(func: FuncT) -> FuncT:
|
||||
actual = func
|
||||
if isinstance(actual, staticmethod):
|
||||
actual = actual.__func__
|
||||
if not inspect.iscoroutinefunction(actual):
|
||||
raise TypeError('Listener function must be a coroutine function.')
|
||||
raise TypeError("Listener function must be a coroutine function.")
|
||||
actual.__cog_listener__ = True
|
||||
to_assign = name or actual.__name__
|
||||
try:
|
||||
@@ -292,17 +318,18 @@ class Cog(metaclass=CogMeta):
|
||||
# to pick it up but the metaclass unfurls the function and
|
||||
# thus the assignments need to be on the actual function
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def has_error_handler(self):
|
||||
def has_error_handler(self) -> bool:
|
||||
""":class:`bool`: Checks whether the cog has an error handler.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
|
||||
return not hasattr(self.cog_command_error.__func__, "__cog_special_method__")
|
||||
|
||||
@_cog_special_method
|
||||
def cog_unload(self):
|
||||
def cog_unload(self) -> None:
|
||||
"""A special method that is called when the cog gets removed.
|
||||
|
||||
This function **cannot** be a coroutine. It must be a regular
|
||||
@@ -313,7 +340,7 @@ class Cog(metaclass=CogMeta):
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
def bot_check_once(self, ctx):
|
||||
def bot_check_once(self, ctx: Context) -> bool:
|
||||
"""A special method that registers as a :meth:`.Bot.check_once`
|
||||
check.
|
||||
|
||||
@@ -323,7 +350,7 @@ class Cog(metaclass=CogMeta):
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
def bot_check(self, ctx):
|
||||
def bot_check(self, ctx: Context) -> bool:
|
||||
"""A special method that registers as a :meth:`.Bot.check`
|
||||
check.
|
||||
|
||||
@@ -333,8 +360,8 @@ class Cog(metaclass=CogMeta):
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
def cog_check(self, ctx):
|
||||
"""A special method that registers as a :func:`commands.check`
|
||||
def cog_check(self, ctx: Context) -> bool:
|
||||
"""A special method that registers as a :func:`~discord.ext.commands.check`
|
||||
for every command and subcommand in this cog.
|
||||
|
||||
This function **can** be a coroutine and must take a sole parameter,
|
||||
@@ -343,7 +370,7 @@ class Cog(metaclass=CogMeta):
|
||||
return True
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_command_error(self, ctx, error):
|
||||
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
|
||||
"""A special method that is called whenever an error
|
||||
is dispatched inside this cog.
|
||||
|
||||
@@ -362,7 +389,7 @@ class Cog(metaclass=CogMeta):
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_before_invoke(self, ctx):
|
||||
async def cog_before_invoke(self, ctx: Context) -> None:
|
||||
"""A special method that acts as a cog local pre-invoke hook.
|
||||
|
||||
This is similar to :meth:`.Command.before_invoke`.
|
||||
@@ -377,7 +404,7 @@ class Cog(metaclass=CogMeta):
|
||||
pass
|
||||
|
||||
@_cog_special_method
|
||||
async def cog_after_invoke(self, ctx):
|
||||
async def cog_after_invoke(self, ctx: Context) -> None:
|
||||
"""A special method that acts as a cog local post-invoke hook.
|
||||
|
||||
This is similar to :meth:`.Command.after_invoke`.
|
||||
@@ -391,7 +418,7 @@ class Cog(metaclass=CogMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _inject(self, bot):
|
||||
def _inject(self: CogT, bot: BotBase) -> CogT:
|
||||
cls = self.__class__
|
||||
|
||||
# realistically, the only thing that can cause loading errors
|
||||
@@ -426,7 +453,7 @@ class Cog(metaclass=CogMeta):
|
||||
|
||||
return self
|
||||
|
||||
def _eject(self, bot):
|
||||
def _eject(self, bot: BotBase) -> None:
|
||||
cls = self.__class__
|
||||
|
||||
try:
|
||||
|
||||
@@ -21,15 +21,53 @@ LIABILITY, WHETHER 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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
|
||||
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
|
||||
__all__ = (
|
||||
'Context',
|
||||
)
|
||||
from discord.message import Message
|
||||
from discord import Permissions
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from discord.abc import MessageableChannel
|
||||
from discord.guild import Guild
|
||||
from discord.member import Member
|
||||
from discord.state import ConnectionState
|
||||
from discord.user import ClientUser, User
|
||||
from discord.webhook import WebhookMessage
|
||||
from discord.interactions import Interaction
|
||||
from discord.voice_client import VoiceProtocol
|
||||
|
||||
from .bot import Bot, AutoShardedBot
|
||||
from .cog import Cog
|
||||
from .core import Command
|
||||
from .help import HelpCommand
|
||||
from .view import StringView
|
||||
|
||||
__all__ = ("Context",)
|
||||
|
||||
MISSING: Any = discord.utils.MISSING
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
BotT = TypeVar("BotT", bound="Union[Bot, AutoShardedBot]")
|
||||
CogT = TypeVar("CogT", bound="Cog")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
P = ParamSpec("P")
|
||||
else:
|
||||
P = TypeVar("P")
|
||||
|
||||
|
||||
class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
r"""Represents the context in which a command is being invoked under.
|
||||
|
||||
This class contains a lot of meta data to help you understand more about
|
||||
@@ -46,17 +84,22 @@ class Context(discord.abc.Messageable):
|
||||
The bot that contains the command being executed.
|
||||
args: :class:`list`
|
||||
The list of transformed arguments that were passed into the command.
|
||||
If this is accessed during the :func:`on_command_error` event
|
||||
If this is accessed during the :func:`.on_command_error` event
|
||||
then this list could be incomplete.
|
||||
kwargs: :class:`dict`
|
||||
A dictionary of transformed arguments that were passed into the command.
|
||||
Similar to :attr:`args`\, if this is accessed in the
|
||||
:func:`on_command_error` event then this dict could be incomplete.
|
||||
prefix: :class:`str`
|
||||
:func:`.on_command_error` event then this dict could be incomplete.
|
||||
current_parameter: Optional[:class:`inspect.Parameter`]
|
||||
The parameter that is currently being inspected and converted.
|
||||
This is only of use for within converters.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
prefix: Optional[:class:`str`]
|
||||
The prefix that was used to invoke the command.
|
||||
command: :class:`Command`
|
||||
command: Optional[:class:`Command`]
|
||||
The command that is being invoked currently.
|
||||
invoked_with: :class:`str`
|
||||
invoked_with: Optional[:class:`str`]
|
||||
The command name that triggered this invocation. Useful for finding out
|
||||
which alias called the command.
|
||||
invoked_parents: List[:class:`str`]
|
||||
@@ -67,7 +110,7 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
invoked_subcommand: :class:`Command`
|
||||
invoked_subcommand: Optional[:class:`Command`]
|
||||
The subcommand that was invoked.
|
||||
If no valid subcommand was invoked then this is equal to ``None``.
|
||||
subcommand_passed: Optional[:class:`str`]
|
||||
@@ -79,23 +122,42 @@ class Context(discord.abc.Messageable):
|
||||
A boolean that indicates if the command failed to be parsed, checked,
|
||||
or invoked.
|
||||
"""
|
||||
interaction: Optional[Interaction] = None
|
||||
|
||||
def __init__(self, **attrs):
|
||||
self.message = attrs.pop('message', None)
|
||||
self.bot = attrs.pop('bot', None)
|
||||
self.args = attrs.pop('args', [])
|
||||
self.kwargs = attrs.pop('kwargs', {})
|
||||
self.prefix = attrs.pop('prefix')
|
||||
self.command = attrs.pop('command', None)
|
||||
self.view = attrs.pop('view', None)
|
||||
self.invoked_with = attrs.pop('invoked_with', None)
|
||||
self.invoked_parents = attrs.pop('invoked_parents', [])
|
||||
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
||||
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
||||
self.command_failed = attrs.pop('command_failed', False)
|
||||
self._state = self.message._state
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
message: Message,
|
||||
bot: BotT,
|
||||
view: StringView,
|
||||
args: List[Any] = MISSING,
|
||||
kwargs: Dict[str, Any] = MISSING,
|
||||
prefix: Optional[str] = None,
|
||||
command: Optional[Command] = None,
|
||||
invoked_with: Optional[str] = None,
|
||||
invoked_parents: List[str] = MISSING,
|
||||
invoked_subcommand: Optional[Command] = None,
|
||||
subcommand_passed: Optional[str] = None,
|
||||
command_failed: bool = False,
|
||||
current_parameter: Optional[inspect.Parameter] = None,
|
||||
):
|
||||
self.message: Message = message
|
||||
self.bot: BotT = bot
|
||||
self.args: List[Any] = args or []
|
||||
self.kwargs: Dict[str, Any] = kwargs or {}
|
||||
self.prefix: Optional[str] = prefix
|
||||
self.command: Optional[Command] = command
|
||||
self.view: StringView = view
|
||||
self.invoked_with: Optional[str] = invoked_with
|
||||
self.invoked_parents: List[str] = invoked_parents or []
|
||||
self.invoked_subcommand: Optional[Command] = invoked_subcommand
|
||||
self.subcommand_passed: Optional[str] = subcommand_passed
|
||||
self.command_failed: bool = command_failed
|
||||
self.current_parameter: Optional[inspect.Parameter] = current_parameter
|
||||
self._ignored_params: List[inspect.Parameter] = []
|
||||
self._state: ConnectionState = self.message._state
|
||||
|
||||
async def invoke(self, command, /, *args, **kwargs):
|
||||
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
@@ -117,7 +179,7 @@ class Context(discord.abc.Messageable):
|
||||
command: :class:`.Command`
|
||||
The command that is going to be called.
|
||||
\*args
|
||||
The arguments to to use.
|
||||
The arguments to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
|
||||
@@ -126,17 +188,9 @@ class Context(discord.abc.Messageable):
|
||||
TypeError
|
||||
The command argument to invoke is missing.
|
||||
"""
|
||||
arguments = []
|
||||
if command.cog is not None:
|
||||
arguments.append(command.cog)
|
||||
return await command(self, *args, **kwargs)
|
||||
|
||||
arguments.append(self)
|
||||
arguments.extend(args)
|
||||
|
||||
ret = await command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
|
||||
async def reinvoke(self, *, call_hooks=False, restart=True):
|
||||
async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None:
|
||||
"""|coro|
|
||||
|
||||
Calls the command again.
|
||||
@@ -169,7 +223,7 @@ class Context(discord.abc.Messageable):
|
||||
cmd = self.command
|
||||
view = self.view
|
||||
if cmd is None:
|
||||
raise ValueError('This context is not valid.')
|
||||
raise ValueError("This context is not valid.")
|
||||
|
||||
# some state to revert to when we're done
|
||||
index, previous = view.index, view.previous
|
||||
@@ -180,10 +234,10 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.index = len(self.prefix or "")
|
||||
view.previous = 0
|
||||
self.invoked_parents = []
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
@@ -199,15 +253,32 @@ class Context(discord.abc.Messageable):
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
def valid(self) -> bool:
|
||||
""":class:`bool`: Checks if the invocation context is valid to be invoked with."""
|
||||
return self.prefix is not None and self.command is not None
|
||||
|
||||
async def _get_channel(self):
|
||||
async def _get_channel(self) -> discord.abc.Messageable:
|
||||
return self.channel
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
def clean_prefix(self) -> str:
|
||||
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self.prefix is None:
|
||||
return ""
|
||||
|
||||
user = self.me
|
||||
# this breaks if the prefix mention is not the bot itself but I
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
# for this common use case rather than waste performance for the
|
||||
# odd one.
|
||||
pattern = re.compile(r"<@!?%s>" % user.id)
|
||||
return pattern.sub("@%s" % user.display_name.replace("\\", r"\\"), self.prefix)
|
||||
|
||||
@property
|
||||
def cog(self) -> Optional[Cog]:
|
||||
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
@@ -215,38 +286,46 @@ class Context(discord.abc.Messageable):
|
||||
return self.command.cog
|
||||
|
||||
@discord.utils.cached_property
|
||||
def guild(self):
|
||||
def guild(self) -> Optional[Guild]:
|
||||
"""Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available."""
|
||||
return self.message.guild
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
def channel(self) -> MessageableChannel:
|
||||
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Message.channel`.
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@discord.utils.cached_property
|
||||
def author(self):
|
||||
def author(self) -> Union[User, Member]:
|
||||
"""Union[:class:`~discord.User`, :class:`.Member`]:
|
||||
Returns the author associated with this context's command. Shorthand for :attr:`.Message.author`
|
||||
"""
|
||||
return self.message.author
|
||||
|
||||
@discord.utils.cached_property
|
||||
def me(self):
|
||||
def me(self) -> Union[Member, ClientUser]:
|
||||
"""Union[:class:`.Member`, :class:`.ClientUser`]:
|
||||
Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message contexts.
|
||||
"""
|
||||
return self.guild.me if self.guild is not None else self.bot.user
|
||||
# bot.user will never be None at this point.
|
||||
return self.guild.me if self.guild is not None else self.bot.user # type: ignore
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
def voice_client(self) -> Optional[VoiceProtocol]:
|
||||
r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
|
||||
g = self.guild
|
||||
return g.voice_client if g else None
|
||||
|
||||
async def send_help(self, *args):
|
||||
def author_permissions(self) -> Permissions:
|
||||
"""Returns the author permissions in the given channel.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return self.channel.permissions_for(self.author)
|
||||
|
||||
async def send_help(self, *args: Any) -> Any:
|
||||
"""send_help(entity=<bot>)
|
||||
|
||||
|coro|
|
||||
@@ -298,12 +377,12 @@ class Context(discord.abc.Messageable):
|
||||
return None
|
||||
|
||||
entity = args[0]
|
||||
if entity is None:
|
||||
return None
|
||||
|
||||
if isinstance(entity, str):
|
||||
entity = bot.get_cog(entity) or bot.get_command(entity)
|
||||
|
||||
if entity is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
entity.qualified_name
|
||||
except AttributeError:
|
||||
@@ -313,7 +392,7 @@ class Context(discord.abc.Messageable):
|
||||
await cmd.prepare_help_command(self, entity.qualified_name)
|
||||
|
||||
try:
|
||||
if hasattr(entity, '__cog_commands__'):
|
||||
if hasattr(entity, "__cog_commands__"):
|
||||
injected = wrap_callback(cmd.send_cog_help)
|
||||
return await injected(entity)
|
||||
elif isinstance(entity, Group):
|
||||
@@ -327,6 +406,97 @@ class Context(discord.abc.Messageable):
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
|
||||
@discord.utils.copy_doc(discord.Message.reply)
|
||||
async def reply(self, content=None, **kwargs):
|
||||
return await self.message.reply(content, **kwargs)
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
content: Optional[str] = None,
|
||||
return_message: Literal[False] = False,
|
||||
ephemeral: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Optional[Union[Message, WebhookMessage]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
content: Optional[str] = None,
|
||||
return_message: Literal[True] = True,
|
||||
ephemeral: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Union[Message, WebhookMessage]:
|
||||
...
|
||||
|
||||
async def send(
|
||||
self, content: Optional[str] = None, return_message: bool = True, ephemeral: bool = False, **kwargs: Any
|
||||
) -> Optional[Union[Message, WebhookMessage]]:
|
||||
"""
|
||||
|coro|
|
||||
|
||||
A shortcut method to :meth:`.abc.Messageable.send` with interaction helpers.
|
||||
|
||||
This function takes all the parameters of :meth:`.abc.Messageable.send` plus the following:
|
||||
|
||||
Parameters
|
||||
------------
|
||||
return_message: :class:`bool`
|
||||
Ignored if not in a slash command context.
|
||||
If this is set to False more native interaction methods will be used.
|
||||
ephemeral: :class:`bool`
|
||||
Ignored if not in a slash command context.
|
||||
Indicates if the message should only be visible to the user who started the interaction.
|
||||
If a view is sent with an ephemeral message and it has no timeout set then the timeout
|
||||
is set to 15 minutes.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[Union[:class:`.Message`, :class:`.WebhookMessage`]]
|
||||
In a slash command context, the message that was sent if return_message is True.
|
||||
|
||||
In a normal context, it always returns a :class:`.Message`
|
||||
"""
|
||||
|
||||
if self.interaction is None or (
|
||||
self.interaction.response.responded_at is not None
|
||||
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
|
||||
):
|
||||
return await super().send(content, **kwargs)
|
||||
|
||||
# Remove unsupported arguments from kwargs
|
||||
kwargs.pop("nonce", None)
|
||||
kwargs.pop("stickers", None)
|
||||
kwargs.pop("reference", None)
|
||||
kwargs.pop("delete_after", None)
|
||||
kwargs.pop("mention_author", None)
|
||||
|
||||
if not (
|
||||
return_message
|
||||
or self.interaction.response.is_done()
|
||||
or any(arg in kwargs for arg in ("file", "files", "allowed_mentions"))
|
||||
):
|
||||
send = self.interaction.response.send_message
|
||||
else:
|
||||
# We have to defer in order to use the followup webhook
|
||||
if not self.interaction.response.is_done():
|
||||
await self.interaction.response.defer(ephemeral=ephemeral)
|
||||
|
||||
send = self.interaction.followup.send
|
||||
|
||||
return await send(content, ephemeral=ephemeral, **kwargs) # type: ignore
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self, content: Optional[str] = None, return_message: Literal[False] = False, **kwargs: Any
|
||||
) -> Optional[Union[Message, WebhookMessage]]:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self, content: Optional[str] = None, return_message: Literal[True] = True, **kwargs: Any
|
||||
) -> Union[Message, WebhookMessage]:
|
||||
...
|
||||
|
||||
@discord.utils.copy_doc(Message.reply)
|
||||
async def reply(
|
||||
self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any
|
||||
) -> Optional[Union[Message, WebhookMessage]]:
|
||||
return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import Any, Callable, Deque, Dict, Optional, Type, TypeVar, TYPE_CHECKING
|
||||
from discord.enums import Enum
|
||||
import time
|
||||
import asyncio
|
||||
@@ -30,23 +34,31 @@ from collections import deque
|
||||
from ...abc import PrivateChannel
|
||||
from .errors import MaxConcurrencyReached
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...message import Message
|
||||
|
||||
__all__ = (
|
||||
'BucketType',
|
||||
'Cooldown',
|
||||
'CooldownMapping',
|
||||
'MaxConcurrency',
|
||||
"BucketType",
|
||||
"Cooldown",
|
||||
"CooldownMapping",
|
||||
"DynamicCooldownMapping",
|
||||
"MaxConcurrency",
|
||||
)
|
||||
|
||||
class BucketType(Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
guild = 2
|
||||
channel = 3
|
||||
member = 4
|
||||
category = 5
|
||||
role = 6
|
||||
C = TypeVar("C", bound="CooldownMapping")
|
||||
MC = TypeVar("MC", bound="MaxConcurrency")
|
||||
|
||||
def get_key(self, msg):
|
||||
|
||||
class BucketType(Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
guild = 2
|
||||
channel = 3
|
||||
member = 4
|
||||
category = 5
|
||||
role = 6
|
||||
|
||||
def get_key(self, msg: Message) -> Any:
|
||||
if self is BucketType.user:
|
||||
return msg.author.id
|
||||
elif self is BucketType.guild:
|
||||
@@ -56,33 +68,52 @@ class BucketType(Enum):
|
||||
elif self is BucketType.member:
|
||||
return ((msg.guild and msg.guild.id), msg.author.id)
|
||||
elif self is BucketType.category:
|
||||
return (msg.channel.category or msg.channel).id
|
||||
return (msg.channel.category or msg.channel).id # type: ignore
|
||||
elif self is BucketType.role:
|
||||
# we return the channel id of a private-channel as there are only roles in guilds
|
||||
# and that yields the same result as for a guild with only the @everyone role
|
||||
# NOTE: PrivateChannel doesn't actually have an id attribute but we assume we are
|
||||
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
|
||||
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
|
||||
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id # type: ignore
|
||||
|
||||
def __call__(self, msg):
|
||||
def __call__(self, msg: Message) -> Any:
|
||||
return self.get_key(msg)
|
||||
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
||||
"""Represents a cooldown for a command.
|
||||
|
||||
def __init__(self, rate, per, type):
|
||||
self.rate = int(rate)
|
||||
self.per = float(per)
|
||||
self.type = type
|
||||
self._window = 0.0
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
Attributes
|
||||
-----------
|
||||
rate: :class:`int`
|
||||
The total number of tokens available per :attr:`per` seconds.
|
||||
per: :class:`float`
|
||||
The length of the cooldown period in seconds.
|
||||
"""
|
||||
|
||||
if not callable(self.type):
|
||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||
__slots__ = ("rate", "per", "_window", "_tokens", "_last")
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
def __init__(self, rate: float, per: float) -> None:
|
||||
self.rate: int = int(rate)
|
||||
self.per: float = float(per)
|
||||
self._window: float = 0.0
|
||||
self._tokens: int = self.rate
|
||||
self._last: float = 0.0
|
||||
|
||||
def get_tokens(self, current: Optional[float] = None) -> int:
|
||||
"""Returns the number of available tokens before rate limiting is applied.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
current: Optional[:class:`float`]
|
||||
The time in seconds since Unix epoch to calculate tokens at.
|
||||
If not supplied then :func:`time.time()` is used.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of tokens available before the cooldown is to be applied.
|
||||
"""
|
||||
if not current:
|
||||
current = time.time()
|
||||
|
||||
@@ -92,7 +123,20 @@ class Cooldown:
|
||||
tokens = self.rate
|
||||
return tokens
|
||||
|
||||
def get_retry_after(self, current=None):
|
||||
def get_retry_after(self, current: Optional[float] = None) -> float:
|
||||
"""Returns the time in seconds until the cooldown will be reset.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
current: Optional[:class:`float`]
|
||||
The current time in seconds since Unix epoch.
|
||||
If not supplied, then :func:`time.time()` is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`float`
|
||||
The number of seconds to wait before this cooldown will be reset.
|
||||
"""
|
||||
current = current or time.time()
|
||||
tokens = self.get_tokens(current)
|
||||
|
||||
@@ -101,7 +145,20 @@ class Cooldown:
|
||||
|
||||
return 0.0
|
||||
|
||||
def update_rate_limit(self, current=None):
|
||||
def update_rate_limit(self, current: Optional[float] = None) -> Optional[float]:
|
||||
"""Updates the cooldown rate limit.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
current: Optional[:class:`float`]
|
||||
The time in seconds since Unix epoch to update the rate limit at.
|
||||
If not supplied, then :func:`time.time()` is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`float`]
|
||||
The retry-after time in seconds if rate limited.
|
||||
"""
|
||||
current = current or time.time()
|
||||
self._last = current
|
||||
|
||||
@@ -118,43 +175,59 @@ class Cooldown:
|
||||
# we're not so decrement our tokens
|
||||
self._tokens -= 1
|
||||
|
||||
# see if we got rate limited due to this token change, and if
|
||||
# so update the window to point to our current time frame
|
||||
if self._tokens == 0:
|
||||
self._window = current
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
"""Reset the cooldown to its initial state."""
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Cooldown(self.rate, self.per, self.type)
|
||||
def copy(self) -> Cooldown:
|
||||
"""Creates a copy of this cooldown.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Cooldown`
|
||||
A new instance of this cooldown.
|
||||
"""
|
||||
return Cooldown(self.rate, self.per)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Cooldown rate: {self.rate} per: {self.per} window: {self._window} tokens: {self._tokens}>"
|
||||
|
||||
def __repr__(self):
|
||||
return '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.format(self)
|
||||
|
||||
class CooldownMapping:
|
||||
def __init__(self, original):
|
||||
self._cache = {}
|
||||
self._cooldown = original
|
||||
def __init__(
|
||||
self,
|
||||
original: Optional[Cooldown],
|
||||
type: Callable[[Message], Any],
|
||||
) -> None:
|
||||
if not callable(type):
|
||||
raise TypeError("Cooldown type must be a BucketType or callable")
|
||||
|
||||
def copy(self):
|
||||
ret = CooldownMapping(self._cooldown)
|
||||
self._cache: Dict[Any, Cooldown] = {}
|
||||
self._cooldown: Optional[Cooldown] = original
|
||||
self._type: Callable[[Message], Any] = type
|
||||
|
||||
def copy(self) -> CooldownMapping:
|
||||
ret = CooldownMapping(self._cooldown, self._type)
|
||||
ret._cache = self._cache.copy()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
def valid(self) -> bool:
|
||||
return self._cooldown is not None
|
||||
|
||||
@property
|
||||
def type(self) -> Callable[[Message], Any]:
|
||||
return self._type
|
||||
|
||||
@classmethod
|
||||
def from_cooldown(cls, rate, per, type):
|
||||
return cls(Cooldown(rate, per, type))
|
||||
def from_cooldown(cls: Type[C], rate, per, type) -> C:
|
||||
return cls(Cooldown(rate, per), type)
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
return self._cooldown.type(msg)
|
||||
def _bucket_key(self, msg: Message) -> Any:
|
||||
return self._type(msg)
|
||||
|
||||
def _verify_cache_integrity(self, current=None):
|
||||
def _verify_cache_integrity(self, current: Optional[float] = None) -> None:
|
||||
# we want to delete all cache objects that haven't been used
|
||||
# in a cooldown window. e.g. if we have a command that has a
|
||||
# cooldown of 60s and it has not been used in 60s then that key should be deleted
|
||||
@@ -163,24 +236,47 @@ class CooldownMapping:
|
||||
for k in dead_keys:
|
||||
del self._cache[k]
|
||||
|
||||
def get_bucket(self, message, current=None):
|
||||
if self._cooldown.type is BucketType.default:
|
||||
return self._cooldown
|
||||
def create_bucket(self, message: Message) -> Cooldown:
|
||||
return self._cooldown.copy() # type: ignore
|
||||
|
||||
def get_bucket(self, message: Message, current: Optional[float] = None) -> Cooldown:
|
||||
if self._type is BucketType.default:
|
||||
return self._cooldown # type: ignore
|
||||
|
||||
self._verify_cache_integrity(current)
|
||||
key = self._bucket_key(message)
|
||||
if key not in self._cache:
|
||||
bucket = self._cooldown.copy()
|
||||
self._cache[key] = bucket
|
||||
bucket = self.create_bucket(message)
|
||||
if bucket is not None:
|
||||
self._cache[key] = bucket
|
||||
else:
|
||||
bucket = self._cache[key]
|
||||
|
||||
return bucket
|
||||
|
||||
def update_rate_limit(self, message, current=None):
|
||||
def update_rate_limit(self, message: Message, current: Optional[float] = None) -> Optional[float]:
|
||||
bucket = self.get_bucket(message, current)
|
||||
return bucket.update_rate_limit(current)
|
||||
|
||||
|
||||
class DynamicCooldownMapping(CooldownMapping):
|
||||
def __init__(self, factory: Callable[[Message], Cooldown], type: Callable[[Message], Any]) -> None:
|
||||
super().__init__(None, type)
|
||||
self._factory: Callable[[Message], Cooldown] = factory
|
||||
|
||||
def copy(self) -> DynamicCooldownMapping:
|
||||
ret = DynamicCooldownMapping(self._factory, self._type)
|
||||
ret._cache = self._cache.copy()
|
||||
return ret
|
||||
|
||||
@property
|
||||
def valid(self) -> bool:
|
||||
return True
|
||||
|
||||
def create_bucket(self, message: Message) -> Cooldown:
|
||||
return self._factory(message)
|
||||
|
||||
|
||||
class _Semaphore:
|
||||
"""This class is a version of a semaphore.
|
||||
|
||||
@@ -194,30 +290,30 @@ class _Semaphore:
|
||||
overkill for what is basically a counter.
|
||||
"""
|
||||
|
||||
__slots__ = ('value', 'loop', '_waiters')
|
||||
__slots__ = ("value", "loop", "_waiters")
|
||||
|
||||
def __init__(self, number):
|
||||
self.value = number
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self._waiters = deque()
|
||||
def __init__(self, number: int) -> None:
|
||||
self.value: int = number
|
||||
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
self._waiters: Deque[asyncio.Future] = deque()
|
||||
|
||||
def __repr__(self):
|
||||
return '<_Semaphore value={0.value} waiters={1}>'.format(self, len(self._waiters))
|
||||
def __repr__(self) -> str:
|
||||
return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>"
|
||||
|
||||
def locked(self):
|
||||
def locked(self) -> bool:
|
||||
return self.value == 0
|
||||
|
||||
def is_active(self):
|
||||
def is_active(self) -> bool:
|
||||
return len(self._waiters) > 0
|
||||
|
||||
def wake_up(self):
|
||||
def wake_up(self) -> None:
|
||||
while self._waiters:
|
||||
future = self._waiters.popleft()
|
||||
if not future.done():
|
||||
future.set_result(None)
|
||||
return
|
||||
|
||||
async def acquire(self, *, wait=False):
|
||||
async def acquire(self, *, wait: bool = False) -> bool:
|
||||
if not wait and self.value <= 0:
|
||||
# signal that we're not acquiring
|
||||
return False
|
||||
@@ -236,35 +332,36 @@ class _Semaphore:
|
||||
self.value -= 1
|
||||
return True
|
||||
|
||||
def release(self):
|
||||
def release(self) -> None:
|
||||
self.value += 1
|
||||
self.wake_up()
|
||||
|
||||
class MaxConcurrency:
|
||||
__slots__ = ('number', 'per', 'wait', '_mapping')
|
||||
|
||||
def __init__(self, number, *, per, wait):
|
||||
self._mapping = {}
|
||||
self.per = per
|
||||
self.number = number
|
||||
self.wait = wait
|
||||
class MaxConcurrency:
|
||||
__slots__ = ("number", "per", "wait", "_mapping")
|
||||
|
||||
def __init__(self, number: int, *, per: BucketType, wait: bool) -> None:
|
||||
self._mapping: Dict[Any, _Semaphore] = {}
|
||||
self.per: BucketType = per
|
||||
self.number: int = number
|
||||
self.wait: bool = wait
|
||||
|
||||
if number <= 0:
|
||||
raise ValueError('max_concurrency \'number\' cannot be less than 1')
|
||||
raise ValueError("max_concurrency 'number' cannot be less than 1")
|
||||
|
||||
if not isinstance(per, BucketType):
|
||||
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
|
||||
raise TypeError(f"max_concurrency 'per' must be of type BucketType not {type(per)!r}")
|
||||
|
||||
def copy(self):
|
||||
def copy(self: MC) -> MC:
|
||||
return self.__class__(self.number, per=self.per, wait=self.wait)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MaxConcurrency per={0.per!r} number={0.number} wait={0.wait}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<MaxConcurrency per={self.per!r} number={self.number} wait={self.wait}>"
|
||||
|
||||
def get_key(self, message):
|
||||
def get_key(self, message: Message) -> Any:
|
||||
return self.per.get_key(message)
|
||||
|
||||
async def acquire(self, message):
|
||||
async def acquire(self, message: Message) -> None:
|
||||
key = self.get_key(message)
|
||||
|
||||
try:
|
||||
@@ -276,7 +373,7 @@ class MaxConcurrency:
|
||||
if not acquired:
|
||||
raise MaxConcurrencyReached(self.number, self.per)
|
||||
|
||||
async def release(self, message):
|
||||
async def release(self, message: Message) -> None:
|
||||
# Technically there's no reason for this function to be async
|
||||
# But it might be more useful in the future
|
||||
key = self.get_key(message)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
622
discord/ext/commands/flags.py
Normal file
622
discord/ext/commands/flags.py
Normal file
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import (
|
||||
BadFlagArgument,
|
||||
CommandError,
|
||||
MissingFlagArgument,
|
||||
TooManyFlags,
|
||||
MissingRequiredFlag,
|
||||
)
|
||||
|
||||
from discord.utils import resolve_annotation
|
||||
from .view import StringView
|
||||
from .converter import run_converters
|
||||
|
||||
from discord.utils import maybe_coroutine, MISSING
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
Literal,
|
||||
Optional,
|
||||
Pattern,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
List,
|
||||
Any,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
import re
|
||||
|
||||
__all__ = (
|
||||
"Flag",
|
||||
"flag",
|
||||
"FlagConverter",
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
|
||||
@dataclass
|
||||
class Flag:
|
||||
"""Represents a flag parameter for :class:`FlagConverter`.
|
||||
|
||||
The :func:`~discord.ext.commands.flag` function helps
|
||||
create these flag objects, but it is not necessary to
|
||||
do so. These cannot be constructed manually.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the flag.
|
||||
aliases: List[:class:`str`]
|
||||
The aliases of the flag name.
|
||||
attribute: :class:`str`
|
||||
The attribute in the class that corresponds to this flag.
|
||||
default: Any
|
||||
The default value of the flag, if available.
|
||||
annotation: Any
|
||||
The underlying evaluated annotation of the flag.
|
||||
max_args: :class:`int`
|
||||
The maximum number of arguments the flag can accept.
|
||||
A negative value indicates an unlimited amount of arguments.
|
||||
override: :class:`bool`
|
||||
Whether multiple given values overrides the previous value.
|
||||
"""
|
||||
|
||||
name: str = MISSING
|
||||
aliases: List[str] = field(default_factory=list)
|
||||
attribute: str = MISSING
|
||||
annotation: Any = MISSING
|
||||
default: Any = MISSING
|
||||
max_args: int = MISSING
|
||||
override: bool = MISSING
|
||||
cast_to_dict: bool = False
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
""":class:`bool`: Whether the flag is required.
|
||||
|
||||
A required flag has no default value.
|
||||
"""
|
||||
return self.default is MISSING
|
||||
|
||||
|
||||
def flag(
|
||||
*,
|
||||
name: str = MISSING,
|
||||
aliases: List[str] = MISSING,
|
||||
default: Any = MISSING,
|
||||
max_args: int = MISSING,
|
||||
override: bool = MISSING,
|
||||
) -> Any:
|
||||
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
|
||||
class attributes.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
The flag name. If not given, defaults to the attribute name.
|
||||
aliases: List[:class:`str`]
|
||||
Aliases to the flag name. If not given no aliases are set.
|
||||
default: Any
|
||||
The default parameter. This could be either a value or a callable that takes
|
||||
:class:`Context` as its sole parameter. If not given then it defaults to
|
||||
the default value given to the attribute.
|
||||
max_args: :class:`int`
|
||||
The maximum number of arguments the flag can accept.
|
||||
A negative value indicates an unlimited amount of arguments.
|
||||
The default value depends on the annotation given.
|
||||
override: :class:`bool`
|
||||
Whether multiple given values overrides the previous value. The default
|
||||
value depends on the annotation given.
|
||||
"""
|
||||
return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override)
|
||||
|
||||
|
||||
def validate_flag_name(name: str, forbidden: Set[str]):
|
||||
if not name:
|
||||
raise ValueError("flag names should not be empty")
|
||||
|
||||
for ch in name:
|
||||
if ch.isspace():
|
||||
raise ValueError(f"flag name {name!r} cannot have spaces")
|
||||
if ch == "\\":
|
||||
raise ValueError(f"flag name {name!r} cannot have backslashes")
|
||||
if ch in forbidden:
|
||||
raise ValueError(f"flag name {name!r} cannot have any of {forbidden!r} within them")
|
||||
|
||||
|
||||
def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[str, Any]) -> Dict[str, Flag]:
|
||||
annotations = namespace.get("__annotations__", {})
|
||||
case_insensitive = namespace["__commands_flag_case_insensitive__"]
|
||||
flags: Dict[str, Flag] = {}
|
||||
cache: Dict[str, Any] = {}
|
||||
names: Set[str] = set()
|
||||
for name, annotation in annotations.items():
|
||||
flag = namespace.pop(name, MISSING)
|
||||
if isinstance(flag, Flag):
|
||||
flag.annotation = annotation
|
||||
else:
|
||||
flag = Flag(name=name, annotation=annotation, default=flag)
|
||||
|
||||
flag.attribute = name
|
||||
if flag.name is MISSING:
|
||||
flag.name = name
|
||||
|
||||
annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache)
|
||||
|
||||
if (
|
||||
flag.default is MISSING
|
||||
and hasattr(annotation, "__commands_is_flag__")
|
||||
and annotation._can_be_constructible()
|
||||
):
|
||||
flag.default = annotation._construct_default
|
||||
|
||||
if flag.aliases is MISSING:
|
||||
flag.aliases = []
|
||||
|
||||
# Add sensible defaults based off of the type annotation
|
||||
# <type> -> (max_args=1)
|
||||
# List[str] -> (max_args=-1)
|
||||
# Tuple[int, ...] -> (max_args=1)
|
||||
# Dict[K, V] -> (max_args=-1, override=True)
|
||||
# Union[str, int] -> (max_args=1)
|
||||
# Optional[str] -> (default=None, max_args=1)
|
||||
|
||||
try:
|
||||
origin = annotation.__origin__
|
||||
except AttributeError:
|
||||
# A regular type hint
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = 1
|
||||
else:
|
||||
if origin is Union:
|
||||
# typing.Union
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = 1
|
||||
if annotation.__args__[-1] is type(None) and flag.default is MISSING:
|
||||
# typing.Optional
|
||||
flag.default = None
|
||||
elif origin is tuple:
|
||||
# typing.Tuple
|
||||
# tuple parsing is e.g. `flag: peter 20`
|
||||
# for Tuple[str, int] would give you flag: ('peter', 20)
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = 1
|
||||
elif origin is list:
|
||||
# typing.List
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = -1
|
||||
elif origin is dict:
|
||||
# typing.Dict[K, V]
|
||||
# Equivalent to:
|
||||
# typing.List[typing.Tuple[K, V]]
|
||||
flag.cast_to_dict = True
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = -1
|
||||
if flag.override is MISSING:
|
||||
flag.override = True
|
||||
elif origin is Literal:
|
||||
if flag.max_args is MISSING:
|
||||
flag.max_args = 1
|
||||
else:
|
||||
raise TypeError(f"Unsupported typing annotation {annotation!r} for {flag.name!r} flag")
|
||||
|
||||
if flag.override is MISSING:
|
||||
flag.override = False
|
||||
|
||||
# Validate flag names are unique
|
||||
name = flag.name.casefold() if case_insensitive else flag.name
|
||||
if name in names:
|
||||
raise TypeError(f"{flag.name!r} flag conflicts with previous flag or alias.")
|
||||
else:
|
||||
names.add(name)
|
||||
|
||||
for alias in flag.aliases:
|
||||
# Validate alias is unique
|
||||
alias = alias.casefold() if case_insensitive else alias
|
||||
if alias in names:
|
||||
raise TypeError(f"{flag.name!r} flag alias {alias!r} conflicts with previous flag or alias.")
|
||||
else:
|
||||
names.add(alias)
|
||||
|
||||
flags[flag.name] = flag
|
||||
|
||||
return flags
|
||||
|
||||
|
||||
class FlagsMeta(type):
|
||||
if TYPE_CHECKING:
|
||||
__commands_is_flag__: bool
|
||||
__commands_flags__: Dict[str, Flag]
|
||||
__commands_flag_aliases__: Dict[str, str]
|
||||
__commands_flag_regex__: Pattern[str]
|
||||
__commands_flag_case_insensitive__: bool
|
||||
__commands_flag_delimiter__: str
|
||||
__commands_flag_prefix__: str
|
||||
|
||||
def __new__(
|
||||
cls: Type[type],
|
||||
name: str,
|
||||
bases: Tuple[type, ...],
|
||||
attrs: Dict[str, Any],
|
||||
*,
|
||||
case_insensitive: bool = MISSING,
|
||||
delimiter: str = MISSING,
|
||||
prefix: str = MISSING,
|
||||
):
|
||||
attrs["__commands_is_flag__"] = True
|
||||
|
||||
try:
|
||||
global_ns = sys.modules[attrs["__module__"]].__dict__
|
||||
except KeyError:
|
||||
global_ns = {}
|
||||
|
||||
frame = inspect.currentframe()
|
||||
try:
|
||||
if frame is None:
|
||||
local_ns = {}
|
||||
else:
|
||||
if frame.f_back is None:
|
||||
local_ns = frame.f_locals
|
||||
else:
|
||||
local_ns = frame.f_back.f_locals
|
||||
finally:
|
||||
del frame
|
||||
|
||||
flags: Dict[str, Flag] = {}
|
||||
aliases: Dict[str, str] = {}
|
||||
for base in reversed(bases):
|
||||
if base.__dict__.get("__commands_is_flag__", False):
|
||||
flags.update(base.__dict__["__commands_flags__"])
|
||||
aliases.update(base.__dict__["__commands_flag_aliases__"])
|
||||
if case_insensitive is MISSING:
|
||||
attrs["__commands_flag_case_insensitive__"] = base.__dict__["__commands_flag_case_insensitive__"]
|
||||
if delimiter is MISSING:
|
||||
attrs["__commands_flag_delimiter__"] = base.__dict__["__commands_flag_delimiter__"]
|
||||
if prefix is MISSING:
|
||||
attrs["__commands_flag_prefix__"] = base.__dict__["__commands_flag_prefix__"]
|
||||
|
||||
if case_insensitive is not MISSING:
|
||||
attrs["__commands_flag_case_insensitive__"] = case_insensitive
|
||||
if delimiter is not MISSING:
|
||||
attrs["__commands_flag_delimiter__"] = delimiter
|
||||
if prefix is not MISSING:
|
||||
attrs["__commands_flag_prefix__"] = prefix
|
||||
|
||||
case_insensitive = attrs.setdefault("__commands_flag_case_insensitive__", False)
|
||||
delimiter = attrs.setdefault("__commands_flag_delimiter__", ":")
|
||||
prefix = attrs.setdefault("__commands_flag_prefix__", "")
|
||||
|
||||
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
|
||||
flags[flag_name] = flag
|
||||
aliases.update({alias_name: flag_name for alias_name in flag.aliases})
|
||||
|
||||
forbidden = set(delimiter).union(prefix)
|
||||
for flag_name in flags:
|
||||
validate_flag_name(flag_name, forbidden)
|
||||
for alias_name in aliases:
|
||||
validate_flag_name(alias_name, forbidden)
|
||||
|
||||
regex_flags = 0
|
||||
if case_insensitive:
|
||||
flags = {key.casefold(): value for key, value in flags.items()}
|
||||
aliases = {key.casefold(): value.casefold() for key, value in aliases.items()}
|
||||
regex_flags = re.IGNORECASE
|
||||
|
||||
keys = list(re.escape(k) for k in flags)
|
||||
keys.extend(re.escape(a) for a in aliases)
|
||||
keys = sorted(keys, key=lambda t: len(t), reverse=True)
|
||||
|
||||
joined = "|".join(keys)
|
||||
pattern = re.compile(f"(({re.escape(prefix)})(?P<flag>{joined}){re.escape(delimiter)})", regex_flags)
|
||||
attrs["__commands_flag_regex__"] = pattern
|
||||
attrs["__commands_flags__"] = flags
|
||||
attrs["__commands_flag_aliases__"] = aliases
|
||||
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
async def tuple_convert_all(ctx: Context, argument: str, flag: Flag, converter: Any) -> Tuple[Any, ...]:
|
||||
view = StringView(argument)
|
||||
results = []
|
||||
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||
while not view.eof:
|
||||
view.skip_ws()
|
||||
if view.eof:
|
||||
break
|
||||
|
||||
word = view.get_quoted_word()
|
||||
if word is None:
|
||||
break
|
||||
|
||||
try:
|
||||
converted = await run_converters(ctx, converter, word, param)
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BadFlagArgument(flag) from e
|
||||
else:
|
||||
results.append(converted)
|
||||
|
||||
return tuple(results)
|
||||
|
||||
|
||||
async def tuple_convert_flag(ctx: Context, argument: str, flag: Flag, converters: Any) -> Tuple[Any, ...]:
|
||||
view = StringView(argument)
|
||||
results = []
|
||||
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||
for converter in converters:
|
||||
view.skip_ws()
|
||||
if view.eof:
|
||||
break
|
||||
|
||||
word = view.get_quoted_word()
|
||||
if word is None:
|
||||
break
|
||||
|
||||
try:
|
||||
converted = await run_converters(ctx, converter, word, param)
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BadFlagArgument(flag) from e
|
||||
else:
|
||||
results.append(converted)
|
||||
|
||||
if len(results) != len(converters):
|
||||
raise BadFlagArgument(flag)
|
||||
|
||||
return tuple(results)
|
||||
|
||||
|
||||
async def convert_flag(ctx, argument: str, flag: Flag, annotation: Any = None) -> Any:
|
||||
param: inspect.Parameter = ctx.current_parameter # type: ignore
|
||||
annotation = annotation or flag.annotation
|
||||
try:
|
||||
origin = annotation.__origin__
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if origin is tuple:
|
||||
if annotation.__args__[-1] is Ellipsis:
|
||||
return await tuple_convert_all(ctx, argument, flag, annotation.__args__[0])
|
||||
else:
|
||||
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
|
||||
elif origin is list:
|
||||
# typing.List[x]
|
||||
annotation = annotation.__args__[0]
|
||||
return await convert_flag(ctx, argument, flag, annotation)
|
||||
elif origin is Union and annotation.__args__[-1] is type(None):
|
||||
# typing.Optional[x]
|
||||
annotation = Union[annotation.__args__[:-1]]
|
||||
return await run_converters(ctx, annotation, argument, param)
|
||||
elif origin is dict:
|
||||
# typing.Dict[K, V] -> typing.Tuple[K, V]
|
||||
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
|
||||
|
||||
try:
|
||||
return await run_converters(ctx, annotation, argument, param)
|
||||
except CommandError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BadFlagArgument(flag) from e
|
||||
|
||||
|
||||
F = TypeVar("F", bound="FlagConverter")
|
||||
|
||||
|
||||
class FlagConverter(metaclass=FlagsMeta):
|
||||
"""A converter that allows for a user-friendly flag syntax.
|
||||
|
||||
The flags are defined using :pep:`526` type annotations similar
|
||||
to the :mod:`dataclasses` Python module. For more information on
|
||||
how this converter works, check the appropriate
|
||||
:ref:`documentation <ext_commands_flag_converter>`.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(flag_name, flag_value)`` pairs. This allows it
|
||||
to be, for example, constructed as a dict or a list of pairs.
|
||||
Note that aliases are not shown.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
case_insensitive: :class:`bool`
|
||||
A class parameter to toggle case insensitivity of the flag parsing.
|
||||
If ``True`` then flags are parsed in a case insensitive manner.
|
||||
Defaults to ``False``.
|
||||
prefix: :class:`str`
|
||||
The prefix that all flags must be prefixed with. By default
|
||||
there is no prefix.
|
||||
delimiter: :class:`str`
|
||||
The delimiter that separates a flag's argument from the flag's name.
|
||||
By default this is ``:``.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_flags(cls) -> Dict[str, Flag]:
|
||||
"""Dict[:class:`str`, :class:`Flag`]: A mapping of flag name to flag object this converter has."""
|
||||
return cls.__commands_flags__.copy()
|
||||
|
||||
@classmethod
|
||||
def _can_be_constructible(cls) -> bool:
|
||||
return all(not flag.required for flag in cls.__commands_flags__.values())
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
for flag in self.__class__.__commands_flags__.values():
|
||||
yield (flag.name, getattr(self, flag.attribute))
|
||||
|
||||
@classmethod
|
||||
async def _construct_default(cls: Type[F], ctx: Context) -> F:
|
||||
self: F = cls.__new__(cls)
|
||||
flags = cls.__commands_flags__
|
||||
for flag in flags.values():
|
||||
if callable(flag.default):
|
||||
default = await maybe_coroutine(flag.default, ctx)
|
||||
setattr(self, flag.attribute, default)
|
||||
else:
|
||||
setattr(self, flag.attribute, flag.default)
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
pairs = " ".join([f"{flag.attribute}={getattr(self, flag.attribute)!r}" for flag in self.get_flags().values()])
|
||||
return f"<{self.__class__.__name__} {pairs}>"
|
||||
|
||||
@classmethod
|
||||
def parse_flags(cls, argument: str) -> Dict[str, List[str]]:
|
||||
result: Dict[str, List[str]] = {}
|
||||
flags = cls.__commands_flags__
|
||||
aliases = cls.__commands_flag_aliases__
|
||||
last_position = 0
|
||||
last_flag: Optional[Flag] = None
|
||||
|
||||
case_insensitive = cls.__commands_flag_case_insensitive__
|
||||
for match in cls.__commands_flag_regex__.finditer(argument):
|
||||
begin, end = match.span(0)
|
||||
key = match.group("flag")
|
||||
if case_insensitive:
|
||||
key = key.casefold()
|
||||
|
||||
if key in aliases:
|
||||
key = aliases[key]
|
||||
|
||||
flag = flags.get(key)
|
||||
if last_position and last_flag is not None:
|
||||
value = argument[last_position : begin - 1].lstrip()
|
||||
if not value:
|
||||
raise MissingFlagArgument(last_flag)
|
||||
|
||||
try:
|
||||
values = result[last_flag.name]
|
||||
except KeyError:
|
||||
result[last_flag.name] = [value]
|
||||
else:
|
||||
values.append(value)
|
||||
|
||||
last_position = end
|
||||
last_flag = flag
|
||||
|
||||
# Add the remaining string to the last available flag
|
||||
if last_position and last_flag is not None:
|
||||
value = argument[last_position:].strip()
|
||||
if not value:
|
||||
raise MissingFlagArgument(last_flag)
|
||||
|
||||
try:
|
||||
values = result[last_flag.name]
|
||||
except KeyError:
|
||||
result[last_flag.name] = [value]
|
||||
else:
|
||||
values.append(value)
|
||||
|
||||
# Verification of values will come at a later stage
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def convert(cls: Type[F], ctx: Context, argument: str) -> F:
|
||||
"""|coro|
|
||||
|
||||
The method that actually converters an argument to the flag mapping.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cls: Type[:class:`FlagConverter`]
|
||||
The flag converter class.
|
||||
ctx: :class:`Context`
|
||||
The invocation context.
|
||||
argument: :class:`str`
|
||||
The argument to convert from.
|
||||
|
||||
Raises
|
||||
--------
|
||||
FlagError
|
||||
A flag related parsing error.
|
||||
CommandError
|
||||
A command related error.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`FlagConverter`
|
||||
The flag converter instance with all flags parsed.
|
||||
"""
|
||||
arguments = cls.parse_flags(argument)
|
||||
flags = cls.__commands_flags__
|
||||
|
||||
self: F = cls.__new__(cls)
|
||||
for name, flag in flags.items():
|
||||
try:
|
||||
values = arguments[name]
|
||||
except KeyError:
|
||||
if flag.required:
|
||||
raise MissingRequiredFlag(flag)
|
||||
else:
|
||||
if callable(flag.default):
|
||||
default = await maybe_coroutine(flag.default, ctx)
|
||||
setattr(self, flag.attribute, default)
|
||||
else:
|
||||
setattr(self, flag.attribute, flag.default)
|
||||
continue
|
||||
|
||||
if flag.max_args > 0 and len(values) > flag.max_args:
|
||||
if flag.override:
|
||||
values = values[-flag.max_args :]
|
||||
else:
|
||||
raise TooManyFlags(flag, values)
|
||||
|
||||
# Special case:
|
||||
if flag.max_args == 1:
|
||||
value = await convert_flag(ctx, values[0], flag)
|
||||
setattr(self, flag.attribute, value)
|
||||
continue
|
||||
|
||||
# Another special case, tuple parsing.
|
||||
# Tuple parsing is basically converting arguments within the flag
|
||||
# So, given flag: hello 20 as the input and Tuple[str, int] as the type hint
|
||||
# We would receive ('hello', 20) as the resulting value
|
||||
# This uses the same whitespace and quoting rules as regular parameters.
|
||||
values = [await convert_flag(ctx, value, flag) for value in values]
|
||||
|
||||
if flag.cast_to_dict:
|
||||
values = dict(values) # type: ignore
|
||||
|
||||
setattr(self, flag.attribute, values)
|
||||
|
||||
return self
|
||||
@@ -27,16 +27,22 @@ import copy
|
||||
import functools
|
||||
import inspect
|
||||
import re
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import discord.utils
|
||||
|
||||
from .core import Group, Command
|
||||
from .errors import CommandError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context
|
||||
|
||||
__all__ = (
|
||||
'Paginator',
|
||||
'HelpCommand',
|
||||
'DefaultHelpCommand',
|
||||
'MinimalHelpCommand',
|
||||
"Paginator",
|
||||
"HelpCommand",
|
||||
"DefaultHelpCommand",
|
||||
"MinimalHelpCommand",
|
||||
)
|
||||
|
||||
# help -> shows info of bot on top/bottom and lists subcommands
|
||||
@@ -60,6 +66,7 @@ __all__ = (
|
||||
# Type <prefix>help command for more info on a command.
|
||||
# You can also type <prefix>help category for more info on a category.
|
||||
|
||||
|
||||
class Paginator:
|
||||
"""A class that aids in paginating code blocks for Discord messages.
|
||||
|
||||
@@ -81,7 +88,8 @@ class Paginator:
|
||||
The character string inserted between lines. e.g. a newline character.
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
||||
|
||||
def __init__(self, prefix="```", suffix="```", max_size=2000, linesep="\n"):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size
|
||||
@@ -92,7 +100,7 @@ class Paginator:
|
||||
"""Clears the paginator to have no pages."""
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@@ -110,7 +118,7 @@ class Paginator:
|
||||
def _linesep_len(self):
|
||||
return len(self.linesep)
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
def add_line(self, line="", *, empty=False):
|
||||
"""Adds a line to the current page.
|
||||
|
||||
If the line exceeds the :attr:`max_size` then an exception
|
||||
@@ -130,7 +138,7 @@ class Paginator:
|
||||
"""
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
|
||||
if len(line) > max_page_size:
|
||||
raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
|
||||
raise RuntimeError(f"Line exceeds maximum page size {max_page_size}")
|
||||
|
||||
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
|
||||
self.close_page()
|
||||
@@ -139,7 +147,7 @@ class Paginator:
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._current_page.append("")
|
||||
self._count += self._linesep_len
|
||||
|
||||
def close_page(self):
|
||||
@@ -150,7 +158,7 @@ class Paginator:
|
||||
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@@ -168,13 +176,15 @@ class Paginator:
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
|
||||
fmt = "<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>"
|
||||
return fmt.format(self)
|
||||
|
||||
|
||||
def _not_overriden(f):
|
||||
f.__help_command_not_overriden__ = True
|
||||
return f
|
||||
|
||||
|
||||
class _HelpCommandImpl(Command):
|
||||
def __init__(self, inject, *args, **kwargs):
|
||||
super().__init__(inject.command_callback, *args, **kwargs)
|
||||
@@ -187,7 +197,7 @@ class _HelpCommandImpl(Command):
|
||||
self.callback = injected.command_callback
|
||||
|
||||
on_error = injected.on_help_command_error
|
||||
if not hasattr(on_error, '__help_command_not_overriden__'):
|
||||
if not hasattr(on_error, "__help_command_not_overriden__"):
|
||||
if self.cog is not None:
|
||||
self.on_error = self._on_error_cog_implementation
|
||||
else:
|
||||
@@ -212,9 +222,9 @@ class _HelpCommandImpl(Command):
|
||||
def clean_params(self):
|
||||
result = self.params.copy()
|
||||
try:
|
||||
result.popitem(last=False)
|
||||
except Exception:
|
||||
raise ValueError('Missing context parameter') from None
|
||||
del result[next(iter(result))]
|
||||
except StopIteration:
|
||||
raise ValueError("Missing context parameter") from None
|
||||
else:
|
||||
return result
|
||||
|
||||
@@ -250,6 +260,7 @@ class _HelpCommandImpl(Command):
|
||||
cog.walk_commands = cog.walk_commands.__wrapped__
|
||||
self.cog = None
|
||||
|
||||
|
||||
class HelpCommand:
|
||||
r"""The base implementation for help command formatting.
|
||||
|
||||
@@ -272,11 +283,11 @@ class HelpCommand:
|
||||
Defaults to ``False``.
|
||||
verify_checks: Optional[:class:`bool`]
|
||||
Specifies if commands should have their :attr:`.Command.checks` called
|
||||
and verified. If ``True``, always calls :attr:`.Commands.checks`.
|
||||
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
|
||||
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
|
||||
and verified. If ``True``, always calls :attr:`.Command.checks`.
|
||||
If ``None``, only calls :attr:`.Command.checks` in a guild setting.
|
||||
If ``False``, never calls :attr:`.Command.checks`. Defaults to ``True``.
|
||||
|
||||
..versionchanged:: 1.7
|
||||
.. versionchanged:: 1.7
|
||||
command_attrs: :class:`dict`
|
||||
A dictionary of options to pass in for the construction of the help command.
|
||||
This allows you to change the command behaviour without actually changing
|
||||
@@ -285,13 +296,13 @@ class HelpCommand:
|
||||
"""
|
||||
|
||||
MENTION_TRANSFORMS = {
|
||||
'@everyone': '@\u200beveryone',
|
||||
'@here': '@\u200bhere',
|
||||
r'<@!?[0-9]{17,22}>': '@deleted-user',
|
||||
r'<@&[0-9]{17,22}>': '@deleted-role'
|
||||
"@everyone": "@\u200beveryone",
|
||||
"@here": "@\u200bhere",
|
||||
r"<@!?[0-9]{17,22}>": "@deleted-user",
|
||||
r"<@&[0-9]{17,22}>": "@deleted-role",
|
||||
}
|
||||
|
||||
MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
|
||||
MENTION_PATTERN = re.compile("|".join(MENTION_TRANSFORMS.keys()))
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# To prevent race conditions of a single instance while also allowing
|
||||
@@ -305,20 +316,17 @@ class HelpCommand:
|
||||
# The keys can be safely copied as-is since they're 99.99% certain of being
|
||||
# string keys
|
||||
deepcopy = copy.deepcopy
|
||||
self.__original_kwargs__ = {
|
||||
k: deepcopy(v)
|
||||
for k, v in kwargs.items()
|
||||
}
|
||||
self.__original_kwargs__ = {k: deepcopy(v) for k, v in kwargs.items()}
|
||||
self.__original_args__ = deepcopy(args)
|
||||
return self
|
||||
|
||||
def __init__(self, **options):
|
||||
self.show_hidden = options.pop('show_hidden', False)
|
||||
self.verify_checks = options.pop('verify_checks', True)
|
||||
self.command_attrs = attrs = options.pop('command_attrs', {})
|
||||
attrs.setdefault('name', 'help')
|
||||
attrs.setdefault('help', 'Shows this message')
|
||||
self.context = None
|
||||
self.show_hidden = options.pop("show_hidden", False)
|
||||
self.verify_checks = options.pop("verify_checks", True)
|
||||
self.command_attrs = attrs = options.pop("command_attrs", {})
|
||||
attrs.setdefault("name", "help")
|
||||
attrs.setdefault("help", "Shows this message")
|
||||
self.context: Context = discord.utils.MISSING
|
||||
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
|
||||
|
||||
def copy(self):
|
||||
@@ -369,25 +377,10 @@ class HelpCommand:
|
||||
def get_bot_mapping(self):
|
||||
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
||||
bot = self.context.bot
|
||||
mapping = {
|
||||
cog: cog.get_commands()
|
||||
for cog in bot.cogs.values()
|
||||
}
|
||||
mapping = {cog: cog.get_commands() for cog in bot.cogs.values()}
|
||||
mapping[None] = [c for c in bot.commands if c.cog is None]
|
||||
return mapping
|
||||
|
||||
@property
|
||||
def clean_prefix(self):
|
||||
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
|
||||
user = self.context.guild.me if self.context.guild else self.context.bot.user
|
||||
# this breaks if the prefix mention is not the bot itself but I
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
# for this common use case rather than waste performance for the
|
||||
# odd one.
|
||||
pattern = re.compile(fr"<@!?{user.id}>")
|
||||
display_name = user.display_name.replace('\\', r'\\')
|
||||
return pattern.sub('@' + display_name, self.context.prefix)
|
||||
|
||||
@property
|
||||
def invoked_with(self):
|
||||
"""Similar to :attr:`Context.invoked_with` except properly handles
|
||||
@@ -429,20 +422,20 @@ class HelpCommand:
|
||||
if not parent.signature or parent.invoke_without_command:
|
||||
entries.append(parent.name)
|
||||
else:
|
||||
entries.append(parent.name + ' ' + parent.signature)
|
||||
entries.append(parent.name + " " + parent.signature)
|
||||
parent = parent.parent
|
||||
parent_sig = ' '.join(reversed(entries))
|
||||
parent_sig = " ".join(reversed(entries))
|
||||
|
||||
if len(command.aliases) > 0:
|
||||
aliases = '|'.join(command.aliases)
|
||||
fmt = f'[{command.name}|{aliases}]'
|
||||
aliases = "|".join(command.aliases)
|
||||
fmt = f"[{command.name}|{aliases}]"
|
||||
if parent_sig:
|
||||
fmt = parent_sig + ' ' + fmt
|
||||
fmt = parent_sig + " " + fmt
|
||||
alias = fmt
|
||||
else:
|
||||
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
||||
alias = command.name if not parent_sig else parent_sig + " " + command.name
|
||||
|
||||
return f'{self.clean_prefix}{alias} {command.signature}'
|
||||
return f"{self.context.clean_prefix}{alias} {command.signature}"
|
||||
|
||||
def remove_mentions(self, string):
|
||||
"""Removes mentions from the string to prevent abuse.
|
||||
@@ -456,7 +449,7 @@ class HelpCommand:
|
||||
"""
|
||||
|
||||
def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
|
||||
return transforms.get(obj.group(0), '@invalid')
|
||||
return transforms.get(obj.group(0), "@invalid")
|
||||
|
||||
return self.MENTION_PATTERN.sub(replace, string)
|
||||
|
||||
@@ -607,10 +600,7 @@ class HelpCommand:
|
||||
The maximum width of the commands.
|
||||
"""
|
||||
|
||||
as_lengths = (
|
||||
discord.utils._string_width(c.name)
|
||||
for c in commands
|
||||
)
|
||||
as_lengths = (discord.utils._string_width(c.name) for c in commands)
|
||||
return max(as_lengths, default=0)
|
||||
|
||||
def get_destination(self):
|
||||
@@ -625,14 +615,13 @@ class HelpCommand:
|
||||
:class:`.abc.Messageable`
|
||||
The destination where the help command will be output.
|
||||
"""
|
||||
return self.context.channel
|
||||
return self.context
|
||||
|
||||
async def send_error_message(self, error):
|
||||
"""|coro|
|
||||
|
||||
Handles the implementation when an error happens in the help command.
|
||||
For example, the result of :meth:`command_not_found` or
|
||||
:meth:`command_has_no_subcommand_found` will be passed here.
|
||||
For example, the result of :meth:`command_not_found` will be passed here.
|
||||
|
||||
You can override this method to customise the behaviour.
|
||||
|
||||
@@ -857,7 +846,7 @@ class HelpCommand:
|
||||
# Since we want to have detailed errors when someone
|
||||
# passes an invalid subcommand, we need to walk through
|
||||
# the command group chain ourselves.
|
||||
keys = command.split(' ')
|
||||
keys = command.split(" ")
|
||||
cmd = bot.all_commands.get(keys[0])
|
||||
if cmd is None:
|
||||
string = await maybe_coro(self.command_not_found, self.remove_mentions(keys[0]))
|
||||
@@ -880,6 +869,7 @@ class HelpCommand:
|
||||
else:
|
||||
return await self.send_command_help(cmd)
|
||||
|
||||
|
||||
class DefaultHelpCommand(HelpCommand):
|
||||
"""The implementation of the default help command.
|
||||
|
||||
@@ -917,14 +907,14 @@ class DefaultHelpCommand(HelpCommand):
|
||||
"""
|
||||
|
||||
def __init__(self, **options):
|
||||
self.width = options.pop('width', 80)
|
||||
self.indent = options.pop('indent', 2)
|
||||
self.sort_commands = options.pop('sort_commands', True)
|
||||
self.dm_help = options.pop('dm_help', False)
|
||||
self.dm_help_threshold = options.pop('dm_help_threshold', 1000)
|
||||
self.commands_heading = options.pop('commands_heading', "Commands:")
|
||||
self.no_category = options.pop('no_category', 'No Category')
|
||||
self.paginator = options.pop('paginator', None)
|
||||
self.width = options.pop("width", 80)
|
||||
self.indent = options.pop("indent", 2)
|
||||
self.sort_commands = options.pop("sort_commands", True)
|
||||
self.dm_help = options.pop("dm_help", False)
|
||||
self.dm_help_threshold = options.pop("dm_help_threshold", 1000)
|
||||
self.commands_heading = options.pop("commands_heading", "Commands:")
|
||||
self.no_category = options.pop("no_category", "No Category")
|
||||
self.paginator = options.pop("paginator", None)
|
||||
|
||||
if self.paginator is None:
|
||||
self.paginator = Paginator()
|
||||
@@ -934,14 +924,16 @@ class DefaultHelpCommand(HelpCommand):
|
||||
def shorten_text(self, text):
|
||||
""":class:`str`: Shortens text to fit into the :attr:`width`."""
|
||||
if len(text) > self.width:
|
||||
return text[:self.width - 3] + '...'
|
||||
return text[: self.width - 3].rstrip() + "..."
|
||||
return text
|
||||
|
||||
def get_ending_note(self):
|
||||
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||
command_name = self.invoked_with
|
||||
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
|
||||
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
|
||||
return (
|
||||
f"Type {self.context.clean_prefix}{command_name} command for more info on a command.\n"
|
||||
f"You can also type {self.context.clean_prefix}{command_name} category for more info on a category."
|
||||
)
|
||||
|
||||
def add_indented_commands(self, commands, *, heading, max_size=None):
|
||||
"""Indents a list of commands after the specified heading.
|
||||
@@ -962,7 +954,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
if the list of commands is greater than 0.
|
||||
max_size: Optional[:class:`int`]
|
||||
The max size to use for the gap between indents.
|
||||
If unspecified, calls :meth:`get_max_size` on the
|
||||
If unspecified, calls :meth:`~HelpCommand.get_max_size` on the
|
||||
commands parameter.
|
||||
"""
|
||||
|
||||
@@ -985,6 +977,10 @@ class DefaultHelpCommand(HelpCommand):
|
||||
for page in self.paginator.pages:
|
||||
await destination.send(page)
|
||||
|
||||
interaction = self.context.interaction
|
||||
if interaction is not None and destination == self.context.author and not interaction.response.is_done():
|
||||
await interaction.response.send_message("Sent help to your DMs!", ephemeral=True)
|
||||
|
||||
def add_command_formatting(self, command):
|
||||
"""A utility function to format the non-indented block of commands and groups.
|
||||
|
||||
@@ -1015,7 +1011,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
elif self.dm_help is None and len(self.paginator) > self.dm_help_threshold:
|
||||
return ctx.author
|
||||
else:
|
||||
return ctx.channel
|
||||
return ctx
|
||||
|
||||
async def prepare_help_command(self, ctx, command):
|
||||
self.paginator.clear()
|
||||
@@ -1029,10 +1025,11 @@ class DefaultHelpCommand(HelpCommand):
|
||||
# <description> portion
|
||||
self.paginator.add_line(bot.description, empty=True)
|
||||
|
||||
no_category = f'\u200b{self.no_category}:'
|
||||
no_category = f"\u200b{self.no_category}:"
|
||||
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name + ':' if cog is not None else no_category
|
||||
return cog.qualified_name + ":" if cog is not None else no_category
|
||||
|
||||
filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
|
||||
max_size = self.get_max_size(filtered)
|
||||
@@ -1083,6 +1080,7 @@ class DefaultHelpCommand(HelpCommand):
|
||||
|
||||
await self.send_pages()
|
||||
|
||||
|
||||
class MinimalHelpCommand(HelpCommand):
|
||||
"""An implementation of a help command with minimal output.
|
||||
|
||||
@@ -1116,13 +1114,13 @@ class MinimalHelpCommand(HelpCommand):
|
||||
"""
|
||||
|
||||
def __init__(self, **options):
|
||||
self.sort_commands = options.pop('sort_commands', True)
|
||||
self.commands_heading = options.pop('commands_heading', "Commands")
|
||||
self.dm_help = options.pop('dm_help', False)
|
||||
self.dm_help_threshold = options.pop('dm_help_threshold', 1000)
|
||||
self.aliases_heading = options.pop('aliases_heading', "Aliases:")
|
||||
self.no_category = options.pop('no_category', 'No Category')
|
||||
self.paginator = options.pop('paginator', None)
|
||||
self.sort_commands = options.pop("sort_commands", True)
|
||||
self.commands_heading = options.pop("commands_heading", "Commands")
|
||||
self.dm_help = options.pop("dm_help", False)
|
||||
self.dm_help_threshold = options.pop("dm_help_threshold", 1000)
|
||||
self.aliases_heading = options.pop("aliases_heading", "Aliases:")
|
||||
self.no_category = options.pop("no_category", "No Category")
|
||||
self.paginator = options.pop("paginator", None)
|
||||
|
||||
if self.paginator is None:
|
||||
self.paginator = Paginator(suffix=None, prefix=None)
|
||||
@@ -1149,11 +1147,13 @@ class MinimalHelpCommand(HelpCommand):
|
||||
The help command opening note.
|
||||
"""
|
||||
command_name = self.invoked_with
|
||||
return "Use `{0}{1} [command]` for more info on a command.\n" \
|
||||
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
|
||||
return (
|
||||
f"Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n"
|
||||
f"You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category."
|
||||
)
|
||||
|
||||
def get_command_signature(self, command):
|
||||
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
|
||||
return f"{self.context.clean_prefix}{command.qualified_name} {command.signature}"
|
||||
|
||||
def get_ending_note(self):
|
||||
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
||||
@@ -1184,8 +1184,8 @@ class MinimalHelpCommand(HelpCommand):
|
||||
"""
|
||||
if commands:
|
||||
# U+2002 Middle Dot
|
||||
joined = '\u2002'.join(c.name for c in commands)
|
||||
self.paginator.add_line(f'__**{heading}**__')
|
||||
joined = "\u2002".join(c.name for c in commands)
|
||||
self.paginator.add_line(f"__**{heading}**__")
|
||||
self.paginator.add_line(joined)
|
||||
|
||||
def add_subcommand_formatting(self, command):
|
||||
@@ -1201,8 +1201,8 @@ class MinimalHelpCommand(HelpCommand):
|
||||
command: :class:`Command`
|
||||
The command to show information of.
|
||||
"""
|
||||
fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
|
||||
self.paginator.add_line(fmt.format(self.clean_prefix, command.qualified_name, command.short_doc))
|
||||
fmt = "{0}{1} \N{EN DASH} {2}" if command.short_doc else "{0}{1}"
|
||||
self.paginator.add_line(fmt.format(self.context.clean_prefix, command.qualified_name, command.short_doc))
|
||||
|
||||
def add_aliases_formatting(self, aliases):
|
||||
"""Adds the formatting information on a command's aliases.
|
||||
@@ -1272,7 +1272,8 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
no_category = f'\u200b{self.no_category}'
|
||||
no_category = f"\u200b{self.no_category}"
|
||||
|
||||
def get_category(command, *, no_category=no_category):
|
||||
cog = command.cog
|
||||
return cog.qualified_name if cog is not None else no_category
|
||||
@@ -1305,7 +1306,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
|
||||
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
|
||||
if filtered:
|
||||
self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
|
||||
self.paginator.add_line(f"**{cog.qualified_name} {self.commands_heading}**")
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
@@ -1325,7 +1326,7 @@ class MinimalHelpCommand(HelpCommand):
|
||||
if note:
|
||||
self.paginator.add_line(note, empty=True)
|
||||
|
||||
self.paginator.add_line(f'**{self.commands_heading}**')
|
||||
self.paginator.add_line(f"**{self.commands_heading}**")
|
||||
for command in filtered:
|
||||
self.add_subcommand_formatting(command)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
from .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, ExpectedClosingQuoteError
|
||||
|
||||
# map from opening quotes to closing quotes
|
||||
_quotes = {
|
||||
supported_quotes = {
|
||||
'"': '"',
|
||||
"‘": "’",
|
||||
"‚": "‛",
|
||||
@@ -44,7 +44,8 @@ _quotes = {
|
||||
"《": "》",
|
||||
"〈": "〉",
|
||||
}
|
||||
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
|
||||
_all_quotes = set(supported_quotes.keys()) | set(supported_quotes.values())
|
||||
|
||||
|
||||
class StringView:
|
||||
def __init__(self, buffer):
|
||||
@@ -81,20 +82,20 @@ class StringView:
|
||||
|
||||
def skip_string(self, string):
|
||||
strlen = len(string)
|
||||
if self.buffer[self.index:self.index + strlen] == string:
|
||||
if self.buffer[self.index : self.index + strlen] == string:
|
||||
self.previous = self.index
|
||||
self.index += strlen
|
||||
return True
|
||||
return False
|
||||
|
||||
def read_rest(self):
|
||||
result = self.buffer[self.index:]
|
||||
result = self.buffer[self.index :]
|
||||
self.previous = self.index
|
||||
self.index = self.end
|
||||
return result
|
||||
|
||||
def read(self, n):
|
||||
result = self.buffer[self.index:self.index + n]
|
||||
result = self.buffer[self.index : self.index + n]
|
||||
self.previous = self.index
|
||||
self.index += n
|
||||
return result
|
||||
@@ -120,7 +121,7 @@ class StringView:
|
||||
except IndexError:
|
||||
break
|
||||
self.previous = self.index
|
||||
result = self.buffer[self.index:self.index + pos]
|
||||
result = self.buffer[self.index : self.index + pos]
|
||||
self.index += pos
|
||||
return result
|
||||
|
||||
@@ -129,7 +130,7 @@ class StringView:
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
close_quote = _quotes.get(current)
|
||||
close_quote = supported_quotes.get(current)
|
||||
is_quoted = bool(close_quote)
|
||||
if is_quoted:
|
||||
result = []
|
||||
@@ -144,11 +145,11 @@ class StringView:
|
||||
if is_quoted:
|
||||
# unexpected EOF
|
||||
raise ExpectedClosingQuoteError(close_quote)
|
||||
return ''.join(result)
|
||||
return "".join(result)
|
||||
|
||||
# currently we accept strings in the format of "hello world"
|
||||
# to embed a quote inside the string you must escape it: "a \"world\""
|
||||
if current == '\\':
|
||||
if current == "\\":
|
||||
next_char = self.get()
|
||||
if not next_char:
|
||||
# string ends with \ and no character after it
|
||||
@@ -156,7 +157,7 @@ class StringView:
|
||||
# if we're quoted then we're expecting a closing quote
|
||||
raise ExpectedClosingQuoteError(close_quote)
|
||||
# if we aren't then we just let it through
|
||||
return ''.join(result)
|
||||
return "".join(result)
|
||||
|
||||
if next_char in _escaped_quotes:
|
||||
# escaped quote
|
||||
@@ -179,14 +180,13 @@ class StringView:
|
||||
raise InvalidEndOfQuotedStringError(next_char)
|
||||
|
||||
# we're quoted so it's okay
|
||||
return ''.join(result)
|
||||
return "".join(result)
|
||||
|
||||
if current.isspace() and not is_quoted:
|
||||
# end of word found
|
||||
return ''.join(result)
|
||||
return "".join(result)
|
||||
|
||||
result.append(current)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.format(self)
|
||||
return f"<StringView pos: {self.index} prev: {self.previous} end: {self.end} eof: {self.eof}>"
|
||||
|
||||
@@ -22,35 +22,90 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from collections.abc import Sequence
|
||||
from discord.backoff import ExponentialBackoff
|
||||
from discord.utils import MISSING
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__all__ = ("loop",)
|
||||
|
||||
__all__ = (
|
||||
'loop',
|
||||
)
|
||||
T = TypeVar("T")
|
||||
_func = Callable[..., Awaitable[Any]]
|
||||
LF = TypeVar("LF", bound=_func)
|
||||
FT = TypeVar("FT", bound=_func)
|
||||
ET = TypeVar("ET", bound=Callable[[Any, BaseException], Awaitable[Any]])
|
||||
|
||||
class Loop:
|
||||
|
||||
class SleepHandle:
|
||||
__slots__ = ("future", "loop", "handle")
|
||||
|
||||
def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> None:
|
||||
self.loop = loop
|
||||
self.future = future = loop.create_future()
|
||||
relative_delta = discord.utils.compute_timedelta(dt)
|
||||
self.handle = loop.call_later(relative_delta, future.set_result, True)
|
||||
|
||||
def recalculate(self, dt: datetime.datetime) -> None:
|
||||
self.handle.cancel()
|
||||
relative_delta = discord.utils.compute_timedelta(dt)
|
||||
self.handle = self.loop.call_later(relative_delta, self.future.set_result, True)
|
||||
|
||||
def wait(self) -> asyncio.Future[Any]:
|
||||
return self.future
|
||||
|
||||
def done(self) -> bool:
|
||||
return self.future.done()
|
||||
|
||||
def cancel(self) -> None:
|
||||
self.handle.cancel()
|
||||
self.future.cancel()
|
||||
|
||||
|
||||
class Loop(Generic[LF]):
|
||||
"""A background task helper that abstracts the loop and reconnection logic for you.
|
||||
|
||||
The main interface to create this is through :func:`loop`.
|
||||
"""
|
||||
def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
|
||||
self.coro = coro
|
||||
self.reconnect = reconnect
|
||||
self.loop = loop
|
||||
self.count = count
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coro: LF,
|
||||
seconds: float,
|
||||
hours: float,
|
||||
minutes: float,
|
||||
time: Union[datetime.time, Sequence[datetime.time]],
|
||||
count: Optional[int],
|
||||
reconnect: bool,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
) -> None:
|
||||
self.coro: LF = coro
|
||||
self.reconnect: bool = reconnect
|
||||
self.loop: asyncio.AbstractEventLoop = loop
|
||||
self.count: Optional[int] = count
|
||||
self._current_loop = 0
|
||||
self._task = None
|
||||
self._handle: SleepHandle = MISSING
|
||||
self._task: asyncio.Task[None] = MISSING
|
||||
self._injected = None
|
||||
self._valid_exception = (
|
||||
OSError,
|
||||
@@ -67,18 +122,18 @@ class Loop:
|
||||
self._stop_next_iteration = False
|
||||
|
||||
if self.count is not None and self.count <= 0:
|
||||
raise ValueError('count must be greater than 0 or None.')
|
||||
raise ValueError("count must be greater than 0 or None.")
|
||||
|
||||
self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
|
||||
self.change_interval(seconds=seconds, minutes=minutes, hours=hours, time=time)
|
||||
self._last_iteration_failed = False
|
||||
self._last_iteration = None
|
||||
self._last_iteration: datetime.datetime = MISSING
|
||||
self._next_iteration = None
|
||||
|
||||
if not inspect.iscoroutinefunction(self.coro):
|
||||
raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))
|
||||
raise TypeError(f"Expected coroutine function, not {type(self.coro).__name__!r}.")
|
||||
|
||||
async def _call_loop_function(self, name, *args, **kwargs):
|
||||
coro = getattr(self, '_' + name)
|
||||
async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> None:
|
||||
coro = getattr(self, "_" + name)
|
||||
if coro is None:
|
||||
return
|
||||
|
||||
@@ -87,14 +142,22 @@ class Loop:
|
||||
else:
|
||||
await coro(*args, **kwargs)
|
||||
|
||||
async def _loop(self, *args, **kwargs):
|
||||
def _try_sleep_until(self, dt: datetime.datetime):
|
||||
self._handle = SleepHandle(dt=dt, loop=self.loop)
|
||||
return self._handle.wait()
|
||||
|
||||
async def _loop(self, *args: Any, **kwargs: Any) -> None:
|
||||
backoff = ExponentialBackoff()
|
||||
await self._call_loop_function('before_loop')
|
||||
sleep_until = discord.utils.sleep_until
|
||||
await self._call_loop_function("before_loop")
|
||||
self._last_iteration_failed = False
|
||||
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
|
||||
if self._time is not MISSING:
|
||||
# the time index should be prepared every time the internal loop is started
|
||||
self._prepare_time_index()
|
||||
self._next_iteration = self._get_next_sleep_time()
|
||||
else:
|
||||
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
|
||||
try:
|
||||
await asyncio.sleep(0) # allows canceling in before_loop
|
||||
await self._try_sleep_until(self._next_iteration)
|
||||
while True:
|
||||
if not self._last_iteration_failed:
|
||||
self._last_iteration = self._next_iteration
|
||||
@@ -102,42 +165,56 @@ class Loop:
|
||||
try:
|
||||
await self.coro(*args, **kwargs)
|
||||
self._last_iteration_failed = False
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if now > self._next_iteration:
|
||||
self._next_iteration = now
|
||||
except self._valid_exception:
|
||||
self._last_iteration_failed = True
|
||||
if not self.reconnect:
|
||||
raise
|
||||
await asyncio.sleep(backoff.delay())
|
||||
else:
|
||||
await self._try_sleep_until(self._next_iteration)
|
||||
|
||||
if self._stop_next_iteration:
|
||||
return
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if now > self._next_iteration:
|
||||
self._next_iteration = now
|
||||
if self._time is not MISSING:
|
||||
self._prepare_time_index(now)
|
||||
|
||||
self._current_loop += 1
|
||||
if self._current_loop == self.count:
|
||||
break
|
||||
|
||||
await sleep_until(self._next_iteration)
|
||||
except asyncio.CancelledError:
|
||||
self._is_being_cancelled = True
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._has_failed = True
|
||||
await self._call_loop_function('error', exc)
|
||||
await self._call_loop_function("error", exc)
|
||||
raise exc
|
||||
finally:
|
||||
await self._call_loop_function('after_loop')
|
||||
await self._call_loop_function("after_loop")
|
||||
self._handle.cancel()
|
||||
self._is_being_cancelled = False
|
||||
self._current_loop = 0
|
||||
self._stop_next_iteration = False
|
||||
self._has_failed = False
|
||||
|
||||
def __get__(self, obj, objtype):
|
||||
def __get__(self, obj: T, objtype: Type[T]) -> Loop[LF]:
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
|
||||
count=self.count, reconnect=self.reconnect, loop=self.loop)
|
||||
copy: Loop[LF] = Loop(
|
||||
self.coro,
|
||||
seconds=self._seconds,
|
||||
hours=self._hours,
|
||||
minutes=self._minutes,
|
||||
time=self._time,
|
||||
count=self.count,
|
||||
reconnect=self.reconnect,
|
||||
loop=self.loop,
|
||||
)
|
||||
copy._injected = obj
|
||||
copy._before_loop = self._before_loop
|
||||
copy._after_loop = self._after_loop
|
||||
@@ -146,23 +223,63 @@ class Loop:
|
||||
return copy
|
||||
|
||||
@property
|
||||
def current_loop(self):
|
||||
def seconds(self) -> Optional[float]:
|
||||
"""Optional[:class:`float`]: Read-only value for the number of seconds
|
||||
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._seconds is not MISSING:
|
||||
return self._seconds
|
||||
|
||||
@property
|
||||
def minutes(self) -> Optional[float]:
|
||||
"""Optional[:class:`float`]: Read-only value for the number of minutes
|
||||
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._minutes is not MISSING:
|
||||
return self._minutes
|
||||
|
||||
@property
|
||||
def hours(self) -> Optional[float]:
|
||||
"""Optional[:class:`float`]: Read-only value for the number of hours
|
||||
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._hours is not MISSING:
|
||||
return self._hours
|
||||
|
||||
@property
|
||||
def time(self) -> Optional[List[datetime.time]]:
|
||||
"""Optional[List[:class:`datetime.time`]]: Read-only list for the exact times this loop runs at.
|
||||
``None`` if relative times were passed instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._time is not MISSING:
|
||||
return self._time.copy()
|
||||
|
||||
@property
|
||||
def current_loop(self) -> int:
|
||||
""":class:`int`: The current iteration of the loop."""
|
||||
return self._current_loop
|
||||
|
||||
@property
|
||||
def next_iteration(self):
|
||||
def next_iteration(self) -> Optional[datetime.datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
if self._task is None:
|
||||
if self._task is MISSING:
|
||||
return None
|
||||
elif self._task and self._task.done() or self._stop_next_iteration:
|
||||
return None
|
||||
return self._next_iteration
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
r"""|coro|
|
||||
|
||||
Calls the internal callback that the task holds.
|
||||
@@ -182,7 +299,7 @@ class Loop:
|
||||
|
||||
return await self.coro(*args, **kwargs)
|
||||
|
||||
def start(self, *args, **kwargs):
|
||||
def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
|
||||
r"""Starts the internal task in the event loop.
|
||||
|
||||
Parameters
|
||||
@@ -203,19 +320,19 @@ class Loop:
|
||||
The task that has been created.
|
||||
"""
|
||||
|
||||
if self._task is not None and not self._task.done():
|
||||
raise RuntimeError('Task is already launched and is not completed.')
|
||||
if self._task is not MISSING and not self._task.done():
|
||||
raise RuntimeError("Task is already launched and is not completed.")
|
||||
|
||||
if self._injected is not None:
|
||||
args = (self._injected, *args)
|
||||
|
||||
if self.loop is None:
|
||||
if self.loop is MISSING:
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
self._task = self.loop.create_task(self._loop(*args, **kwargs))
|
||||
return self._task
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
r"""Gracefully stops the task from running.
|
||||
|
||||
Unlike :meth:`cancel`\, this allows the task to finish its
|
||||
@@ -233,18 +350,18 @@ class Loop:
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
if self._task and not self._task.done():
|
||||
if self._task is not MISSING and not self._task.done():
|
||||
self._stop_next_iteration = True
|
||||
|
||||
def _can_be_cancelled(self):
|
||||
return not self._is_being_cancelled and self._task and not self._task.done()
|
||||
def _can_be_cancelled(self) -> bool:
|
||||
return bool(not self._is_being_cancelled and self._task and not self._task.done())
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
"""Cancels the internal task, if it is running."""
|
||||
if self._can_be_cancelled():
|
||||
self._task.cancel()
|
||||
|
||||
def restart(self, *args, **kwargs):
|
||||
def restart(self, *args: Any, **kwargs: Any) -> None:
|
||||
r"""A convenience method to restart the internal task.
|
||||
|
||||
.. note::
|
||||
@@ -255,12 +372,12 @@ class Loop:
|
||||
Parameters
|
||||
------------
|
||||
\*args
|
||||
The arguments to to use.
|
||||
The arguments to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
"""
|
||||
|
||||
def restart_when_over(fut, *, args=args, kwargs=kwargs):
|
||||
def restart_when_over(fut: Any, *, args: Any = args, kwargs: Any = kwargs) -> None:
|
||||
self._task.remove_done_callback(restart_when_over)
|
||||
self.start(*args, **kwargs)
|
||||
|
||||
@@ -268,7 +385,7 @@ class Loop:
|
||||
self._task.add_done_callback(restart_when_over)
|
||||
self._task.cancel()
|
||||
|
||||
def add_exception_type(self, *exceptions):
|
||||
def add_exception_type(self, *exceptions: Type[BaseException]) -> None:
|
||||
r"""Adds exception types to be handled during the reconnect logic.
|
||||
|
||||
By default the exception types handled are those handled by
|
||||
@@ -291,13 +408,13 @@ class Loop:
|
||||
|
||||
for exc in exceptions:
|
||||
if not inspect.isclass(exc):
|
||||
raise TypeError(f'{exc!r} must be a class.')
|
||||
raise TypeError(f"{exc!r} must be a class.")
|
||||
if not issubclass(exc, BaseException):
|
||||
raise TypeError(f'{exc!r} must inherit from BaseException.')
|
||||
raise TypeError(f"{exc!r} must inherit from BaseException.")
|
||||
|
||||
self._valid_exception = (*self._valid_exception, *exceptions)
|
||||
|
||||
def clear_exception_types(self):
|
||||
def clear_exception_types(self) -> None:
|
||||
"""Removes all exception types that are handled.
|
||||
|
||||
.. note::
|
||||
@@ -306,7 +423,7 @@ class Loop:
|
||||
"""
|
||||
self._valid_exception = tuple()
|
||||
|
||||
def remove_exception_type(self, *exceptions):
|
||||
def remove_exception_type(self, *exceptions: Type[BaseException]) -> bool:
|
||||
r"""Removes exception types from being handled during the reconnect logic.
|
||||
|
||||
Parameters
|
||||
@@ -323,34 +440,34 @@ class Loop:
|
||||
self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
|
||||
return len(self._valid_exception) == old_length - len(exceptions)
|
||||
|
||||
def get_task(self):
|
||||
def get_task(self) -> Optional[asyncio.Task[None]]:
|
||||
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
|
||||
return self._task
|
||||
return self._task if self._task is not MISSING else None
|
||||
|
||||
def is_being_cancelled(self):
|
||||
def is_being_cancelled(self) -> bool:
|
||||
"""Whether the task is being cancelled."""
|
||||
return self._is_being_cancelled
|
||||
|
||||
def failed(self):
|
||||
def failed(self) -> bool:
|
||||
""":class:`bool`: Whether the internal task has failed.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return self._has_failed
|
||||
|
||||
def is_running(self):
|
||||
def is_running(self) -> bool:
|
||||
""":class:`bool`: Check if the task is currently running.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
return not bool(self._task.done()) if self._task else False
|
||||
return not bool(self._task.done()) if self._task is not MISSING else False
|
||||
|
||||
async def _error(self, *args):
|
||||
exception = args[-1]
|
||||
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
|
||||
async def _error(self, *args: Any) -> None:
|
||||
exception: Exception = args[-1]
|
||||
print(f"Unhandled exception in internal background task {self.coro.__name__!r}.", file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
def before_loop(self, coro):
|
||||
def before_loop(self, coro: FT) -> FT:
|
||||
"""A decorator that registers a coroutine to be called before the loop starts running.
|
||||
|
||||
This is useful if you want to wait for some bot state before the loop starts,
|
||||
@@ -370,12 +487,12 @@ class Loop:
|
||||
"""
|
||||
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
|
||||
|
||||
self._before_loop = coro
|
||||
return coro
|
||||
|
||||
def after_loop(self, coro):
|
||||
def after_loop(self, coro: FT) -> FT:
|
||||
"""A decorator that register a coroutine to be called after the loop finished running.
|
||||
|
||||
The coroutine must take no arguments (except ``self`` in a class context).
|
||||
@@ -398,12 +515,12 @@ class Loop:
|
||||
"""
|
||||
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
|
||||
|
||||
self._after_loop = coro
|
||||
return coro
|
||||
|
||||
def error(self, coro):
|
||||
def error(self, coro: ET) -> ET:
|
||||
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
||||
|
||||
The coroutine must take only one argument the exception raised (except ``self`` in a class context).
|
||||
@@ -424,22 +541,90 @@ class Loop:
|
||||
The function was not a coroutine.
|
||||
"""
|
||||
if not inspect.iscoroutinefunction(coro):
|
||||
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
|
||||
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
|
||||
|
||||
self._error = coro
|
||||
self._error = coro # type: ignore
|
||||
return coro
|
||||
|
||||
def _get_next_sleep_time(self):
|
||||
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
|
||||
def _get_next_sleep_time(self) -> datetime.datetime:
|
||||
if self._sleep is not MISSING:
|
||||
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
|
||||
|
||||
def change_interval(self, *, seconds=0, minutes=0, hours=0):
|
||||
if self._time_index >= len(self._time):
|
||||
self._time_index = 0
|
||||
if self._current_loop == 0:
|
||||
# if we're at the last index on the first iteration, we need to sleep until tomorrow
|
||||
return datetime.datetime.combine(
|
||||
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1), self._time[0]
|
||||
)
|
||||
|
||||
next_time = self._time[self._time_index]
|
||||
|
||||
if self._current_loop == 0:
|
||||
self._time_index += 1
|
||||
return datetime.datetime.combine(datetime.datetime.now(datetime.timezone.utc), next_time)
|
||||
|
||||
next_date = self._last_iteration
|
||||
if self._time_index == 0:
|
||||
# we can assume that the earliest time should be scheduled for "tomorrow"
|
||||
next_date += datetime.timedelta(days=1)
|
||||
|
||||
self._time_index += 1
|
||||
return datetime.datetime.combine(next_date, next_time)
|
||||
|
||||
def _prepare_time_index(self, now: datetime.datetime = MISSING) -> None:
|
||||
# now kwarg should be a datetime.datetime representing the time "now"
|
||||
# to calculate the next time index from
|
||||
|
||||
# pre-condition: self._time is set
|
||||
time_now = (
|
||||
now if now is not MISSING else datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
|
||||
).timetz()
|
||||
for idx, time in enumerate(self._time):
|
||||
if time >= time_now:
|
||||
self._time_index = idx
|
||||
break
|
||||
else:
|
||||
self._time_index = 0
|
||||
|
||||
def _get_time_parameter(
|
||||
self,
|
||||
time: Union[datetime.time, Sequence[datetime.time]],
|
||||
*,
|
||||
dt: Type[datetime.time] = datetime.time,
|
||||
utc: datetime.timezone = datetime.timezone.utc,
|
||||
) -> List[datetime.time]:
|
||||
if isinstance(time, dt):
|
||||
inner = time if time.tzinfo is not None else time.replace(tzinfo=utc)
|
||||
return [inner]
|
||||
if not isinstance(time, Sequence):
|
||||
raise TypeError(
|
||||
f"Expected datetime.time or a sequence of datetime.time for ``time``, received {type(time)!r} instead."
|
||||
)
|
||||
if not time:
|
||||
raise ValueError("time parameter must not be an empty sequence.")
|
||||
|
||||
ret: List[datetime.time] = []
|
||||
for index, t in enumerate(time):
|
||||
if not isinstance(t, dt):
|
||||
raise TypeError(
|
||||
f"Expected a sequence of {dt!r} for ``time``, received {type(t).__name__!r} at index {index} instead."
|
||||
)
|
||||
ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc))
|
||||
|
||||
ret = sorted(set(ret)) # de-dupe and sort times
|
||||
return ret
|
||||
|
||||
def change_interval(
|
||||
self,
|
||||
*,
|
||||
seconds: float = 0,
|
||||
minutes: float = 0,
|
||||
hours: float = 0,
|
||||
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
|
||||
) -> None:
|
||||
"""Changes the interval for the sleep time.
|
||||
|
||||
.. note::
|
||||
|
||||
This only applies on the next loop iteration. If it is desirable for the change of interval
|
||||
to be applied right away, cancel the task with :meth:`cancel`.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
Parameters
|
||||
@@ -450,23 +635,66 @@ class Loop:
|
||||
The number of minutes between every iteration.
|
||||
hours: :class:`float`
|
||||
The number of hours between every iteration.
|
||||
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
|
||||
The exact times to run this loop at. Either a non-empty list or a single
|
||||
value of :class:`datetime.time` should be passed.
|
||||
This cannot be used in conjunction with the relative time parameters.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. note::
|
||||
|
||||
Duplicate times will be ignored, and only run once.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
An invalid value was given.
|
||||
TypeError
|
||||
An invalid value for the ``time`` parameter was passed, or the
|
||||
``time`` parameter was passed in conjunction with relative time parameters.
|
||||
"""
|
||||
|
||||
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
|
||||
if sleep < 0:
|
||||
raise ValueError('Total number of seconds cannot be less than zero.')
|
||||
if time is MISSING:
|
||||
seconds = seconds or 0
|
||||
minutes = minutes or 0
|
||||
hours = hours or 0
|
||||
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
|
||||
if sleep < 0:
|
||||
raise ValueError("Total number of seconds cannot be less than zero.")
|
||||
|
||||
self._sleep = sleep
|
||||
self.seconds = seconds
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self._sleep = sleep
|
||||
self._seconds = float(seconds)
|
||||
self._hours = float(hours)
|
||||
self._minutes = float(minutes)
|
||||
self._time: List[datetime.time] = MISSING
|
||||
else:
|
||||
if any((seconds, minutes, hours)):
|
||||
raise TypeError("Cannot mix explicit time with relative time")
|
||||
self._time = self._get_time_parameter(time)
|
||||
self._sleep = self._seconds = self._minutes = self._hours = MISSING
|
||||
|
||||
def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
|
||||
if self.is_running():
|
||||
if self._time is not MISSING:
|
||||
# prepare the next time index starting from after the last iteration
|
||||
self._prepare_time_index(now=self._last_iteration)
|
||||
|
||||
self._next_iteration = self._get_next_sleep_time()
|
||||
if not self._handle.done():
|
||||
# the loop is sleeping, recalculate based on new interval
|
||||
self._handle.recalculate(self._next_iteration)
|
||||
|
||||
|
||||
def loop(
|
||||
*,
|
||||
seconds: float = MISSING,
|
||||
minutes: float = MISSING,
|
||||
hours: float = MISSING,
|
||||
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
|
||||
count: Optional[int] = None,
|
||||
reconnect: bool = True,
|
||||
loop: asyncio.AbstractEventLoop = MISSING,
|
||||
) -> Callable[[LF], Loop[LF]]:
|
||||
"""A decorator that schedules a task in the background for you with
|
||||
optional reconnect logic. The decorator returns a :class:`Loop`.
|
||||
|
||||
@@ -478,6 +706,19 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
|
||||
The number of minutes between every iteration.
|
||||
hours: :class:`float`
|
||||
The number of hours between every iteration.
|
||||
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
|
||||
The exact times to run this loop at. Either a non-empty list or a single
|
||||
value of :class:`datetime.time` should be passed. Timezones are supported.
|
||||
If no timezone is given for the times, it is assumed to represent UTC time.
|
||||
|
||||
This cannot be used in conjunction with the relative time parameters.
|
||||
|
||||
.. note::
|
||||
|
||||
Duplicate times will be ignored, and only run once.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
count: Optional[:class:`int`]
|
||||
The number of loops to do, ``None`` if it should be an
|
||||
infinite loop.
|
||||
@@ -494,16 +735,20 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
|
||||
ValueError
|
||||
An invalid value was given.
|
||||
TypeError
|
||||
The function was not a coroutine.
|
||||
The function was not a coroutine, an invalid value for the ``time`` parameter was passed,
|
||||
or ``time`` parameter was passed in conjunction with relative time parameters.
|
||||
"""
|
||||
def decorator(func):
|
||||
kwargs = {
|
||||
'seconds': seconds,
|
||||
'minutes': minutes,
|
||||
'hours': hours,
|
||||
'count': count,
|
||||
'reconnect': reconnect,
|
||||
'loop': loop
|
||||
}
|
||||
return Loop(func, **kwargs)
|
||||
|
||||
def decorator(func: LF) -> Loop[LF]:
|
||||
return Loop[LF](
|
||||
func,
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
count=count,
|
||||
time=time,
|
||||
reconnect=reconnect,
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -22,12 +22,14 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
from __future__ import annotations
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
|
||||
import os
|
||||
import io
|
||||
|
||||
__all__ = (
|
||||
'File',
|
||||
)
|
||||
__all__ = ("File",)
|
||||
|
||||
|
||||
class File:
|
||||
r"""A parameter object used for :meth:`abc.Messageable.send`
|
||||
@@ -40,7 +42,7 @@ class File:
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
fp: Union[:class:`str`, :class:`io.BufferedIOBase`]
|
||||
fp: Union[:class:`os.PathLike`, :class:`io.BufferedIOBase`]
|
||||
A file-like object opened in binary mode and read mode
|
||||
or a filename representing a file in the hard drive to
|
||||
open.
|
||||
@@ -60,19 +62,28 @@ class File:
|
||||
Whether the attachment is a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
|
||||
__slots__ = ("fp", "filename", "spoiler", "_original_pos", "_owner", "_closer")
|
||||
|
||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
||||
self.fp = fp
|
||||
if TYPE_CHECKING:
|
||||
fp: io.BufferedIOBase
|
||||
filename: Optional[str]
|
||||
spoiler: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fp: Union[str, bytes, os.PathLike, io.BufferedIOBase],
|
||||
filename: Optional[str] = None,
|
||||
*,
|
||||
spoiler: bool = False,
|
||||
):
|
||||
if isinstance(fp, io.IOBase):
|
||||
if not (fp.seekable() and fp.readable()):
|
||||
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
|
||||
raise ValueError(f"File buffer {fp!r} must be seekable and readable")
|
||||
self.fp = fp
|
||||
self._original_pos = fp.tell()
|
||||
self._owner = False
|
||||
else:
|
||||
self.fp = open(fp, 'rb')
|
||||
self.fp = open(fp, "rb")
|
||||
self._original_pos = 0
|
||||
self._owner = True
|
||||
|
||||
@@ -87,16 +98,16 @@ class File:
|
||||
if isinstance(fp, str):
|
||||
_, self.filename = os.path.split(fp)
|
||||
else:
|
||||
self.filename = getattr(fp, 'name', None)
|
||||
self.filename = getattr(fp, "name", None)
|
||||
else:
|
||||
self.filename = filename
|
||||
|
||||
if spoiler and self.filename is not None and not self.filename.startswith('SPOILER_'):
|
||||
self.filename = 'SPOILER_' + self.filename
|
||||
if spoiler and self.filename is not None and not self.filename.startswith("SPOILER_"):
|
||||
self.filename = "SPOILER_" + self.filename
|
||||
|
||||
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
|
||||
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith("SPOILER_"))
|
||||
|
||||
def reset(self, *, seek=True):
|
||||
def reset(self, *, seek: Union[int, bool] = True) -> None:
|
||||
# The `seek` parameter is needed because
|
||||
# the retry-loop is iterated over multiple times
|
||||
# starting from 0, as an implementation quirk
|
||||
@@ -108,7 +119,7 @@ class File:
|
||||
if seek:
|
||||
self.fp.seek(self._original_pos)
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
self.fp.close = self._closer
|
||||
if self._owner:
|
||||
self._closer()
|
||||
|
||||
228
discord/flags.py
228
discord/flags.py
@@ -29,17 +29,19 @@ from typing import Any, Callable, ClassVar, Dict, Generic, Iterator, List, Optio
|
||||
from .enums import UserFlags
|
||||
|
||||
__all__ = (
|
||||
'SystemChannelFlags',
|
||||
'MessageFlags',
|
||||
'PublicUserFlags',
|
||||
'Intents',
|
||||
'MemberCacheFlags',
|
||||
"SystemChannelFlags",
|
||||
"MessageFlags",
|
||||
"PublicUserFlags",
|
||||
"Intents",
|
||||
"MemberCacheFlags",
|
||||
"ApplicationFlags",
|
||||
)
|
||||
|
||||
FV = TypeVar('FV', bound='flag_value')
|
||||
BF = TypeVar('BF', bound='BaseFlags')
|
||||
FV = TypeVar("FV", bound="flag_value")
|
||||
BF = TypeVar("BF", bound="BaseFlags")
|
||||
|
||||
class flag_value(Generic[BF]):
|
||||
|
||||
class flag_value:
|
||||
def __init__(self, func: Callable[[Any], int]):
|
||||
self.flag = func(None)
|
||||
self.__doc__ = func.__doc__
|
||||
@@ -61,18 +63,22 @@ class flag_value(Generic[BF]):
|
||||
instance._set_flag(self.flag, value)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<flag_value flag={self.flag!r}>'
|
||||
return f"<flag_value flag={self.flag!r}>"
|
||||
|
||||
|
||||
class alias_flag_value(flag_value):
|
||||
pass
|
||||
|
||||
|
||||
def fill_with_flags(*, inverted: bool = False):
|
||||
def decorator(cls: Type[BF]):
|
||||
# fmt: off
|
||||
cls.VALID_FLAGS = {
|
||||
name: value.flag
|
||||
for name, value in cls.__dict__.items()
|
||||
if isinstance(value, flag_value)
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
if inverted:
|
||||
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
|
||||
@@ -81,8 +87,10 @@ def fill_with_flags(*, inverted: bool = False):
|
||||
cls.DEFAULT_VALUE = 0
|
||||
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# n.b. flags must inherit from this and use the decorator above
|
||||
class BaseFlags:
|
||||
VALID_FLAGS: ClassVar[Dict[str, int]]
|
||||
@@ -90,13 +98,13 @@ class BaseFlags:
|
||||
|
||||
value: int
|
||||
|
||||
__slots__ = ('value',)
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, **kwargs: bool):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
raise TypeError(f"{key!r} is not a valid flag name.")
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@@ -115,7 +123,7 @@ class BaseFlags:
|
||||
return hash(self.value)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} value={self.value}>'
|
||||
return f"<{self.__class__.__name__} value={self.value}>"
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, bool]]:
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
@@ -134,7 +142,8 @@ class BaseFlags:
|
||||
elif toggle is False:
|
||||
self.value &= ~o
|
||||
else:
|
||||
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
|
||||
raise TypeError(f"Value to set for {self.__class__.__name__} must be a bool.")
|
||||
|
||||
|
||||
@fill_with_flags(inverted=True)
|
||||
class SystemChannelFlags(BaseFlags):
|
||||
@@ -187,7 +196,7 @@ class SystemChannelFlags(BaseFlags):
|
||||
elif toggle is False:
|
||||
self.value |= o
|
||||
else:
|
||||
raise TypeError('Value to set for SystemChannelFlags must be a bool.')
|
||||
raise TypeError("Value to set for SystemChannelFlags must be a bool.")
|
||||
|
||||
@flag_value
|
||||
def join_notifications(self):
|
||||
@@ -196,9 +205,17 @@ class SystemChannelFlags(BaseFlags):
|
||||
|
||||
@flag_value
|
||||
def premium_subscriptions(self):
|
||||
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
|
||||
""":class:`bool`: Returns ``True`` if the system channel is used for "Nitro boosting" notifications."""
|
||||
return 2
|
||||
|
||||
@flag_value
|
||||
def guild_reminder_notifications(self):
|
||||
""":class:`bool`: Returns ``True`` if the system channel is used for server setup helpful tips notifications.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 4
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class MessageFlags(BaseFlags):
|
||||
@@ -262,6 +279,23 @@ class MessageFlags(BaseFlags):
|
||||
"""
|
||||
return 16
|
||||
|
||||
@flag_value
|
||||
def has_thread(self):
|
||||
""":class:`bool`: Returns ``True`` if the source message is associated with a thread.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 32
|
||||
|
||||
@flag_value
|
||||
def ephemeral(self):
|
||||
""":class:`bool`: Returns ``True`` if the source message is ephemeral.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 64
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class PublicUserFlags(BaseFlags):
|
||||
r"""Wraps up the Discord User Public flags.
|
||||
@@ -368,6 +402,14 @@ class PublicUserFlags(BaseFlags):
|
||||
"""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
@flag_value
|
||||
def discord_certified_moderator(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Discord Certified Moderator.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return UserFlags.discord_certified_moderator.value
|
||||
|
||||
def all(self) -> List[UserFlags]:
|
||||
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
|
||||
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
|
||||
@@ -419,7 +461,7 @@ class Intents(BaseFlags):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
raise TypeError(f"{key!r} is not a valid flag name.")
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@@ -438,16 +480,6 @@ class Intents(BaseFlags):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def default(cls: Type[Intents]) -> Intents:
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||
except :attr:`presences` and :attr:`members`.
|
||||
"""
|
||||
self = cls.all()
|
||||
self.presences = False
|
||||
self.members = False
|
||||
return self
|
||||
|
||||
@flag_value
|
||||
def guilds(self):
|
||||
""":class:`bool`: Whether guild related events are enabled.
|
||||
@@ -482,12 +514,13 @@ class Intents(BaseFlags):
|
||||
|
||||
- :func:`on_member_join`
|
||||
- :func:`on_member_remove`
|
||||
- :func:`on_member_update` (nickname, roles)
|
||||
- :func:`on_member_update`
|
||||
- :func:`on_user_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :meth:`Client.get_all_members`
|
||||
- :meth:`Client.get_user`
|
||||
- :meth:`Guild.chunk`
|
||||
- :meth:`Guild.fetch_members`
|
||||
- :meth:`Guild.get_member`
|
||||
@@ -496,7 +529,7 @@ class Intents(BaseFlags):
|
||||
- :attr:`Member.nick`
|
||||
- :attr:`Member.premium_since`
|
||||
- :attr:`User.name`
|
||||
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :attr:`User.avatar`
|
||||
- :attr:`User.discriminator`
|
||||
|
||||
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
||||
@@ -523,18 +556,34 @@ class Intents(BaseFlags):
|
||||
|
||||
@flag_value
|
||||
def emojis(self):
|
||||
""":class:`bool`: Whether guild emoji related events are enabled.
|
||||
""":class:`bool`: Alias of :attr:`.emojis_and_stickers`.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Changed to an alias.
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@alias_flag_value
|
||||
def emojis_and_stickers(self):
|
||||
""":class:`bool`: Whether guild emoji and sticker related events are enabled.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_emojis_update`
|
||||
- :func:`on_guild_stickers_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Emoji`
|
||||
- :class:`GuildSticker`
|
||||
- :meth:`Client.get_emoji`
|
||||
- :meth:`Client.get_sticker`
|
||||
- :meth:`Client.emojis`
|
||||
- :meth:`Client.stickers`
|
||||
- :attr:`Guild.emojis`
|
||||
- :attr:`Guild.stickers`
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@@ -545,6 +594,9 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_integrations_update`
|
||||
- :func:`on_integration_create`
|
||||
- :func:`on_integration_update`
|
||||
- :func:`on_raw_integration_delete`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
@@ -588,6 +640,10 @@ class Intents(BaseFlags):
|
||||
- :attr:`VoiceChannel.members`
|
||||
- :attr:`VoiceChannel.voice_states`
|
||||
- :attr:`Member.voice`
|
||||
|
||||
.. note::
|
||||
|
||||
This intent is required to connect to voice.
|
||||
"""
|
||||
return 1 << 7
|
||||
|
||||
@@ -597,7 +653,7 @@ class Intents(BaseFlags):
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_member_update` (activities, status)
|
||||
- :func:`on_presence_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
@@ -627,7 +683,6 @@ class Intents(BaseFlags):
|
||||
- :func:`on_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_edit` (both guilds and DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
@@ -682,7 +737,6 @@ class Intents(BaseFlags):
|
||||
- :func:`on_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_edit` (only for DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
@@ -802,6 +856,7 @@ class Intents(BaseFlags):
|
||||
"""
|
||||
return 1 << 14
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class MemberCacheFlags(BaseFlags):
|
||||
"""Controls the library's cache policy when it comes to members.
|
||||
@@ -852,7 +907,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
self.value = (1 << bits) - 1
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError(f'{key!r} is not a valid flag name.')
|
||||
raise TypeError(f"{key!r} is not a valid flag name.")
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
@@ -875,17 +930,6 @@ class MemberCacheFlags(BaseFlags):
|
||||
def _empty(self):
|
||||
return self.value == self.DEFAULT_VALUE
|
||||
|
||||
@flag_value
|
||||
def online(self):
|
||||
""":class:`bool`: Whether to cache members with a status.
|
||||
|
||||
For example, members that are part of the initial ``GUILD_CREATE``
|
||||
or become online at a later point. This requires :attr:`Intents.presences`.
|
||||
|
||||
Members that go offline are no longer cached.
|
||||
"""
|
||||
return 1
|
||||
|
||||
@flag_value
|
||||
def voice(self):
|
||||
""":class:`bool`: Whether to cache members that are in voice.
|
||||
@@ -894,7 +938,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
Members that leave voice are no longer cached.
|
||||
"""
|
||||
return 2
|
||||
return 1
|
||||
|
||||
@flag_value
|
||||
def joined(self):
|
||||
@@ -905,7 +949,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
|
||||
Members that leave the guild are no longer cached.
|
||||
"""
|
||||
return 4
|
||||
return 2
|
||||
|
||||
@classmethod
|
||||
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
|
||||
@@ -926,35 +970,89 @@ class MemberCacheFlags(BaseFlags):
|
||||
self = cls.none()
|
||||
if intents.members:
|
||||
self.joined = True
|
||||
if intents.presences:
|
||||
self.online = True
|
||||
if intents.voice_states:
|
||||
self.voice = True
|
||||
|
||||
if not self.joined and self.online and self.voice:
|
||||
self.voice = False
|
||||
|
||||
return self
|
||||
|
||||
def _verify_intents(self, intents: Intents):
|
||||
if self.online and not intents.presences:
|
||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
||||
|
||||
if self.voice and not intents.voice_states:
|
||||
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
|
||||
raise ValueError("MemberCacheFlags.voice requires Intents.voice_states")
|
||||
|
||||
if self.joined and not intents.members:
|
||||
raise ValueError('MemberCacheFlags.joined requires Intents.members')
|
||||
|
||||
if not self.joined and self.voice and self.online:
|
||||
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
|
||||
'to properly evict members from the cache.'
|
||||
raise ValueError(msg)
|
||||
raise ValueError("MemberCacheFlags.joined requires Intents.members")
|
||||
|
||||
@property
|
||||
def _voice_only(self):
|
||||
return self.value == 2
|
||||
|
||||
@property
|
||||
def _online_only(self):
|
||||
return self.value == 1
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class ApplicationFlags(BaseFlags):
|
||||
r"""Wraps up the Discord Application flags.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two ApplicationFlags are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two ApplicationFlags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||
to be, for example, constructed as a dict or a list of pairs.
|
||||
Note that aliases are not shown.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value: :class:`int`
|
||||
The raw value. You should query flags via the properties
|
||||
rather than using this raw value.
|
||||
"""
|
||||
|
||||
@flag_value
|
||||
def gateway_presence(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
|
||||
receive presence information over the gateway.
|
||||
"""
|
||||
return 1 << 12
|
||||
|
||||
@flag_value
|
||||
def gateway_presence_limited(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
|
||||
presence information over the gateway.
|
||||
"""
|
||||
return 1 << 13
|
||||
|
||||
@flag_value
|
||||
def gateway_guild_members(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
|
||||
receive guild members information over the gateway.
|
||||
"""
|
||||
return 1 << 14
|
||||
|
||||
@flag_value
|
||||
def gateway_guild_members_limited(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
|
||||
guild members information over the gateway.
|
||||
"""
|
||||
return 1 << 15
|
||||
|
||||
@flag_value
|
||||
def verification_pending_guild_limit(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is currently pending verification
|
||||
and has hit the guild limit.
|
||||
"""
|
||||
return 1 << 16
|
||||
|
||||
@flag_value
|
||||
def embedded(self):
|
||||
""":class:`bool`: Returns ``True`` if the application is embedded within the Discord client."""
|
||||
return 1 << 17
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1835
discord/guild.py
1835
discord/guild.py
File diff suppressed because it is too large
Load Diff
1805
discord/http.py
1805
discord/http.py
File diff suppressed because it is too large
Load Diff
@@ -22,17 +22,36 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from .utils import _get_as_snowflake, get, parse_time
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING, overload, Type, Tuple
|
||||
from .utils import _get_as_snowflake, parse_time, MISSING
|
||||
from .user import User
|
||||
from .errors import InvalidArgument
|
||||
from .enums import try_enum, ExpireBehaviour
|
||||
|
||||
__all__ = (
|
||||
'IntegrationAccount',
|
||||
'Integration',
|
||||
"IntegrationAccount",
|
||||
"IntegrationApplication",
|
||||
"Integration",
|
||||
"StreamIntegration",
|
||||
"BotIntegration",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.integration import (
|
||||
IntegrationAccount as IntegrationAccountPayload,
|
||||
Integration as IntegrationPayload,
|
||||
StreamIntegration as StreamIntegrationPayload,
|
||||
BotIntegration as BotIntegrationPayload,
|
||||
IntegrationType,
|
||||
IntegrationApplication as IntegrationApplicationPayload,
|
||||
)
|
||||
from .guild import Guild
|
||||
from .role import Role
|
||||
|
||||
|
||||
class IntegrationAccount:
|
||||
"""Represents an integration account.
|
||||
|
||||
@@ -40,20 +59,21 @@ class IntegrationAccount:
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
id: :class:`str`
|
||||
The account ID.
|
||||
name: :class:`str`
|
||||
The account name.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name')
|
||||
__slots__ = ("id", "name")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
def __init__(self, data: IntegrationAccountPayload) -> None:
|
||||
self.id: str = data["id"]
|
||||
self.name: str = data["name"]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<IntegrationAccount id={self.id} name={self.name!r}>"
|
||||
|
||||
def __repr__(self):
|
||||
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)
|
||||
|
||||
class Integration:
|
||||
"""Represents a guild integration.
|
||||
@@ -62,6 +82,83 @@ class Integration:
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The integration ID.
|
||||
name: :class:`str`
|
||||
The integration name.
|
||||
guild: :class:`Guild`
|
||||
The guild of the integration.
|
||||
type: :class:`str`
|
||||
The integration type (i.e. Twitch).
|
||||
enabled: :class:`bool`
|
||||
Whether the integration is currently enabled.
|
||||
account: :class:`IntegrationAccount`
|
||||
The account linked to this integration.
|
||||
user: :class:`User`
|
||||
The user that added this integration.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"guild",
|
||||
"id",
|
||||
"_state",
|
||||
"type",
|
||||
"name",
|
||||
"account",
|
||||
"user",
|
||||
"enabled",
|
||||
)
|
||||
|
||||
def __init__(self, *, data: IntegrationPayload, guild: Guild) -> None:
|
||||
self.guild = guild
|
||||
self._state = guild._state
|
||||
self._from_data(data)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>"
|
||||
|
||||
def _from_data(self, data: IntegrationPayload) -> None:
|
||||
self.id: int = int(data["id"])
|
||||
self.type: IntegrationType = data["type"]
|
||||
self.name: str = data["name"]
|
||||
self.account: IntegrationAccount = IntegrationAccount(data["account"])
|
||||
|
||||
user = data.get("user")
|
||||
self.user = User(state=self._state, data=user) if user else None
|
||||
self.enabled: bool = data["enabled"]
|
||||
|
||||
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the integration.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: :class:`str`
|
||||
The reason the integration was deleted. Shows up on the audit log.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to delete the integration.
|
||||
HTTPException
|
||||
Deleting the integration failed.
|
||||
"""
|
||||
await self._state.http.delete_integration(self.guild.id, self.id, reason=reason)
|
||||
|
||||
|
||||
class StreamIntegration(Integration):
|
||||
"""Represents a stream integration for Twitch or YouTube.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The integration ID.
|
||||
name: :class:`str`
|
||||
@@ -74,8 +171,6 @@ class Integration:
|
||||
Whether the integration is currently enabled.
|
||||
syncing: :class:`bool`
|
||||
Where the integration is currently syncing.
|
||||
role: :class:`Role`
|
||||
The role which the integration uses for subscribers.
|
||||
enable_emoticons: Optional[:class:`bool`]
|
||||
Whether emoticons should be synced for this integration (currently twitch only).
|
||||
expire_behaviour: :class:`ExpireBehaviour`
|
||||
@@ -90,37 +185,45 @@ class Integration:
|
||||
An aware UTC datetime representing when the integration was last synced.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
|
||||
'syncing', 'role', 'expire_behaviour', 'expire_behavior',
|
||||
'expire_grace_period', 'synced_at', 'user', 'account',
|
||||
'enable_emoticons', '_role_id')
|
||||
__slots__ = (
|
||||
"revoked",
|
||||
"expire_behaviour",
|
||||
"expire_grace_period",
|
||||
"synced_at",
|
||||
"_role_id",
|
||||
"syncing",
|
||||
"enable_emoticons",
|
||||
"subscriber_count",
|
||||
)
|
||||
|
||||
def __init__(self, *, data, guild):
|
||||
self.guild = guild
|
||||
self._state = guild._state
|
||||
self._from_data(data)
|
||||
def _from_data(self, data: StreamIntegrationPayload) -> None:
|
||||
super()._from_data(data)
|
||||
self.revoked: bool = data["revoked"]
|
||||
self.expire_behaviour: ExpireBehaviour = try_enum(ExpireBehaviour, data["expire_behavior"])
|
||||
self.expire_grace_period: int = data["expire_grace_period"]
|
||||
self.synced_at: datetime.datetime = parse_time(data["synced_at"])
|
||||
self._role_id: Optional[int] = _get_as_snowflake(data, "role_id")
|
||||
self.syncing: bool = data["syncing"]
|
||||
self.enable_emoticons: bool = data["enable_emoticons"]
|
||||
self.subscriber_count: int = data["subscriber_count"]
|
||||
|
||||
def __repr__(self):
|
||||
return '<Integration id={0.id} name={0.name!r} type={0.type!r}>'.format(self)
|
||||
@property
|
||||
def expire_behavior(self) -> ExpireBehaviour:
|
||||
""":class:`ExpireBehaviour`: An alias for :attr:`expire_behaviour`."""
|
||||
return self.expire_behaviour
|
||||
|
||||
def _from_data(self, integ):
|
||||
self.id = _get_as_snowflake(integ, 'id')
|
||||
self.name = integ['name']
|
||||
self.type = integ['type']
|
||||
self.enabled = integ['enabled']
|
||||
self.syncing = integ['syncing']
|
||||
self._role_id = _get_as_snowflake(integ, 'role_id')
|
||||
self.role = get(self.guild.roles, id=self._role_id)
|
||||
self.enable_emoticons = integ.get('enable_emoticons')
|
||||
self.expire_behaviour = try_enum(ExpireBehaviour, integ['expire_behavior'])
|
||||
self.expire_behavior = self.expire_behaviour
|
||||
self.expire_grace_period = integ['expire_grace_period']
|
||||
self.synced_at = parse_time(integ['synced_at'])
|
||||
@property
|
||||
def role(self) -> Optional[Role]:
|
||||
"""Optional[:class:`Role`] The role which the integration uses for subscribers."""
|
||||
return self.guild.get_role(self._role_id) # type: ignore
|
||||
|
||||
self.user = User(state=self._state, data=integ['user'])
|
||||
self.account = IntegrationAccount(**integ['account'])
|
||||
|
||||
async def edit(self, **fields):
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
expire_behaviour: ExpireBehaviour = MISSING,
|
||||
expire_grace_period: int = MISSING,
|
||||
enable_emoticons: bool = MISSING,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Edits the integration.
|
||||
@@ -146,34 +249,24 @@ class Integration:
|
||||
InvalidArgument
|
||||
``expire_behaviour`` did not receive a :class:`ExpireBehaviour`.
|
||||
"""
|
||||
try:
|
||||
expire_behaviour = fields['expire_behaviour']
|
||||
except KeyError:
|
||||
expire_behaviour = fields.get('expire_behavior', self.expire_behaviour)
|
||||
payload: Dict[str, Any] = {}
|
||||
if expire_behaviour is not MISSING:
|
||||
if not isinstance(expire_behaviour, ExpireBehaviour):
|
||||
raise InvalidArgument("expire_behaviour field must be of type ExpireBehaviour")
|
||||
|
||||
if not isinstance(expire_behaviour, ExpireBehaviour):
|
||||
raise InvalidArgument('expire_behaviour field must be of type ExpireBehaviour')
|
||||
payload["expire_behavior"] = expire_behaviour.value
|
||||
|
||||
expire_grace_period = fields.get('expire_grace_period', self.expire_grace_period)
|
||||
if expire_grace_period is not MISSING:
|
||||
payload["expire_grace_period"] = expire_grace_period
|
||||
|
||||
payload = {
|
||||
'expire_behavior': expire_behaviour.value,
|
||||
'expire_grace_period': expire_grace_period,
|
||||
}
|
||||
|
||||
enable_emoticons = fields.get('enable_emoticons')
|
||||
|
||||
if enable_emoticons is not None:
|
||||
payload['enable_emoticons'] = enable_emoticons
|
||||
if enable_emoticons is not MISSING:
|
||||
payload["enable_emoticons"] = enable_emoticons
|
||||
|
||||
# This endpoint is undocumented.
|
||||
# Unsure if it returns the data or not as a result
|
||||
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
|
||||
|
||||
self.expire_behaviour = expire_behaviour
|
||||
self.expire_behavior = self.expire_behaviour
|
||||
self.expire_grace_period = expire_grace_period
|
||||
self.enable_emoticons = enable_emoticons
|
||||
|
||||
async def sync(self):
|
||||
async def sync(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Syncs the integration.
|
||||
@@ -191,19 +284,83 @@ class Integration:
|
||||
await self._state.http.sync_integration(self.guild.id, self.id)
|
||||
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the integration.
|
||||
class IntegrationApplication:
|
||||
"""Represents an application for a bot integration.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_guild` permission to
|
||||
do this.
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permission to delete the integration.
|
||||
HTTPException
|
||||
Deleting the integration failed.
|
||||
"""
|
||||
await self._state.http.delete_integration(self.guild.id, self.id)
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID for this application.
|
||||
name: :class:`str`
|
||||
The application's name.
|
||||
icon: Optional[:class:`str`]
|
||||
The application's icon hash.
|
||||
description: :class:`str`
|
||||
The application's description. Can be an empty string.
|
||||
summary: :class:`str`
|
||||
The summary of the application. Can be an empty string.
|
||||
user: Optional[:class:`User`]
|
||||
The bot user on this application.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"id",
|
||||
"name",
|
||||
"icon",
|
||||
"description",
|
||||
"summary",
|
||||
"user",
|
||||
)
|
||||
|
||||
def __init__(self, *, data: IntegrationApplicationPayload, state):
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self.icon: Optional[str] = data["icon"]
|
||||
self.description: str = data["description"]
|
||||
self.summary: str = data["summary"]
|
||||
user = data.get("bot")
|
||||
self.user: Optional[User] = User(state=state, data=user) if user else None
|
||||
|
||||
|
||||
class BotIntegration(Integration):
|
||||
"""Represents a bot integration on discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The integration ID.
|
||||
name: :class:`str`
|
||||
The integration name.
|
||||
guild: :class:`Guild`
|
||||
The guild of the integration.
|
||||
type: :class:`str`
|
||||
The integration type (i.e. Twitch).
|
||||
enabled: :class:`bool`
|
||||
Whether the integration is currently enabled.
|
||||
user: :class:`User`
|
||||
The user that added this integration.
|
||||
account: :class:`IntegrationAccount`
|
||||
The integration account information.
|
||||
application: :class:`IntegrationApplication`
|
||||
The application tied to this integration.
|
||||
"""
|
||||
|
||||
__slots__ = ("application",)
|
||||
|
||||
def _from_data(self, data: BotIntegrationPayload) -> None:
|
||||
super()._from_data(data)
|
||||
self.application = IntegrationApplication(data=data["application"], state=self._state)
|
||||
|
||||
|
||||
def _integration_factory(value: str) -> Tuple[Type[Integration], str]:
|
||||
if value == "discord":
|
||||
return BotIntegration, value
|
||||
elif value in ("twitch", "youtube"):
|
||||
return StreamIntegration, value
|
||||
else:
|
||||
return Integration, value
|
||||
|
||||
@@ -25,20 +25,54 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
|
||||
import asyncio
|
||||
|
||||
from . import utils
|
||||
from .enums import try_enum, InteractionType
|
||||
from .enums import try_enum, InteractionType, InteractionResponseType
|
||||
from .errors import InteractionResponded, HTTPException, ClientException
|
||||
from .channel import PartialMessageable, ChannelType
|
||||
|
||||
from .user import User
|
||||
from .member import Member
|
||||
from .message import Message, Attachment
|
||||
from .object import Object
|
||||
from .permissions import Permissions
|
||||
from .webhook.async_ import async_context, Webhook, handle_message_parameters
|
||||
|
||||
__all__ = (
|
||||
'Interaction',
|
||||
"Interaction",
|
||||
"InteractionMessage",
|
||||
"InteractionResponse",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from .types.interactions import (
|
||||
Interaction as InteractionPayload,
|
||||
InteractionData,
|
||||
)
|
||||
from .guild import Guild
|
||||
from .state import ConnectionState
|
||||
from .file import File
|
||||
from .mentions import AllowedMentions
|
||||
from aiohttp import ClientSession
|
||||
from .embeds import Embed
|
||||
from .ui.view import View
|
||||
from .channel import TextChannel, CategoryChannel, StoreChannel, PartialMessageable
|
||||
from .threads import Thread
|
||||
|
||||
InteractionChannel = Union[TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable]
|
||||
|
||||
MISSING: Any = utils.MISSING
|
||||
|
||||
|
||||
class Interaction:
|
||||
"""Represents a Discord interaction.
|
||||
|
||||
An interaction happens when a user does an action that needs to
|
||||
be notified. Current examples are slash commands but future examples
|
||||
include forms and buttons.
|
||||
be notified. Current examples are slash commands and components.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
@@ -56,49 +90,679 @@ class Interaction:
|
||||
The application ID that the interaction was for.
|
||||
user: Optional[Union[:class:`User`, :class:`Member`]]
|
||||
The user or member that sent the interaction.
|
||||
message: Optional[:class:`Message`]
|
||||
The message that sent this interaction.
|
||||
token: :class:`str`
|
||||
The token to continue the interaction. These are valid
|
||||
for 15 minutes.
|
||||
data: :class:`dict`
|
||||
The raw interaction data.
|
||||
"""
|
||||
__slots__ = (
|
||||
'id',
|
||||
'type',
|
||||
'guild_id',
|
||||
'channel_id',
|
||||
'data',
|
||||
'application_id',
|
||||
'user',
|
||||
'token',
|
||||
'version',
|
||||
'_state',
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"id",
|
||||
"type",
|
||||
"guild_id",
|
||||
"channel_id",
|
||||
"data",
|
||||
"application_id",
|
||||
"message",
|
||||
"user",
|
||||
"token",
|
||||
"version",
|
||||
"_permissions",
|
||||
"_state",
|
||||
"_session",
|
||||
"_original_message",
|
||||
"_cs_response",
|
||||
"_cs_followup",
|
||||
"_cs_channel",
|
||||
)
|
||||
|
||||
def __init__(self, *, data, state=None):
|
||||
self._state = state
|
||||
def __init__(self, *, data: InteractionPayload, state: ConnectionState):
|
||||
self._state: ConnectionState = state
|
||||
self._session: ClientSession = state.http._HTTPClient__session
|
||||
self._original_message: Optional[InteractionMessage] = None
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.id = int(data['id'])
|
||||
self.type = try_enum(InteractionType, data['type'])
|
||||
self.data = data.get('data')
|
||||
self.token = data['token']
|
||||
self.version = data['version']
|
||||
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
|
||||
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
|
||||
self.application_id = utils._get_as_snowflake(data, 'application_id')
|
||||
def _from_data(self, data: InteractionPayload):
|
||||
self.id: int = int(data["id"])
|
||||
self.type: InteractionType = try_enum(InteractionType, data["type"])
|
||||
self.data: Optional[InteractionData] = data.get("data")
|
||||
self.token: str = data["token"]
|
||||
self.version: int = data["version"]
|
||||
self.channel_id: Optional[int] = utils._get_as_snowflake(data, "channel_id")
|
||||
self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id")
|
||||
self.application_id: int = int(data["application_id"])
|
||||
|
||||
self.message: Optional[Message]
|
||||
try:
|
||||
self.message = Message(state=self._state, channel=self.channel, data=data["message"]) # type: ignore
|
||||
except KeyError:
|
||||
self.message = None
|
||||
|
||||
self.user: Optional[Union[User, Member]] = None
|
||||
self._permissions: int = 0
|
||||
|
||||
# TODO: there's a potential data loss here
|
||||
if self.guild_id:
|
||||
guild = self.guild or Object(id=self.guild_id)
|
||||
try:
|
||||
member = data["member"] # type: ignore
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.user = Member(state=self._state, guild=guild, data=member) # type: ignore
|
||||
self._permissions = int(member.get("permissions", 0))
|
||||
else:
|
||||
try:
|
||||
self.user = User(state=self._state, data=data["user"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
def guild(self) -> Optional[Guild]:
|
||||
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
|
||||
return self._state and self._state.get_guild(self.guild_id)
|
||||
return self._state and self._state._get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
|
||||
@utils.cached_slot_property("_cs_channel")
|
||||
def channel(self) -> Optional[InteractionChannel]:
|
||||
"""Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]: The channel the interaction was sent from.
|
||||
|
||||
Note that due to a Discord limitation, DM channels are not resolved since there is
|
||||
no data to complete them.
|
||||
no data to complete them. These are :class:`PartialMessageable` instead.
|
||||
"""
|
||||
guild = self.guild
|
||||
return guild and guild.get_channel(self.channel_id)
|
||||
channel = guild and guild._resolve_channel(self.channel_id)
|
||||
if channel is None:
|
||||
if self.channel_id is not None:
|
||||
type = ChannelType.text if self.guild_id is not None else ChannelType.private
|
||||
return PartialMessageable(state=self._state, id=self.channel_id, type=type)
|
||||
return None
|
||||
return channel # type: ignore
|
||||
|
||||
@property
|
||||
def permissions(self) -> Permissions:
|
||||
""":class:`Permissions`: The resolved permissions of the member in the channel, including overwrites.
|
||||
|
||||
In a non-guild context where this doesn't apply, an empty permissions object is returned.
|
||||
"""
|
||||
return Permissions(self._permissions)
|
||||
|
||||
@utils.cached_slot_property("_cs_response")
|
||||
def response(self) -> InteractionResponse:
|
||||
""":class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction.
|
||||
|
||||
A response can only be done once. If secondary messages need to be sent, consider using :attr:`followup`
|
||||
instead.
|
||||
"""
|
||||
return InteractionResponse(self)
|
||||
|
||||
@utils.cached_slot_property("_cs_followup")
|
||||
def followup(self) -> Webhook:
|
||||
""":class:`Webhook`: Returns the follow up webhook for follow up interactions."""
|
||||
payload = {
|
||||
"id": self.application_id,
|
||||
"type": 3,
|
||||
"token": self.token,
|
||||
}
|
||||
return Webhook.from_state(data=payload, state=self._state)
|
||||
|
||||
async def original_message(self) -> InteractionMessage:
|
||||
"""|coro|
|
||||
|
||||
Fetches the original interaction response message associated with the interaction.
|
||||
|
||||
If the interaction response was :meth:`InteractionResponse.send_message` then this would
|
||||
return the message that was sent using that response. Otherwise, this would return
|
||||
the message that triggered the interaction.
|
||||
|
||||
Repeated calls to this will return a cached value.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Fetching the original response message failed.
|
||||
ClientException
|
||||
The channel for the message could not be resolved.
|
||||
|
||||
Returns
|
||||
--------
|
||||
InteractionMessage
|
||||
The original interaction response message.
|
||||
"""
|
||||
|
||||
if self._original_message is not None:
|
||||
return self._original_message
|
||||
|
||||
# TODO: fix later to not raise?
|
||||
channel = self.channel
|
||||
if channel is None:
|
||||
raise ClientException("Channel for message could not be resolved")
|
||||
|
||||
adapter = async_context.get()
|
||||
data = await adapter.get_original_interaction_response(
|
||||
application_id=self.application_id,
|
||||
token=self.token,
|
||||
session=self._session,
|
||||
)
|
||||
state = _InteractionMessageState(self, self._state)
|
||||
message = InteractionMessage(state=state, channel=channel, data=data) # type: ignore
|
||||
self._original_message = message
|
||||
return message
|
||||
|
||||
async def edit_original_message(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = MISSING,
|
||||
embeds: List[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
file: File = MISSING,
|
||||
files: List[File] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
) -> InteractionMessage:
|
||||
"""|coro|
|
||||
|
||||
Edits the original interaction response message.
|
||||
|
||||
This is a lower level interface to :meth:`InteractionMessage.edit` in case
|
||||
you do not want to fetch the message and save an HTTP request.
|
||||
|
||||
This method is also the only way to edit the original message if
|
||||
the message sent was ephemeral.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
content: Optional[:class:`str`]
|
||||
The content to edit the message with or ``None`` to clear it.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds to edit the message with.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The embed to edit the message with. ``None`` suppresses the embeds.
|
||||
This should not be mixed with the ``embeds`` parameter.
|
||||
file: :class:`File`
|
||||
The file to upload. This cannot be mixed with ``files`` parameter.
|
||||
files: List[:class:`File`]
|
||||
A list of files to send with the content. This cannot be mixed with the
|
||||
``file`` parameter.
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
Forbidden
|
||||
Edited a message that is not yours.
|
||||
TypeError
|
||||
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
|
||||
ValueError
|
||||
The length of ``embeds`` was invalid.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`InteractionMessage`
|
||||
The newly edited message.
|
||||
"""
|
||||
|
||||
previous_mentions: Optional[AllowedMentions] = self._state.allowed_mentions
|
||||
params = handle_message_parameters(
|
||||
content=content,
|
||||
file=file,
|
||||
files=files,
|
||||
embed=embed,
|
||||
embeds=embeds,
|
||||
view=view,
|
||||
allowed_mentions=allowed_mentions,
|
||||
previous_allowed_mentions=previous_mentions,
|
||||
)
|
||||
adapter = async_context.get()
|
||||
data = await adapter.edit_original_interaction_response(
|
||||
self.application_id,
|
||||
self.token,
|
||||
session=self._session,
|
||||
payload=params.payload,
|
||||
multipart=params.multipart,
|
||||
files=params.files,
|
||||
)
|
||||
|
||||
# The message channel types should always match
|
||||
message = InteractionMessage(state=self._state, channel=self.channel, data=data) # type: ignore
|
||||
if view and not view.is_finished():
|
||||
self._state.store_view(view, message.id)
|
||||
return message
|
||||
|
||||
async def delete_original_message(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the original interaction response message.
|
||||
|
||||
This is a lower level interface to :meth:`InteractionMessage.delete` in case
|
||||
you do not want to fetch the message and save an HTTP request.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Deleting the message failed.
|
||||
Forbidden
|
||||
Deleted a message that is not yours.
|
||||
"""
|
||||
adapter = async_context.get()
|
||||
await adapter.delete_original_interaction_response(
|
||||
self.application_id,
|
||||
self.token,
|
||||
session=self._session,
|
||||
)
|
||||
|
||||
|
||||
class InteractionResponse:
|
||||
"""Represents a Discord interaction response.
|
||||
|
||||
This type can be accessed through :attr:`Interaction.response`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
"responded_at",
|
||||
"_parent",
|
||||
)
|
||||
|
||||
def __init__(self, parent: Interaction):
|
||||
self.responded_at: Optional[datetime] = None
|
||||
self._parent: Interaction = parent
|
||||
|
||||
def is_done(self) -> bool:
|
||||
""":class:`bool`: Indicates whether an interaction response has been done before.
|
||||
|
||||
An interaction can only be responded to once.
|
||||
"""
|
||||
return self.responded_at is not None
|
||||
|
||||
async def defer(self, *, ephemeral: bool = False) -> None:
|
||||
"""|coro|
|
||||
|
||||
Defers the interaction response.
|
||||
|
||||
This is typically used when the interaction is acknowledged
|
||||
and a secondary action will be done later.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ephemeral: :class:`bool`
|
||||
Indicates whether the deferred message will eventually be ephemeral.
|
||||
This only applies for interactions of type :attr:`InteractionType.application_command`.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Deferring the interaction failed.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
if self.is_done():
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
defer_type: int = 0
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
parent = self._parent
|
||||
if parent.type is InteractionType.component:
|
||||
defer_type = InteractionResponseType.deferred_message_update.value
|
||||
elif parent.type is InteractionType.application_command:
|
||||
defer_type = InteractionResponseType.deferred_channel_message.value
|
||||
if ephemeral:
|
||||
data = {"flags": 64}
|
||||
|
||||
if defer_type:
|
||||
adapter = async_context.get()
|
||||
await adapter.create_interaction_response(
|
||||
parent.id, parent.token, session=parent._session, type=defer_type, data=data
|
||||
)
|
||||
|
||||
self.responded_at = utils.utcnow()
|
||||
|
||||
async def pong(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Pongs the ping interaction.
|
||||
|
||||
This should rarely be used.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Ponging the interaction failed.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
if self.is_done():
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
parent = self._parent
|
||||
if parent.type is InteractionType.ping:
|
||||
adapter = async_context.get()
|
||||
await adapter.create_interaction_response(
|
||||
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
|
||||
)
|
||||
self.responded_at = utils.utcnow()
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
content: Optional[Any] = None,
|
||||
*,
|
||||
embed: Embed = MISSING,
|
||||
embeds: List[Embed] = MISSING,
|
||||
view: View = MISSING,
|
||||
tts: bool = False,
|
||||
ephemeral: bool = False,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Responds to this interaction by sending a message.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: Optional[:class:`str`]
|
||||
The content of the message to send.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds to send with the content. Maximum of 10. This cannot
|
||||
be mixed with the ``embed`` parameter.
|
||||
embed: :class:`Embed`
|
||||
The rich embed for the content to send. This cannot be mixed with
|
||||
``embeds`` parameter.
|
||||
tts: :class:`bool`
|
||||
Indicates if the message should be sent using text-to-speech.
|
||||
view: :class:`discord.ui.View`
|
||||
The view to send with the message.
|
||||
ephemeral: :class:`bool`
|
||||
Indicates if the message should only be visible to the user who started the interaction.
|
||||
If a view is sent with an ephemeral message and it has no timeout set then the timeout
|
||||
is set to 15 minutes.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Sending the message failed.
|
||||
TypeError
|
||||
You specified both ``embed`` and ``embeds``.
|
||||
ValueError
|
||||
The length of ``embeds`` was invalid.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
if self.is_done():
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"tts": tts,
|
||||
}
|
||||
|
||||
if embed is not MISSING and embeds is not MISSING:
|
||||
raise TypeError("cannot mix embed and embeds keyword arguments")
|
||||
|
||||
if embed is not MISSING:
|
||||
embeds = [embed]
|
||||
|
||||
if embeds:
|
||||
if len(embeds) > 10:
|
||||
raise ValueError("embeds cannot exceed maximum of 10 elements")
|
||||
payload["embeds"] = [e.to_dict() for e in embeds]
|
||||
|
||||
if content is not None:
|
||||
payload["content"] = str(content)
|
||||
|
||||
if ephemeral:
|
||||
payload["flags"] = 64
|
||||
|
||||
if view is not MISSING:
|
||||
payload["components"] = view.to_components()
|
||||
|
||||
parent = self._parent
|
||||
adapter = async_context.get()
|
||||
await adapter.create_interaction_response(
|
||||
parent.id,
|
||||
parent.token,
|
||||
session=parent._session,
|
||||
type=InteractionResponseType.channel_message.value,
|
||||
data=payload,
|
||||
)
|
||||
|
||||
if view is not MISSING:
|
||||
if ephemeral and view.timeout is None:
|
||||
view.timeout = 15 * 60.0
|
||||
|
||||
self._parent._state.store_view(view)
|
||||
|
||||
self.responded_at = utils.utcnow()
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
*,
|
||||
content: Optional[Any] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
embeds: List[Embed] = MISSING,
|
||||
attachments: List[Attachment] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Responds to this interaction by editing the original message of
|
||||
a component interaction.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: Optional[:class:`str`]
|
||||
The new content to replace the message with. ``None`` removes the content.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds to edit the message with.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The embed to edit the message with. ``None`` suppresses the embeds.
|
||||
This should not be mixed with the ``embeds`` parameter.
|
||||
attachments: List[:class:`Attachment`]
|
||||
A list of attachments to keep in the message. If ``[]`` is passed
|
||||
then all attachments are removed.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
TypeError
|
||||
You specified both ``embed`` and ``embeds``.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
if self.is_done():
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
parent = self._parent
|
||||
msg = parent.message
|
||||
state = parent._state
|
||||
message_id = msg.id if msg else None
|
||||
if parent.type is not InteractionType.component:
|
||||
return
|
||||
|
||||
payload = {}
|
||||
if content is not MISSING:
|
||||
if content is None:
|
||||
payload["content"] = None
|
||||
else:
|
||||
payload["content"] = str(content)
|
||||
|
||||
if embed is not MISSING and embeds is not MISSING:
|
||||
raise TypeError("cannot mix both embed and embeds keyword arguments")
|
||||
|
||||
if embed is not MISSING:
|
||||
if embed is None:
|
||||
embeds = []
|
||||
else:
|
||||
embeds = [embed]
|
||||
|
||||
if embeds is not MISSING:
|
||||
payload["embeds"] = [e.to_dict() for e in embeds]
|
||||
|
||||
if attachments is not MISSING:
|
||||
payload["attachments"] = [a.to_dict() for a in attachments]
|
||||
|
||||
if view is not MISSING:
|
||||
state.prevent_view_updates_for(message_id)
|
||||
if view is None:
|
||||
payload["components"] = []
|
||||
else:
|
||||
payload["components"] = view.to_components()
|
||||
|
||||
adapter = async_context.get()
|
||||
await adapter.create_interaction_response(
|
||||
parent.id,
|
||||
parent.token,
|
||||
session=parent._session,
|
||||
type=InteractionResponseType.message_update.value,
|
||||
data=payload,
|
||||
)
|
||||
|
||||
if view and not view.is_finished():
|
||||
state.store_view(view, message_id)
|
||||
|
||||
self.responded_at = utils.utcnow()
|
||||
|
||||
|
||||
class _InteractionMessageState:
|
||||
__slots__ = ("_parent", "_interaction")
|
||||
|
||||
def __init__(self, interaction: Interaction, parent: ConnectionState):
|
||||
self._interaction: Interaction = interaction
|
||||
self._parent: ConnectionState = parent
|
||||
|
||||
def _get_guild(self, guild_id):
|
||||
return self._parent._get_guild(guild_id)
|
||||
|
||||
def store_user(self, data):
|
||||
return self._parent.store_user(data)
|
||||
|
||||
def create_user(self, data):
|
||||
return self._parent.create_user(data)
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
return self._parent.http
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._parent, attr)
|
||||
|
||||
|
||||
class InteractionMessage(Message):
|
||||
"""Represents the original interaction response message.
|
||||
|
||||
This allows you to edit or delete the message associated with
|
||||
the interaction response. To retrieve this object see :meth:`Interaction.original_message`.
|
||||
|
||||
This inherits from :class:`discord.Message` with changes to
|
||||
:meth:`edit` and :meth:`delete` to work.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
_state: _InteractionMessageState
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
content: Optional[str] = MISSING,
|
||||
embeds: List[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
file: File = MISSING,
|
||||
files: List[File] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
) -> InteractionMessage:
|
||||
"""|coro|
|
||||
|
||||
Edits the message.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
content: Optional[:class:`str`]
|
||||
The content to edit the message with or ``None`` to clear it.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds to edit the message with.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The embed to edit the message with. ``None`` suppresses the embeds.
|
||||
This should not be mixed with the ``embeds`` parameter.
|
||||
file: :class:`File`
|
||||
The file to upload. This cannot be mixed with ``files`` parameter.
|
||||
files: List[:class:`File`]
|
||||
A list of files to send with the content. This cannot be mixed with the
|
||||
``file`` parameter.
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
Forbidden
|
||||
Edited a message that is not yours.
|
||||
TypeError
|
||||
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
|
||||
ValueError
|
||||
The length of ``embeds`` was invalid.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`InteractionMessage`
|
||||
The newly edited message.
|
||||
"""
|
||||
return await self._state._interaction.edit_original_message(
|
||||
content=content,
|
||||
embeds=embeds,
|
||||
embed=embed,
|
||||
file=file,
|
||||
files=files,
|
||||
view=view,
|
||||
allowed_mentions=allowed_mentions,
|
||||
)
|
||||
|
||||
async def delete(self, *, delay: Optional[float] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the message.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
delay: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait before deleting the message.
|
||||
The waiting is done in the background and deletion failures are ignored.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have proper permissions to delete the message.
|
||||
NotFound
|
||||
The message was deleted already.
|
||||
HTTPException
|
||||
Deleting the message failed.
|
||||
"""
|
||||
|
||||
if delay is not None:
|
||||
|
||||
async def inner_call(delay: float = delay):
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
await self._state._interaction.delete_original_message()
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.create_task(inner_call())
|
||||
else:
|
||||
await self._state._interaction.delete_original_message()
|
||||
|
||||
@@ -22,18 +22,42 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Type, TypeVar, Union, TYPE_CHECKING
|
||||
from .asset import Asset
|
||||
from .utils import parse_time, snowflake_time, _get_as_snowflake
|
||||
from .object import Object
|
||||
from .mixins import Hashable
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
from .enums import ChannelType, VerificationLevel, InviteTarget, try_enum
|
||||
from .appinfo import PartialAppInfo
|
||||
|
||||
__all__ = (
|
||||
'PartialInviteChannel',
|
||||
'PartialInviteGuild',
|
||||
'Invite',
|
||||
"PartialInviteChannel",
|
||||
"PartialInviteGuild",
|
||||
"Invite",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.invite import (
|
||||
Invite as InvitePayload,
|
||||
InviteGuild as InviteGuildPayload,
|
||||
GatewayInvite as GatewayInvitePayload,
|
||||
)
|
||||
from .types.channel import (
|
||||
PartialChannel as InviteChannelPayload,
|
||||
)
|
||||
from .state import ConnectionState
|
||||
from .guild import Guild
|
||||
from .abc import GuildChannel
|
||||
from .user import User
|
||||
|
||||
InviteGuildType = Union[Guild, "PartialInviteGuild", Object]
|
||||
InviteChannelType = Union[GuildChannel, "PartialInviteChannel", Object]
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
class PartialInviteChannel:
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
@@ -68,29 +92,30 @@ class PartialInviteChannel:
|
||||
The partial channel's type.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name', 'type')
|
||||
__slots__ = ("id", "name", "type")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
self.type = kwargs.pop('type')
|
||||
def __init__(self, data: InviteChannelPayload):
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self.type: ChannelType = try_enum(ChannelType, data["type"])
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<PartialInviteChannel id={self.id} name={self.name} type={self.type!r}>"
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
return f'<#{self.id}>'
|
||||
return f"<#{self.id}>"
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
|
||||
class PartialInviteGuild:
|
||||
"""Represents a "partial" invite guild.
|
||||
|
||||
@@ -125,93 +150,61 @@ class PartialInviteGuild:
|
||||
The partial guild's verification level.
|
||||
features: List[:class:`str`]
|
||||
A list of features the guild has. See :attr:`Guild.features` for more information.
|
||||
icon: Optional[:class:`str`]
|
||||
The partial guild's icon.
|
||||
banner: Optional[:class:`str`]
|
||||
The partial guild's banner.
|
||||
splash: Optional[:class:`str`]
|
||||
The partial guild's invite splash.
|
||||
description: Optional[:class:`str`]
|
||||
The partial guild's description.
|
||||
"""
|
||||
|
||||
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash',
|
||||
'verification_level', 'description')
|
||||
__slots__ = ("_state", "features", "_icon", "_banner", "id", "name", "_splash", "verification_level", "description")
|
||||
|
||||
def __init__(self, state, data, id):
|
||||
self._state = state
|
||||
self.id = id
|
||||
self.name = data['name']
|
||||
self.features = data.get('features', [])
|
||||
self.icon = data.get('icon')
|
||||
self.banner = data.get('banner')
|
||||
self.splash = data.get('splash')
|
||||
self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
|
||||
self.description = data.get('description')
|
||||
def __init__(self, state: ConnectionState, data: InviteGuildPayload, id: int):
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = id
|
||||
self.name: str = data["name"]
|
||||
self.features: List[str] = data.get("features", [])
|
||||
self._icon: Optional[str] = data.get("icon")
|
||||
self._banner: Optional[str] = data.get("banner")
|
||||
self._splash: Optional[str] = data.get("splash")
|
||||
self.verification_level: VerificationLevel = try_enum(VerificationLevel, data.get("verification_level"))
|
||||
self.description: Optional[str] = data.get("description")
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} features={0.features} ' \
|
||||
'description={0.description!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__} id={self.id} name={self.name!r} features={self.features} "
|
||||
f"description={self.description!r}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`Asset`: Returns the guild's icon asset."""
|
||||
return self.icon_url_as()
|
||||
|
||||
def is_icon_animated(self):
|
||||
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
return bool(self.icon and self.icon.startswith('a_'))
|
||||
|
||||
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
|
||||
"""The same operation as :meth:`Guild.icon_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
|
||||
def icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_guild_icon(self._state, self.id, self._icon)
|
||||
|
||||
@property
|
||||
def banner_url(self):
|
||||
""":class:`Asset`: Returns the guild's banner asset."""
|
||||
return self.banner_url_as()
|
||||
|
||||
def banner_url_as(self, *, format='webp', size=2048):
|
||||
"""The same operation as :meth:`Guild.banner_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
|
||||
def banner(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
|
||||
if self._banner is None:
|
||||
return None
|
||||
return Asset._from_guild_image(self._state, self.id, self._banner, path="banners")
|
||||
|
||||
@property
|
||||
def splash_url(self):
|
||||
""":class:`Asset`: Returns the guild's invite splash asset."""
|
||||
return self.splash_url_as()
|
||||
def splash(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
|
||||
if self._splash is None:
|
||||
return None
|
||||
return Asset._from_guild_image(self._state, self.id, self._splash, path="splashes")
|
||||
|
||||
def splash_url_as(self, *, format='webp', size=2048):
|
||||
"""The same operation as :meth:`Guild.splash_url_as`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
|
||||
I = TypeVar("I", bound="Invite")
|
||||
|
||||
|
||||
class Invite(Hashable):
|
||||
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
||||
@@ -237,32 +230,35 @@ class Invite(Hashable):
|
||||
|
||||
Returns the invite URL.
|
||||
|
||||
|
||||
The following table illustrates what methods will obtain the attributes:
|
||||
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| Attribute | Method |
|
||||
+====================================+==========================================================+
|
||||
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` |
|
||||
+------------------------------------+----------------------------------------------------------+
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| Attribute | Method |
|
||||
+====================================+============================================================+
|
||||
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
| :attr:`expires_at` | :meth:`Client.fetch_invite` with `with_expiration` enabled |
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
|
||||
If it's not in the table above then it is available by all methods.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
max_age: :class:`int`
|
||||
How long the before the invite expires in seconds.
|
||||
How long before the invite expires in seconds.
|
||||
A value of ``0`` indicates that it doesn't expire.
|
||||
code: :class:`str`
|
||||
The URL fragment used for the invite.
|
||||
@@ -280,105 +276,190 @@ class Invite(Hashable):
|
||||
max_uses: :class:`int`
|
||||
How many times the invite can be used.
|
||||
A value of ``0`` indicates that it has unlimited uses.
|
||||
inviter: :class:`User`
|
||||
inviter: Optional[:class:`User`]
|
||||
The user who created the invite.
|
||||
approximate_member_count: Optional[:class:`int`]
|
||||
The approximate number of members in the guild.
|
||||
approximate_presence_count: Optional[:class:`int`]
|
||||
The approximate number of members currently active in the guild.
|
||||
This includes idle, dnd, online, and invisible members. Offline members are excluded.
|
||||
expires_at: Optional[:class:`datetime.datetime`]
|
||||
The expiration date of the invite. If the value is ``None`` when received through
|
||||
`Client.fetch_invite` with `with_expiration` enabled, the invite will never expire.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]
|
||||
The channel the invite is for.
|
||||
target_type: :class:`InviteTarget`
|
||||
The type of target for the voice channel invite.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
target_user: Optional[:class:`User`]
|
||||
The user whose stream to display for this invite, if any.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
target_application: Optional[:class:`PartialAppInfo`]
|
||||
The embedded application the invite targets, if any.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses',
|
||||
'temporary', 'max_uses', 'inviter', 'channel', '_state',
|
||||
'approximate_member_count', 'approximate_presence_count' )
|
||||
__slots__ = (
|
||||
"max_age",
|
||||
"code",
|
||||
"guild",
|
||||
"revoked",
|
||||
"created_at",
|
||||
"uses",
|
||||
"temporary",
|
||||
"max_uses",
|
||||
"inviter",
|
||||
"channel",
|
||||
"target_user",
|
||||
"target_type",
|
||||
"_state",
|
||||
"approximate_member_count",
|
||||
"approximate_presence_count",
|
||||
"target_application",
|
||||
"expires_at",
|
||||
)
|
||||
|
||||
BASE = 'https://discord.gg'
|
||||
BASE = "https://discord.gg"
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.max_age = data.get('max_age')
|
||||
self.code = data.get('code')
|
||||
self.guild = data.get('guild')
|
||||
self.revoked = data.get('revoked')
|
||||
self.created_at = parse_time(data.get('created_at'))
|
||||
self.temporary = data.get('temporary')
|
||||
self.uses = data.get('uses')
|
||||
self.max_uses = data.get('max_uses')
|
||||
self.approximate_presence_count = data.get('approximate_presence_count')
|
||||
self.approximate_member_count = data.get('approximate_member_count')
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
state: ConnectionState,
|
||||
data: InvitePayload,
|
||||
guild: Optional[Union[PartialInviteGuild, Guild]] = None,
|
||||
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
|
||||
):
|
||||
self._state: ConnectionState = state
|
||||
self.max_age: Optional[int] = data.get("max_age")
|
||||
self.code: str = data["code"]
|
||||
self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get("guild"), guild)
|
||||
self.revoked: Optional[bool] = data.get("revoked")
|
||||
self.created_at: Optional[datetime.datetime] = parse_time(data.get("created_at"))
|
||||
self.temporary: Optional[bool] = data.get("temporary")
|
||||
self.uses: Optional[int] = data.get("uses")
|
||||
self.max_uses: Optional[int] = data.get("max_uses")
|
||||
self.approximate_presence_count: Optional[int] = data.get("approximate_presence_count")
|
||||
self.approximate_member_count: Optional[int] = data.get("approximate_member_count")
|
||||
|
||||
inviter_data = data.get('inviter')
|
||||
self.inviter = None if inviter_data is None else self._state.store_user(inviter_data)
|
||||
self.channel = data.get('channel')
|
||||
expires_at = data.get("expires_at", None)
|
||||
self.expires_at: Optional[datetime.datetime] = parse_time(expires_at) if expires_at else None
|
||||
|
||||
inviter_data = data.get("inviter")
|
||||
self.inviter: Optional[User] = None if inviter_data is None else self._state.create_user(inviter_data)
|
||||
|
||||
self.channel: Optional[InviteChannelType] = self._resolve_channel(data.get("channel"), channel)
|
||||
|
||||
target_user_data = data.get("target_user")
|
||||
self.target_user: Optional[User] = (
|
||||
None if target_user_data is None else self._state.create_user(target_user_data)
|
||||
)
|
||||
|
||||
self.target_type: InviteTarget = try_enum(InviteTarget, data.get("target_type", 0))
|
||||
|
||||
application = data.get("target_application")
|
||||
self.target_application: Optional[PartialAppInfo] = (
|
||||
PartialAppInfo(data=application, state=state) if application else None
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_incomplete(cls, *, state, data):
|
||||
def from_incomplete(cls: Type[I], *, state: ConnectionState, data: InvitePayload) -> I:
|
||||
guild: Optional[Union[Guild, PartialInviteGuild]]
|
||||
try:
|
||||
guild_id = int(data['guild']['id'])
|
||||
guild_data = data["guild"]
|
||||
except KeyError:
|
||||
# If we're here, then this is a group DM
|
||||
guild = None
|
||||
else:
|
||||
guild_id = int(guild_data["id"])
|
||||
guild = state._get_guild(guild_id)
|
||||
if guild is None:
|
||||
# If it's not cached, then it has to be a partial guild
|
||||
guild_data = data['guild']
|
||||
guild = PartialInviteGuild(state, guild_data, guild_id)
|
||||
|
||||
# As far as I know, invites always need a channel
|
||||
# So this should never raise.
|
||||
channel_data = data['channel']
|
||||
channel_id = int(channel_data['id'])
|
||||
channel_type = try_enum(ChannelType, channel_data['type'])
|
||||
channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type)
|
||||
channel: Union[PartialInviteChannel, GuildChannel] = PartialInviteChannel(data["channel"])
|
||||
if guild is not None and not isinstance(guild, PartialInviteGuild):
|
||||
# Upgrade the partial data if applicable
|
||||
channel = guild.get_channel(channel_id) or channel
|
||||
channel = guild.get_channel(channel.id) or channel
|
||||
|
||||
data['guild'] = guild
|
||||
data['channel'] = channel
|
||||
return cls(state=state, data=data)
|
||||
return cls(state=state, data=data, guild=guild, channel=channel)
|
||||
|
||||
@classmethod
|
||||
def from_gateway(cls, *, state, data):
|
||||
guild_id = _get_as_snowflake(data, 'guild_id')
|
||||
guild = state._get_guild(guild_id)
|
||||
channel_id = _get_as_snowflake(data, 'channel_id')
|
||||
def from_gateway(cls: Type[I], *, state: ConnectionState, data: GatewayInvitePayload) -> I:
|
||||
guild_id: Optional[int] = _get_as_snowflake(data, "guild_id")
|
||||
guild: Optional[Union[Guild, Object]] = state._get_guild(guild_id)
|
||||
channel_id = int(data["channel_id"])
|
||||
if guild is not None:
|
||||
channel = guild.get_channel(channel_id) or Object(id=channel_id)
|
||||
channel = guild.get_channel(channel_id) or Object(id=channel_id) # type: ignore
|
||||
else:
|
||||
guild = Object(id=guild_id)
|
||||
guild = Object(id=guild_id) if guild_id is not None else None
|
||||
channel = Object(id=channel_id)
|
||||
|
||||
data['guild'] = guild
|
||||
data['channel'] = channel
|
||||
return cls(state=state, data=data)
|
||||
return cls(state=state, data=data, guild=guild, channel=channel) # type: ignore
|
||||
|
||||
def __str__(self):
|
||||
def _resolve_guild(
|
||||
self,
|
||||
data: Optional[InviteGuildPayload],
|
||||
guild: Optional[Union[Guild, PartialInviteGuild]] = None,
|
||||
) -> Optional[InviteGuildType]:
|
||||
if guild is not None:
|
||||
return guild
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
guild_id = int(data["id"])
|
||||
return PartialInviteGuild(self._state, data, guild_id)
|
||||
|
||||
def _resolve_channel(
|
||||
self,
|
||||
data: Optional[InviteChannelPayload],
|
||||
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
|
||||
) -> Optional[InviteChannelType]:
|
||||
if channel is not None:
|
||||
return channel
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return PartialInviteChannel(data)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.url
|
||||
|
||||
def __repr__(self):
|
||||
return '<Invite code={0.code!r} guild={0.guild!r} ' \
|
||||
'online={0.approximate_presence_count} ' \
|
||||
'members={0.approximate_member_count}>'.format(self)
|
||||
def __int__(self) -> int:
|
||||
return 0 # To keep the object compatible with the hashable abc.
|
||||
|
||||
def __hash__(self):
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Invite code={self.code!r} guild={self.guild!r} "
|
||||
f"online={self.approximate_presence_count} "
|
||||
f"members={self.approximate_member_count}>"
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.code)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
def id(self) -> str:
|
||||
""":class:`str`: Returns the proper code portion of the invite."""
|
||||
return self.code
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def url(self) -> str:
|
||||
""":class:`str`: A property that retrieves the invite URL."""
|
||||
return self.BASE + '/' + self.code
|
||||
return self.BASE + "/" + self.code
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
async def delete(self, *, reason: Optional[str] = None):
|
||||
"""|coro|
|
||||
|
||||
Revokes the instant invite.
|
||||
|
||||
@@ -26,42 +26,64 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
|
||||
from typing import Awaitable, TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator
|
||||
|
||||
from .errors import NoMoreItems
|
||||
from .utils import time_snowflake, maybe_coroutine
|
||||
from .utils import snowflake_time, time_snowflake, maybe_coroutine
|
||||
from .object import Object
|
||||
from .audit_logs import AuditLogEntry
|
||||
|
||||
__all__ = (
|
||||
'ReactionIterator',
|
||||
'HistoryIterator',
|
||||
'AuditLogIterator',
|
||||
'GuildIterator',
|
||||
'MemberIterator',
|
||||
"ReactionIterator",
|
||||
"HistoryIterator",
|
||||
"AuditLogIterator",
|
||||
"GuildIterator",
|
||||
"MemberIterator",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.audit_log import (
|
||||
AuditLog as AuditLogPayload,
|
||||
)
|
||||
from .types.guild import (
|
||||
Guild as GuildPayload,
|
||||
)
|
||||
from .types.message import (
|
||||
Message as MessagePayload,
|
||||
)
|
||||
from .types.user import (
|
||||
PartialUser as PartialUserPayload,
|
||||
)
|
||||
|
||||
from .types.threads import (
|
||||
Thread as ThreadPayload,
|
||||
)
|
||||
|
||||
from .member import Member
|
||||
from .user import User
|
||||
from .message import Message
|
||||
from .audit_logs import AuditLogEntry
|
||||
from .guild import Guild
|
||||
from .threads import Thread
|
||||
from .abc import Snowflake
|
||||
|
||||
T = TypeVar('T')
|
||||
OT = TypeVar('OT')
|
||||
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
|
||||
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
|
||||
T = TypeVar("T")
|
||||
OT = TypeVar("OT")
|
||||
_Func = Callable[[T], Union[OT, Awaitable[OT]]]
|
||||
|
||||
OLDEST_OBJECT = Object(id=0)
|
||||
|
||||
|
||||
class _AsyncIterator(AsyncIterator[T]):
|
||||
__slots__ = ()
|
||||
|
||||
def get(self, **attrs: Any) -> Optional[T]:
|
||||
def predicate(elem):
|
||||
async def next(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, **attrs: Any) -> Awaitable[Optional[T]]:
|
||||
def predicate(elem: T):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split('__')
|
||||
nested = attr.split("__")
|
||||
obj = elem
|
||||
for attribute in nested:
|
||||
obj = getattr(obj, attribute)
|
||||
@@ -72,7 +94,7 @@ class _AsyncIterator(AsyncIterator[T]):
|
||||
|
||||
return self.find(predicate)
|
||||
|
||||
async def find(self, predicate: _Predicate[T]) -> Optional[T]:
|
||||
async def find(self, predicate: _Func[T, bool]) -> Optional[T]:
|
||||
while True:
|
||||
try:
|
||||
elem = await self.next()
|
||||
@@ -85,13 +107,13 @@ class _AsyncIterator(AsyncIterator[T]):
|
||||
|
||||
def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
|
||||
if max_size <= 0:
|
||||
raise ValueError('async iterator chunk sizes must be greater than 0.')
|
||||
raise ValueError("async iterator chunk sizes must be greater than 0.")
|
||||
return _ChunkedAsyncIterator(self, max_size)
|
||||
|
||||
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
|
||||
return _MappedAsyncIterator(self, func)
|
||||
|
||||
def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
|
||||
def filter(self, predicate: _Func[T, bool]) -> _FilteredAsyncIterator[T]:
|
||||
return _FilteredAsyncIterator(self, predicate)
|
||||
|
||||
async def flatten(self) -> List[T]:
|
||||
@@ -103,16 +125,18 @@ class _AsyncIterator(AsyncIterator[T]):
|
||||
except NoMoreItems:
|
||||
raise StopAsyncIteration()
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
||||
|
||||
class _ChunkedAsyncIterator(_AsyncIterator[List[T]]):
|
||||
def __init__(self, iterator, max_size):
|
||||
self.iterator = iterator
|
||||
self.max_size = max_size
|
||||
|
||||
async def next(self) -> T:
|
||||
ret = []
|
||||
async def next(self) -> List[T]:
|
||||
ret: List[T] = []
|
||||
n = 0
|
||||
while n < self.max_size:
|
||||
try:
|
||||
@@ -126,6 +150,7 @@ class _ChunkedAsyncIterator(_AsyncIterator[T]):
|
||||
n += 1
|
||||
return ret
|
||||
|
||||
|
||||
class _MappedAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, func):
|
||||
self.iterator = iterator
|
||||
@@ -136,6 +161,7 @@ class _MappedAsyncIterator(_AsyncIterator[T]):
|
||||
item = await self.iterator.next()
|
||||
return await maybe_coroutine(self.func, item)
|
||||
|
||||
|
||||
class _FilteredAsyncIterator(_AsyncIterator[T]):
|
||||
def __init__(self, iterator, predicate):
|
||||
self.iterator = iterator
|
||||
@@ -155,7 +181,8 @@ class _FilteredAsyncIterator(_AsyncIterator[T]):
|
||||
if ret:
|
||||
return item
|
||||
|
||||
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||
|
||||
class ReactionIterator(_AsyncIterator[Union["User", "Member"]]):
|
||||
def __init__(self, message, emoji, limit=100, after=None):
|
||||
self.message = message
|
||||
self.limit = limit
|
||||
@@ -168,7 +195,7 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||
self.channel_id = message.channel.id
|
||||
self.users = asyncio.Queue()
|
||||
|
||||
async def next(self) -> T:
|
||||
async def next(self) -> Union[User, Member]:
|
||||
if self.users.empty():
|
||||
await self.fill_users()
|
||||
|
||||
@@ -185,25 +212,28 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
|
||||
retrieve = self.limit if self.limit <= 100 else 100
|
||||
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.getter(self.channel_id, self.message.id, self.emoji, retrieve, after=after)
|
||||
data: List[PartialUserPayload] = await self.getter(
|
||||
self.channel_id, self.message.id, self.emoji, retrieve, after=after
|
||||
)
|
||||
|
||||
if data:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[-1]['id']))
|
||||
self.after = Object(id=int(data[-1]["id"]))
|
||||
|
||||
if self.guild is None or isinstance(self.guild, Object):
|
||||
for element in reversed(data):
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
else:
|
||||
for element in reversed(data):
|
||||
member_id = int(element['id'])
|
||||
member_id = int(element["id"])
|
||||
member = self.guild.get_member(member_id)
|
||||
if member is not None:
|
||||
await self.users.put(member)
|
||||
else:
|
||||
await self.users.put(User(state=self.state, data=element))
|
||||
|
||||
class HistoryIterator(_AsyncIterator['Message']):
|
||||
|
||||
class HistoryIterator(_AsyncIterator["Message"]):
|
||||
"""Iterator for receiving a channel's message history.
|
||||
|
||||
The messages endpoint has two behaviours we care about here:
|
||||
@@ -237,8 +267,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
||||
``True`` if `after` is specified, otherwise ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, messageable, limit,
|
||||
before=None, after=None, around=None, oldest_first=None):
|
||||
def __init__(self, messageable, limit, before=None, after=None, around=None, oldest_first=None):
|
||||
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
@@ -266,30 +295,30 @@ class HistoryIterator(_AsyncIterator['Message']):
|
||||
|
||||
if self.around:
|
||||
if self.limit is None:
|
||||
raise ValueError('history does not support around with limit=None')
|
||||
raise ValueError("history does not support around with limit=None")
|
||||
if self.limit > 101:
|
||||
raise ValueError("history max limit 101 when specifying around parameter")
|
||||
elif self.limit == 101:
|
||||
self.limit = 100 # Thanks discord
|
||||
|
||||
self._retrieve_messages = self._retrieve_messages_around_strategy
|
||||
self._retrieve_messages = self._retrieve_messages_around_strategy # type: ignore
|
||||
if self.before and self.after:
|
||||
self._filter = lambda m: self.after.id < int(m['id']) < self.before.id
|
||||
self._filter = lambda m: self.after.id < int(m["id"]) < self.before.id
|
||||
elif self.before:
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
self._filter = lambda m: int(m["id"]) < self.before.id
|
||||
elif self.after:
|
||||
self._filter = lambda m: self.after.id < int(m['id'])
|
||||
self._filter = lambda m: self.after.id < int(m["id"])
|
||||
else:
|
||||
if self.reverse:
|
||||
self._retrieve_messages = self._retrieve_messages_after_strategy
|
||||
if (self.before):
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
self._retrieve_messages = self._retrieve_messages_after_strategy # type: ignore
|
||||
if self.before:
|
||||
self._filter = lambda m: int(m["id"]) < self.before.id
|
||||
else:
|
||||
self._retrieve_messages = self._retrieve_messages_before_strategy
|
||||
if (self.after and self.after != OLDEST_OBJECT):
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
self._retrieve_messages = self._retrieve_messages_before_strategy # type: ignore
|
||||
if self.after and self.after != OLDEST_OBJECT:
|
||||
self._filter = lambda m: int(m["id"]) > self.after.id
|
||||
|
||||
async def next(self) -> T:
|
||||
async def next(self) -> Message:
|
||||
if self.messages.empty():
|
||||
await self.fill_messages()
|
||||
|
||||
@@ -308,7 +337,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
||||
return r > 0
|
||||
|
||||
async def fill_messages(self):
|
||||
if not hasattr(self, 'channel'):
|
||||
if not hasattr(self, "channel"):
|
||||
# do the required set up
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
@@ -316,7 +345,7 @@ class HistoryIterator(_AsyncIterator['Message']):
|
||||
if self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
@@ -327,47 +356,47 @@ class HistoryIterator(_AsyncIterator['Message']):
|
||||
for element in data:
|
||||
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
||||
|
||||
async def _retrieve_messages(self, retrieve):
|
||||
async def _retrieve_messages(self, retrieve) -> List[Message]:
|
||||
"""Retrieve messages and update next parameters."""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
async def _retrieve_messages_before_strategy(self, retrieve):
|
||||
"""Retrieve messages using before parameter."""
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, before=before)
|
||||
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, before=before)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(data[-1]['id']))
|
||||
self.before = Object(id=int(data[-1]["id"]))
|
||||
return data
|
||||
|
||||
async def _retrieve_messages_after_strategy(self, retrieve):
|
||||
"""Retrieve messages using after parameter."""
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, after=after)
|
||||
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, after=after)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
self.after = Object(id=int(data[0]["id"]))
|
||||
return data
|
||||
|
||||
async def _retrieve_messages_around_strategy(self, retrieve):
|
||||
"""Retrieve messages using around parameter."""
|
||||
if self.around:
|
||||
around = self.around.id if self.around else None
|
||||
data = await self.logs_from(self.channel.id, retrieve, around=around)
|
||||
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, around=around)
|
||||
self.around = None
|
||||
return data
|
||||
return []
|
||||
|
||||
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
|
||||
class AuditLogIterator(_AsyncIterator["AuditLogEntry"]):
|
||||
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
|
||||
if isinstance(before, datetime.datetime):
|
||||
before = Object(id=time_snowflake(before, high=False))
|
||||
if isinstance(after, datetime.datetime):
|
||||
after = Object(id=time_snowflake(after, high=True))
|
||||
|
||||
|
||||
if oldest_first is None:
|
||||
self.reverse = after is not None
|
||||
else:
|
||||
@@ -384,45 +413,45 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
self._users = {}
|
||||
self._state = guild._state
|
||||
|
||||
|
||||
self._filter = None # entry dict -> bool
|
||||
|
||||
self.entries = asyncio.Queue()
|
||||
|
||||
|
||||
if self.reverse:
|
||||
self._strategy = self._after_strategy
|
||||
if self.before:
|
||||
self._filter = lambda m: int(m['id']) < self.before.id
|
||||
self._filter = lambda m: int(m["id"]) < self.before.id
|
||||
else:
|
||||
self._strategy = self._before_strategy
|
||||
if self.after and self.after != OLDEST_OBJECT:
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
self._filter = lambda m: int(m["id"]) > self.after.id
|
||||
|
||||
async def _before_strategy(self, retrieve):
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
||||
action_type=self.action_type, before=before)
|
||||
data: AuditLogPayload = await self.request(
|
||||
self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, before=before
|
||||
)
|
||||
|
||||
entries = data.get('audit_log_entries', [])
|
||||
entries = data.get("audit_log_entries", [])
|
||||
if len(data) and entries:
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(entries[-1]['id']))
|
||||
return data.get('users', []), entries
|
||||
self.before = Object(id=int(entries[-1]["id"]))
|
||||
return data.get("users", []), entries
|
||||
|
||||
async def _after_strategy(self, retrieve):
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id,
|
||||
action_type=self.action_type, after=after)
|
||||
entries = data.get('audit_log_entries', [])
|
||||
data: AuditLogPayload = await self.request(
|
||||
self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, after=after
|
||||
)
|
||||
entries = data.get("audit_log_entries", [])
|
||||
if len(data) and entries:
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(entries[0]['id']))
|
||||
return data.get('users', []), entries
|
||||
self.after = Object(id=int(entries[0]["id"]))
|
||||
return data.get("users", []), entries
|
||||
|
||||
async def next(self) -> T:
|
||||
async def next(self) -> AuditLogEntry:
|
||||
if self.entries.empty():
|
||||
await self._fill()
|
||||
|
||||
@@ -446,7 +475,7 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
if self._get_retrieve():
|
||||
users, data = await self._strategy(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
@@ -459,13 +488,13 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
|
||||
|
||||
for element in data:
|
||||
# TODO: remove this if statement later
|
||||
if element['action_type'] is None:
|
||||
if element["action_type"] is None:
|
||||
continue
|
||||
|
||||
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
|
||||
|
||||
|
||||
class GuildIterator(_AsyncIterator['Guild']):
|
||||
class GuildIterator(_AsyncIterator["Guild"]):
|
||||
"""Iterator for receiving the client's guilds.
|
||||
|
||||
The guilds endpoint has the same two behaviours as described
|
||||
@@ -493,6 +522,7 @@ class GuildIterator(_AsyncIterator['Guild']):
|
||||
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Object after which all guilds must be.
|
||||
"""
|
||||
|
||||
def __init__(self, bot, limit, before=None, after=None):
|
||||
|
||||
if isinstance(before, datetime.datetime):
|
||||
@@ -512,14 +542,14 @@ class GuildIterator(_AsyncIterator['Guild']):
|
||||
self.guilds = asyncio.Queue()
|
||||
|
||||
if self.before and self.after:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
self._filter = lambda m: int(m['id']) > self.after.id
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
|
||||
self._filter = lambda m: int(m["id"]) > self.after.id
|
||||
elif self.after:
|
||||
self._retrieve_guilds = self._retrieve_guilds_after_strategy
|
||||
self._retrieve_guilds = self._retrieve_guilds_after_strategy # type: ignore
|
||||
else:
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy
|
||||
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
|
||||
|
||||
async def next(self) -> T:
|
||||
async def next(self) -> Guild:
|
||||
if self.guilds.empty():
|
||||
await self.fill_guilds()
|
||||
|
||||
@@ -539,6 +569,7 @@ class GuildIterator(_AsyncIterator['Guild']):
|
||||
|
||||
def create_guild(self, data):
|
||||
from .guild import Guild
|
||||
|
||||
return Guild(state=self.state, data=data)
|
||||
|
||||
async def fill_guilds(self):
|
||||
@@ -553,31 +584,32 @@ class GuildIterator(_AsyncIterator['Guild']):
|
||||
for element in data:
|
||||
await self.guilds.put(self.create_guild(element))
|
||||
|
||||
async def _retrieve_guilds(self, retrieve):
|
||||
async def _retrieve_guilds(self, retrieve) -> List[Guild]:
|
||||
"""Retrieve guilds and update next parameters."""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
async def _retrieve_guilds_before_strategy(self, retrieve):
|
||||
"""Retrieve guilds using before parameter."""
|
||||
before = self.before.id if self.before else None
|
||||
data = await self.get_guilds(retrieve, before=before)
|
||||
data: List[GuildPayload] = await self.get_guilds(retrieve, before=before)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.before = Object(id=int(data[-1]['id']))
|
||||
self.before = Object(id=int(data[-1]["id"]))
|
||||
return data
|
||||
|
||||
async def _retrieve_guilds_after_strategy(self, retrieve):
|
||||
"""Retrieve guilds using after parameter."""
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.get_guilds(retrieve, after=after)
|
||||
data: List[GuildPayload] = await self.get_guilds(retrieve, after=after)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[0]['id']))
|
||||
self.after = Object(id=int(data[0]["id"]))
|
||||
return data
|
||||
|
||||
class MemberIterator(_AsyncIterator['Member']):
|
||||
|
||||
class MemberIterator(_AsyncIterator["Member"]):
|
||||
def __init__(self, guild, limit=1000, after=None):
|
||||
|
||||
if isinstance(after, datetime.datetime):
|
||||
@@ -591,7 +623,7 @@ class MemberIterator(_AsyncIterator['Member']):
|
||||
self.get_members = self.state.http.get_members
|
||||
self.members = asyncio.Queue()
|
||||
|
||||
async def next(self) -> T:
|
||||
async def next(self) -> Member:
|
||||
if self.members.empty():
|
||||
await self.fill_members()
|
||||
|
||||
@@ -618,13 +650,105 @@ class MemberIterator(_AsyncIterator['Member']):
|
||||
return
|
||||
|
||||
if len(data) < 1000:
|
||||
self.limit = 0 # terminate loop
|
||||
self.limit = 0 # terminate loop
|
||||
|
||||
self.after = Object(id=int(data[-1]['user']['id']))
|
||||
self.after = Object(id=int(data[-1]["user"]["id"]))
|
||||
|
||||
for element in reversed(data):
|
||||
await self.members.put(self.create_member(element))
|
||||
|
||||
def create_member(self, data):
|
||||
from .member import Member
|
||||
|
||||
return Member(data=data, guild=self.guild, state=self.state)
|
||||
|
||||
|
||||
class ArchivedThreadIterator(_AsyncIterator["Thread"]):
|
||||
def __init__(
|
||||
self,
|
||||
channel_id: int,
|
||||
guild: Guild,
|
||||
limit: Optional[int],
|
||||
joined: bool,
|
||||
private: bool,
|
||||
before: Optional[Union[Snowflake, datetime.datetime]] = None,
|
||||
):
|
||||
self.channel_id = channel_id
|
||||
self.guild = guild
|
||||
self.limit = limit
|
||||
self.joined = joined
|
||||
self.private = private
|
||||
self.http = guild._state.http
|
||||
|
||||
if joined and not private:
|
||||
raise ValueError("Cannot iterate over joined public archived threads")
|
||||
|
||||
self.before: Optional[str]
|
||||
if before is None:
|
||||
self.before = None
|
||||
elif isinstance(before, datetime.datetime):
|
||||
if joined:
|
||||
self.before = str(time_snowflake(before, high=False))
|
||||
else:
|
||||
self.before = before.isoformat()
|
||||
else:
|
||||
if joined:
|
||||
self.before = str(before.id)
|
||||
else:
|
||||
self.before = snowflake_time(before.id).isoformat()
|
||||
|
||||
self.update_before: Callable[[ThreadPayload], str] = self.get_archive_timestamp
|
||||
|
||||
if joined:
|
||||
self.endpoint = self.http.get_joined_private_archived_threads
|
||||
self.update_before = self.get_thread_id
|
||||
elif private:
|
||||
self.endpoint = self.http.get_private_archived_threads
|
||||
else:
|
||||
self.endpoint = self.http.get_public_archived_threads
|
||||
|
||||
self.queue: asyncio.Queue[Thread] = asyncio.Queue()
|
||||
self.has_more: bool = True
|
||||
|
||||
async def next(self) -> Thread:
|
||||
if self.queue.empty():
|
||||
await self.fill_queue()
|
||||
|
||||
try:
|
||||
return self.queue.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
@staticmethod
|
||||
def get_archive_timestamp(data: ThreadPayload) -> str:
|
||||
return data["thread_metadata"]["archive_timestamp"]
|
||||
|
||||
@staticmethod
|
||||
def get_thread_id(data: ThreadPayload) -> str:
|
||||
return data["id"] # type: ignore
|
||||
|
||||
async def fill_queue(self) -> None:
|
||||
if not self.has_more:
|
||||
raise NoMoreItems()
|
||||
|
||||
limit = 50 if self.limit is None else max(self.limit, 50)
|
||||
data = await self.endpoint(self.channel_id, before=self.before, limit=limit)
|
||||
|
||||
# This stuff is obviously WIP because 'members' is always empty
|
||||
threads: List[ThreadPayload] = data.get("threads", [])
|
||||
for d in reversed(threads):
|
||||
self.queue.put_nowait(self.create_thread(d))
|
||||
|
||||
self.has_more = data.get("has_more", False)
|
||||
if self.limit is not None:
|
||||
self.limit -= len(threads)
|
||||
if self.limit <= 0:
|
||||
self.has_more = False
|
||||
|
||||
if self.has_more:
|
||||
self.before = self.update_before(threads[-1])
|
||||
|
||||
def create_thread(self, data: ThreadPayload) -> Thread:
|
||||
from .threads import Thread
|
||||
|
||||
return Thread(guild=self.guild, state=self.guild._state, data=data)
|
||||
|
||||
@@ -22,28 +22,53 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import itertools
|
||||
import sys
|
||||
from operator import attrgetter
|
||||
from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union, overload
|
||||
|
||||
import discord.abc
|
||||
|
||||
from . import utils
|
||||
from .errors import ClientException
|
||||
from .user import BaseUser, User
|
||||
from .activity import create_activity
|
||||
from .asset import Asset
|
||||
from .utils import MISSING
|
||||
from .user import BaseUser, User, _UserTag
|
||||
from .activity import create_activity, ActivityTypes
|
||||
from .permissions import Permissions
|
||||
from .enums import Status, try_enum
|
||||
from .colour import Colour
|
||||
from .object import Object
|
||||
|
||||
__all__ = (
|
||||
'VoiceState',
|
||||
'Member',
|
||||
"VoiceState",
|
||||
"Member",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .asset import Asset
|
||||
from .channel import DMChannel, VoiceChannel, StageChannel
|
||||
from .flags import PublicUserFlags
|
||||
from .guild import Guild
|
||||
from .types.activity import PartialPresenceUpdate
|
||||
from .types.member import (
|
||||
MemberWithUser as MemberWithUserPayload,
|
||||
Member as MemberPayload,
|
||||
UserWithMember as UserWithMemberPayload,
|
||||
)
|
||||
from .types.user import User as UserPayload
|
||||
from .abc import Snowflake
|
||||
from .state import ConnectionState
|
||||
from .message import Message
|
||||
from .role import Role
|
||||
from .types.voice import VoiceState as VoiceStatePayload
|
||||
|
||||
VocalGuildChannel = Union[VoiceChannel, StageChannel]
|
||||
|
||||
|
||||
class VoiceState:
|
||||
"""Represents a Discord user's voice state.
|
||||
|
||||
@@ -87,42 +112,55 @@ class VoiceState:
|
||||
is not currently in a voice channel.
|
||||
"""
|
||||
|
||||
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute',
|
||||
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel',
|
||||
'requested_to_speak_at', 'suppress')
|
||||
__slots__ = (
|
||||
"session_id",
|
||||
"deaf",
|
||||
"mute",
|
||||
"self_mute",
|
||||
"self_stream",
|
||||
"self_video",
|
||||
"self_deaf",
|
||||
"afk",
|
||||
"channel",
|
||||
"requested_to_speak_at",
|
||||
"suppress",
|
||||
)
|
||||
|
||||
def __init__(self, *, data, channel=None):
|
||||
self.session_id = data.get('session_id')
|
||||
def __init__(self, *, data: VoiceStatePayload, channel: Optional[VocalGuildChannel] = None):
|
||||
self.session_id: str = data.get("session_id")
|
||||
self._update(data, channel)
|
||||
|
||||
def _update(self, data, channel):
|
||||
self.self_mute = data.get('self_mute', False)
|
||||
self.self_deaf = data.get('self_deaf', False)
|
||||
self.self_stream = data.get('self_stream', False)
|
||||
self.self_video = data.get('self_video', False)
|
||||
self.afk = data.get('suppress', False)
|
||||
self.mute = data.get('mute', False)
|
||||
self.deaf = data.get('deaf', False)
|
||||
self.suppress = data.get('suppress', False)
|
||||
self.requested_to_speak_at = utils.parse_time(data.get('request_to_speak_timestamp'))
|
||||
self.channel = channel
|
||||
def _update(self, data: VoiceStatePayload, channel: Optional[VocalGuildChannel]):
|
||||
self.self_mute: bool = data.get("self_mute", False)
|
||||
self.self_deaf: bool = data.get("self_deaf", False)
|
||||
self.self_stream: bool = data.get("self_stream", False)
|
||||
self.self_video: bool = data.get("self_video", False)
|
||||
self.afk: bool = data.get("suppress", False)
|
||||
self.mute: bool = data.get("mute", False)
|
||||
self.deaf: bool = data.get("deaf", False)
|
||||
self.suppress: bool = data.get("suppress", False)
|
||||
self.requested_to_speak_at: Optional[datetime.datetime] = utils.parse_time(
|
||||
data.get("request_to_speak_timestamp")
|
||||
)
|
||||
self.channel: Optional[VocalGuildChannel] = channel
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
attrs = [
|
||||
('self_mute', self.self_mute),
|
||||
('self_deaf', self.self_deaf),
|
||||
('self_stream', self.self_stream),
|
||||
('suppress', self.suppress),
|
||||
('requested_to_speak_at', self.requested_to_speak_at),
|
||||
('channel', self.channel)
|
||||
("self_mute", self.self_mute),
|
||||
("self_deaf", self.self_deaf),
|
||||
("self_stream", self.self_stream),
|
||||
("suppress", self.suppress),
|
||||
("requested_to_speak_at", self.requested_to_speak_at),
|
||||
("channel", self.channel),
|
||||
]
|
||||
inner = ' '.join('%s=%r' % t for t in attrs)
|
||||
return f'<{self.__class__.__name__} {inner}>'
|
||||
inner = " ".join("%s=%r" % t for t in attrs)
|
||||
return f"<{self.__class__.__name__} {inner}>"
|
||||
|
||||
|
||||
def flatten_user(cls):
|
||||
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
|
||||
# ignore private/special methods
|
||||
if attr.startswith('_'):
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
|
||||
# don't override what we already have
|
||||
@@ -131,9 +169,9 @@ def flatten_user(cls):
|
||||
|
||||
# if it's a slotted attribute or a property, redirect it
|
||||
# slotted members are implemented as member_descriptors in Type.__dict__
|
||||
if not hasattr(value, '__annotations__'):
|
||||
getter = attrgetter('_user.' + attr)
|
||||
setattr(cls, attr, property(getter, doc=f'Equivalent to :attr:`User.{attr}`'))
|
||||
if not hasattr(value, "__annotations__"):
|
||||
getter = attrgetter("_user." + attr)
|
||||
setattr(cls, attr, property(getter, doc=f"Equivalent to :attr:`User.{attr}`"))
|
||||
else:
|
||||
# Technically, this can also use attrgetter
|
||||
# However I'm not sure how I feel about "functions" returning properties
|
||||
@@ -142,9 +180,12 @@ def flatten_user(cls):
|
||||
def generate_function(x):
|
||||
# We want sphinx to properly show coroutine functions as coroutines
|
||||
if inspect.iscoroutinefunction(value):
|
||||
async def general(self, *args, **kwargs):
|
||||
|
||||
async def general(self, *args, **kwargs): # type: ignore
|
||||
return await getattr(self._user, x)(*args, **kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
|
||||
@@ -157,10 +198,12 @@ def flatten_user(cls):
|
||||
|
||||
return cls
|
||||
|
||||
_BaseUser = discord.abc.User
|
||||
|
||||
M = TypeVar("M", bound="Member")
|
||||
|
||||
|
||||
@flatten_user
|
||||
class Member(discord.abc.Messageable, _BaseUser):
|
||||
class Member(discord.abc.Messageable, _UserTag):
|
||||
"""Represents a Discord member to a :class:`Guild`.
|
||||
|
||||
This implements a lot of the functionality of :class:`User`.
|
||||
@@ -185,6 +228,10 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
Returns the member's name with the discriminator.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the user's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
joined_at: Optional[:class:`datetime.datetime`]
|
||||
@@ -192,6 +239,13 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
|
||||
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
|
||||
The activities that the user is currently doing.
|
||||
|
||||
.. note::
|
||||
|
||||
Due to a Discord API limitation, a user's Spotify activity may not appear
|
||||
if they are listening to a song with a title longer
|
||||
than 128 characters. See :issue:`1738` for more information.
|
||||
|
||||
guild: :class:`Guild`
|
||||
The guild that the member belongs to.
|
||||
nick: Optional[:class:`str`]
|
||||
@@ -202,80 +256,103 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
.. versionadded:: 1.6
|
||||
premium_since: Optional[:class:`datetime.datetime`]
|
||||
An aware datetime object that specifies the date and time in UTC when the member used their
|
||||
Nitro boost on the guild, if available. This could be ``None``.
|
||||
"Nitro boost" on the guild, if available. This could be ``None``.
|
||||
"""
|
||||
|
||||
__slots__ = ('_roles', 'joined_at', 'premium_since', '_client_status',
|
||||
'activities', 'guild', 'pending', 'nick', '_user', '_state')
|
||||
__slots__ = (
|
||||
"_roles",
|
||||
"joined_at",
|
||||
"premium_since",
|
||||
"activities",
|
||||
"guild",
|
||||
"pending",
|
||||
"nick",
|
||||
"_client_status",
|
||||
"_user",
|
||||
"_state",
|
||||
"_avatar",
|
||||
)
|
||||
|
||||
def __init__(self, *, data, guild, state):
|
||||
self._state = state
|
||||
self._user = state.store_user(data['user'])
|
||||
self.guild = guild
|
||||
self.joined_at = utils.parse_time(data.get('joined_at'))
|
||||
self.premium_since = utils.parse_time(data.get('premium_since'))
|
||||
self._update_roles(data)
|
||||
self._client_status = {
|
||||
None: 'offline'
|
||||
}
|
||||
self.activities = tuple(map(create_activity, data.get('activities', [])))
|
||||
self.nick = data.get('nick', None)
|
||||
self.pending = data.get('pending', False)
|
||||
if TYPE_CHECKING:
|
||||
name: str
|
||||
id: int
|
||||
discriminator: str
|
||||
bot: bool
|
||||
system: bool
|
||||
created_at: datetime.datetime
|
||||
default_avatar: Asset
|
||||
avatar: Optional[Asset]
|
||||
dm_channel: Optional[DMChannel]
|
||||
create_dm = User.create_dm
|
||||
mutual_guilds: List[Guild]
|
||||
public_flags: PublicUserFlags
|
||||
banner: Optional[Asset]
|
||||
accent_color: Optional[Colour]
|
||||
accent_colour: Optional[Colour]
|
||||
|
||||
def __str__(self):
|
||||
def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState):
|
||||
self._state: ConnectionState = state
|
||||
self._user: User = state.store_user(data["user"])
|
||||
self.guild: Guild = guild
|
||||
self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get("joined_at"))
|
||||
self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get("premium_since"))
|
||||
self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data["roles"]))
|
||||
self._client_status: Dict[Optional[str], str] = {None: "offline"}
|
||||
self.activities: Tuple[ActivityTypes, ...] = tuple()
|
||||
self.nick: Optional[str] = data.get("nick", None)
|
||||
self.pending: bool = data.get("pending", False)
|
||||
self._avatar: Optional[str] = data.get("avatar")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._user)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}' \
|
||||
f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _BaseUser) and other.id == self.id
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}"
|
||||
f" bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>"
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, _UserTag) and other.id == self.id
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._user)
|
||||
|
||||
@classmethod
|
||||
def _from_message(cls, *, message, data):
|
||||
def _from_message(cls: Type[M], *, message: Message, data: MemberPayload) -> M:
|
||||
author = message.author
|
||||
data['user'] = author._to_minimal_user_json()
|
||||
return cls(data=data, guild=message.guild, state=message._state)
|
||||
data["user"] = author._to_minimal_user_json() # type: ignore
|
||||
return cls(data=data, guild=message.guild, state=message._state) # type: ignore
|
||||
|
||||
def _update_from_message(self, data):
|
||||
self.joined_at = utils.parse_time(data.get('joined_at'))
|
||||
self.premium_since = utils.parse_time(data.get('premium_since'))
|
||||
self._update_roles(data)
|
||||
self.nick = data.get('nick', None)
|
||||
self.pending = data.get('pending', False)
|
||||
def _update_from_message(self, data: MemberPayload) -> None:
|
||||
self.joined_at = utils.parse_time(data.get("joined_at"))
|
||||
self.premium_since = utils.parse_time(data.get("premium_since"))
|
||||
self._roles = utils.SnowflakeList(map(int, data["roles"]))
|
||||
self.nick = data.get("nick", None)
|
||||
self.pending = data.get("pending", False)
|
||||
|
||||
@classmethod
|
||||
def _try_upgrade(cls, *, data, guild, state):
|
||||
def _try_upgrade(
|
||||
cls: Type[M], *, data: UserWithMemberPayload, guild: Guild, state: ConnectionState
|
||||
) -> Union[User, M]:
|
||||
# A User object with a 'member' key
|
||||
try:
|
||||
member_data = data.pop('member')
|
||||
member_data = data.pop("member")
|
||||
except KeyError:
|
||||
return state.store_user(data)
|
||||
return state.create_user(data)
|
||||
else:
|
||||
member_data['user'] = data
|
||||
return cls(data=member_data, guild=guild, state=state)
|
||||
member_data["user"] = data # type: ignore
|
||||
return cls(data=member_data, guild=guild, state=state) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def _from_presence_update(cls, *, data, guild, state):
|
||||
clone = cls(data=data, guild=guild, state=state)
|
||||
to_return = cls(data=data, guild=guild, state=state)
|
||||
to_return._client_status = {
|
||||
sys.intern(key): sys.intern(value)
|
||||
for key, value in data.get('client_status', {}).items()
|
||||
}
|
||||
to_return._client_status[None] = sys.intern(data['status'])
|
||||
return to_return, clone
|
||||
|
||||
@classmethod
|
||||
def _copy(cls, member):
|
||||
self = cls.__new__(cls) # to bypass __init__
|
||||
def _copy(cls: Type[M], member: M) -> M:
|
||||
self: M = cls.__new__(cls) # to bypass __init__
|
||||
|
||||
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
|
||||
self.joined_at = member.joined_at
|
||||
@@ -286,6 +363,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
self.pending = member.pending
|
||||
self.activities = member.activities
|
||||
self._state = member._state
|
||||
self._avatar = member._avatar
|
||||
|
||||
# Reference will not be copied unless necessary by PRESENCE_UPDATE
|
||||
# See below
|
||||
@@ -296,55 +374,52 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
ch = await self.create_dm()
|
||||
return ch
|
||||
|
||||
def _update_roles(self, data):
|
||||
self._roles = utils.SnowflakeList(map(int, data['roles']))
|
||||
|
||||
def _update(self, data):
|
||||
def _update(self, data: MemberPayload) -> None:
|
||||
# the nickname change is optional,
|
||||
# if it isn't in the payload then it didn't change
|
||||
try:
|
||||
self.nick = data['nick']
|
||||
self.nick = data["nick"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.pending = data['pending']
|
||||
self.pending = data["pending"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.premium_since = utils.parse_time(data.get('premium_since'))
|
||||
self._update_roles(data)
|
||||
self.premium_since = utils.parse_time(data.get("premium_since"))
|
||||
self._roles = utils.SnowflakeList(map(int, data["roles"]))
|
||||
self._avatar = data.get("avatar")
|
||||
|
||||
def _presence_update(self, data, user):
|
||||
self.activities = tuple(map(create_activity, data.get('activities', [])))
|
||||
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
|
||||
self.activities = tuple(map(create_activity, data["activities"]))
|
||||
self._client_status = {
|
||||
sys.intern(key): sys.intern(value)
|
||||
for key, value in data.get('client_status', {}).items()
|
||||
sys.intern(key): sys.intern(value) for key, value in data.get("client_status", {}).items() # type: ignore
|
||||
}
|
||||
self._client_status[None] = sys.intern(data['status'])
|
||||
self._client_status[None] = sys.intern(data["status"])
|
||||
|
||||
if len(user) > 1:
|
||||
return self._update_inner_user(user)
|
||||
return False
|
||||
return None
|
||||
|
||||
def _update_inner_user(self, user):
|
||||
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
|
||||
u = self._user
|
||||
original = (u.name, u.avatar, u.discriminator, u._public_flags)
|
||||
original = (u.name, u._avatar, u.discriminator, u._public_flags)
|
||||
# These keys seem to always be available
|
||||
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
|
||||
modified = (user["username"], user["avatar"], user["discriminator"], user.get("public_flags", 0))
|
||||
if original != modified:
|
||||
to_return = User._copy(self._user)
|
||||
u.name, u.avatar, u.discriminator, u._public_flags = modified
|
||||
u.name, u._avatar, u.discriminator, u._public_flags = modified
|
||||
# Signal to dispatch on_user_update
|
||||
return to_return, u
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> Status:
|
||||
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
|
||||
return try_enum(Status, self._client_status[None])
|
||||
|
||||
@property
|
||||
def raw_status(self):
|
||||
def raw_status(self) -> str:
|
||||
""":class:`str`: The member's overall status as a string value.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
@@ -352,31 +427,31 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return self._client_status[None]
|
||||
|
||||
@status.setter
|
||||
def status(self, value):
|
||||
def status(self, value: Status) -> None:
|
||||
# internal use only
|
||||
self._client_status[None] = str(value)
|
||||
|
||||
@property
|
||||
def mobile_status(self):
|
||||
def mobile_status(self) -> Status:
|
||||
""":class:`Status`: The member's status on a mobile device, if applicable."""
|
||||
return try_enum(Status, self._client_status.get('mobile', 'offline'))
|
||||
return try_enum(Status, self._client_status.get("mobile", "offline"))
|
||||
|
||||
@property
|
||||
def desktop_status(self):
|
||||
def desktop_status(self) -> Status:
|
||||
""":class:`Status`: The member's status on the desktop client, if applicable."""
|
||||
return try_enum(Status, self._client_status.get('desktop', 'offline'))
|
||||
return try_enum(Status, self._client_status.get("desktop", "offline"))
|
||||
|
||||
@property
|
||||
def web_status(self):
|
||||
def web_status(self) -> Status:
|
||||
""":class:`Status`: The member's status on the web client, if applicable."""
|
||||
return try_enum(Status, self._client_status.get('web', 'offline'))
|
||||
return try_enum(Status, self._client_status.get("web", "offline"))
|
||||
|
||||
def is_on_mobile(self):
|
||||
def is_on_mobile(self) -> bool:
|
||||
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
|
||||
return 'mobile' in self._client_status
|
||||
return "mobile" in self._client_status
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
def colour(self) -> Colour:
|
||||
""":class:`Colour`: A property that returns a colour denoting the rendered colour
|
||||
for the member. If the default colour is the one rendered then an instance
|
||||
of :meth:`Colour.default` is returned.
|
||||
@@ -384,7 +459,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
There is an alias for this named :attr:`color`.
|
||||
"""
|
||||
|
||||
roles = self.roles[1:] # remove @everyone
|
||||
roles = self.roles[1:] # remove @everyone
|
||||
|
||||
# highest order of the colour is the one that gets rendered.
|
||||
# if the highest is the default colour then the next one with a colour
|
||||
@@ -395,7 +470,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return Colour.default()
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
def color(self) -> Colour:
|
||||
""":class:`Colour`: A property that returns a color denoting the rendered color for
|
||||
the member. If the default color is the one rendered then an instance of :meth:`Colour.default`
|
||||
is returned.
|
||||
@@ -405,7 +480,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return self.colour
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
def roles(self) -> List[Role]:
|
||||
"""List[:class:`Role`]: A :class:`list` of :class:`Role` that the member belongs to. Note
|
||||
that the first element of this list is always the default '@everyone'
|
||||
role.
|
||||
@@ -423,14 +498,14 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return result
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: Returns a string that allows you to mention the member."""
|
||||
if self.nick:
|
||||
return f'<@!{self._user.id}>'
|
||||
return f'<@{self._user.id}>'
|
||||
return f"<@!{self._user.id}>"
|
||||
return f"<@{self._user.id}>"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
def display_name(self) -> str:
|
||||
""":class:`str`: Returns the user's display name.
|
||||
|
||||
For regular users this is just their username, but
|
||||
@@ -440,10 +515,39 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return self.nick or self.name
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
"""Union[:class:`BaseActivity`, :class:`Spotify`]: Returns the primary
|
||||
def display_avatar(self) -> Asset:
|
||||
""":class:`Asset`: Returns the member's display avatar.
|
||||
|
||||
For regular members this is just their avatar, but
|
||||
if they have a guild specific avatar then that
|
||||
is returned instead.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return self.guild_avatar or self._user.avatar or self._user.default_avatar
|
||||
|
||||
@property
|
||||
def guild_avatar(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild avatar
|
||||
the member has. If unavailable, ``None`` is returned.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._avatar is None:
|
||||
return None
|
||||
return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar)
|
||||
|
||||
@property
|
||||
def activity(self) -> Optional[ActivityTypes]:
|
||||
"""Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary
|
||||
activity the user is currently doing. Could be ``None`` if no activity is being done.
|
||||
|
||||
.. note::
|
||||
|
||||
Due to a Discord API limitation, this may be ``None`` if
|
||||
the user is listening to a song on Spotify with a title longer
|
||||
than 128 characters. See :issue:`1738` for more information.
|
||||
|
||||
.. note::
|
||||
|
||||
A user may have multiple activities, these can be accessed under :attr:`activities`.
|
||||
@@ -451,7 +555,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
if self.activities:
|
||||
return self.activities[0]
|
||||
|
||||
def mentioned_in(self, message):
|
||||
def mentioned_in(self, message: Message) -> bool:
|
||||
"""Checks if the member is mentioned in the specified message.
|
||||
|
||||
Parameters
|
||||
@@ -472,29 +576,8 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
return any(self._roles.has(role.id) for role in message.role_mentions)
|
||||
|
||||
def permissions_in(self, channel):
|
||||
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
|
||||
|
||||
Basically equivalent to:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
channel.permissions_for(self)
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
channel: :class:`abc.GuildChannel`
|
||||
The channel to check your permissions for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Permissions`
|
||||
The resolved permissions for the member.
|
||||
"""
|
||||
return channel.permissions_for(self)
|
||||
|
||||
@property
|
||||
def top_role(self):
|
||||
def top_role(self) -> Role:
|
||||
""":class:`Role`: Returns the member's highest role.
|
||||
|
||||
This is useful for figuring where a member stands in the role
|
||||
@@ -507,14 +590,13 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return max(guild.get_role(rid) or guild.default_role for rid in self._roles)
|
||||
|
||||
@property
|
||||
def guild_permissions(self):
|
||||
def guild_permissions(self) -> Permissions:
|
||||
""":class:`Permissions`: Returns the member's guild permissions.
|
||||
|
||||
This only takes into consideration the guild permissions
|
||||
and not most of the implied permissions or any of the
|
||||
channel permission overwrites. For 100% accurate permission
|
||||
calculation, please use either :meth:`permissions_in` or
|
||||
:meth:`abc.GuildChannel.permissions_for`.
|
||||
calculation, please use :meth:`abc.GuildChannel.permissions_for`.
|
||||
|
||||
This does take into consideration guild ownership and the
|
||||
administrator implication.
|
||||
@@ -533,32 +615,47 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return base
|
||||
|
||||
@property
|
||||
def voice(self):
|
||||
def voice(self) -> Optional[VoiceState]:
|
||||
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
|
||||
return self.guild._voice_state_for(self._user.id)
|
||||
|
||||
async def ban(self, **kwargs):
|
||||
async def ban(
|
||||
self,
|
||||
*,
|
||||
delete_message_days: Literal[0, 1, 2, 3, 4, 5, 6, 7] = 1,
|
||||
reason: Optional[str] = None,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Bans this member. Equivalent to :meth:`Guild.ban`.
|
||||
"""
|
||||
await self.guild.ban(self, **kwargs)
|
||||
await self.guild.ban(self, reason=reason, delete_message_days=delete_message_days)
|
||||
|
||||
async def unban(self, *, reason=None):
|
||||
async def unban(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Unbans this member. Equivalent to :meth:`Guild.unban`.
|
||||
"""
|
||||
await self.guild.unban(self, reason=reason)
|
||||
|
||||
async def kick(self, *, reason=None):
|
||||
async def kick(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Kicks this member. Equivalent to :meth:`Guild.kick`.
|
||||
"""
|
||||
await self.guild.kick(self, reason=reason)
|
||||
|
||||
async def edit(self, *, reason=None, **fields):
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
nick: Optional[str] = MISSING,
|
||||
mute: bool = MISSING,
|
||||
deafen: bool = MISSING,
|
||||
suppress: bool = MISSING,
|
||||
roles: List[discord.abc.Snowflake] = MISSING,
|
||||
voice_channel: Optional[VocalGuildChannel] = MISSING,
|
||||
reason: Optional[str] = None,
|
||||
) -> Optional[Member]:
|
||||
"""|coro|
|
||||
|
||||
Edits the member's data.
|
||||
@@ -584,6 +681,9 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
.. versionchanged:: 1.1
|
||||
Can now pass ``None`` to ``voice_channel`` to kick a member from voice.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The newly member is now optionally returned, if applicable.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
nick: Optional[:class:`str`]
|
||||
@@ -597,7 +697,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
roles: Optional[List[:class:`Role`]]
|
||||
roles: List[:class:`Role`]
|
||||
The member's new list of roles. This *replaces* the roles.
|
||||
voice_channel: Optional[:class:`VoiceChannel`]
|
||||
The voice channel to move the member to.
|
||||
@@ -611,69 +711,58 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
You do not have the proper permissions to the action requested.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`.Member`]
|
||||
The newly updated member, if applicable. This is only returned
|
||||
when certain fields are updated.
|
||||
"""
|
||||
http = self._state.http
|
||||
guild_id = self.guild.id
|
||||
me = self._state.self_id == self.id
|
||||
payload = {}
|
||||
payload: Dict[str, Any] = {}
|
||||
|
||||
try:
|
||||
nick = fields['nick']
|
||||
except KeyError:
|
||||
# nick not present so...
|
||||
pass
|
||||
else:
|
||||
nick = nick or ''
|
||||
if nick is not MISSING:
|
||||
nick = nick or ""
|
||||
if me:
|
||||
await http.change_my_nickname(guild_id, nick, reason=reason)
|
||||
else:
|
||||
payload['nick'] = nick
|
||||
payload["nick"] = nick
|
||||
|
||||
deafen = fields.get('deafen')
|
||||
if deafen is not None:
|
||||
payload['deaf'] = deafen
|
||||
if deafen is not MISSING:
|
||||
payload["deaf"] = deafen
|
||||
|
||||
mute = fields.get('mute')
|
||||
if mute is not None:
|
||||
payload['mute'] = mute
|
||||
if mute is not MISSING:
|
||||
payload["mute"] = mute
|
||||
|
||||
suppress = fields.get('suppress')
|
||||
if suppress is not None:
|
||||
if suppress is not MISSING:
|
||||
voice_state_payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'suppress': suppress,
|
||||
"channel_id": self.voice.channel.id,
|
||||
"suppress": suppress,
|
||||
}
|
||||
|
||||
if suppress or self.bot:
|
||||
voice_state_payload['request_to_speak_timestamp'] = None
|
||||
voice_state_payload["request_to_speak_timestamp"] = None
|
||||
|
||||
if me:
|
||||
await http.edit_my_voice_state(guild_id, voice_state_payload)
|
||||
else:
|
||||
if not suppress:
|
||||
voice_state_payload['request_to_speak_timestamp'] = datetime.datetime.utcnow().isoformat()
|
||||
voice_state_payload["request_to_speak_timestamp"] = datetime.datetime.utcnow().isoformat()
|
||||
await http.edit_voice_state(guild_id, self.id, voice_state_payload)
|
||||
|
||||
try:
|
||||
vc = fields['voice_channel']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
payload['channel_id'] = vc and vc.id
|
||||
if voice_channel is not MISSING:
|
||||
payload["channel_id"] = voice_channel and voice_channel.id
|
||||
|
||||
try:
|
||||
roles = fields['roles']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
payload['roles'] = tuple(r.id for r in roles)
|
||||
if roles is not MISSING:
|
||||
payload["roles"] = tuple(r.id for r in roles)
|
||||
|
||||
if payload:
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
data = await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
return Member(data=data, guild=self.guild, state=self._state)
|
||||
|
||||
# TODO: wait for WS event for modify-in-place behaviour
|
||||
|
||||
async def request_to_speak(self):
|
||||
async def request_to_speak(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Request to speak in the connected channel.
|
||||
@@ -695,17 +784,17 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
The operation failed.
|
||||
"""
|
||||
payload = {
|
||||
'channel_id': self.voice.channel.id,
|
||||
'request_to_speak_timestamp': datetime.datetime.utcnow().isoformat(),
|
||||
"channel_id": self.voice.channel.id,
|
||||
"request_to_speak_timestamp": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
if self._state.self_id != self.id:
|
||||
payload['suppress'] = False
|
||||
payload["suppress"] = False
|
||||
await self._state.http.edit_voice_state(self.guild.id, self.id, payload)
|
||||
else:
|
||||
await self._state.http.edit_my_voice_state(self.guild.id, payload)
|
||||
|
||||
async def move_to(self, channel, *, reason=None):
|
||||
async def move_to(self, channel: VocalGuildChannel, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Moves a member to a new voice channel (they must be connected first).
|
||||
@@ -728,7 +817,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
"""
|
||||
await self.edit(voice_channel=channel, reason=reason)
|
||||
|
||||
async def add_roles(self, *roles, reason=None, atomic=True):
|
||||
async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None:
|
||||
r"""|coro|
|
||||
|
||||
Gives the member a number of :class:`Role`\s.
|
||||
@@ -767,7 +856,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
for role in roles:
|
||||
await req(guild_id, user_id, role.id, reason=reason)
|
||||
|
||||
async def remove_roles(self, *roles, reason=None, atomic=True):
|
||||
async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None:
|
||||
r"""|coro|
|
||||
|
||||
Removes :class:`Role`\s from this member.
|
||||
@@ -797,7 +886,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
"""
|
||||
|
||||
if not atomic:
|
||||
new_roles = [Object(id=r.id) for r in self.roles[1:]] # remove @everyone
|
||||
new_roles = [Object(id=r.id) for r in self.roles[1:]] # remove @everyone
|
||||
for role in roles:
|
||||
try:
|
||||
new_roles.remove(Object(id=role.id))
|
||||
@@ -811,3 +900,20 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
user_id = self.id
|
||||
for role in roles:
|
||||
await req(guild_id, user_id, role.id, reason=reason)
|
||||
|
||||
def get_role(self, role_id: int, /) -> Optional[Role]:
|
||||
"""Returns a role with the given ID from roles which the member has.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
role_id: :class:`int`
|
||||
The ID to search for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Role`]
|
||||
The role or ``None`` if not found in the member's roles.
|
||||
"""
|
||||
return self.guild.get_role(role_id) if self._roles.has(role_id) else None
|
||||
|
||||
@@ -22,13 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'AllowedMentions',
|
||||
)
|
||||
from __future__ import annotations
|
||||
from typing import Type, TypeVar, Union, List, TYPE_CHECKING, Any, Union
|
||||
|
||||
__all__ = ("AllowedMentions",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.message import AllowedMentions as AllowedMentionsPayload
|
||||
from .abc import Snowflake
|
||||
|
||||
|
||||
class _FakeBool:
|
||||
def __repr__(self):
|
||||
return 'True'
|
||||
return "True"
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is True
|
||||
@@ -36,7 +42,11 @@ class _FakeBool:
|
||||
def __bool__(self):
|
||||
return True
|
||||
|
||||
default = _FakeBool()
|
||||
|
||||
default: Any = _FakeBool()
|
||||
|
||||
A = TypeVar("A", bound="AllowedMentions")
|
||||
|
||||
|
||||
class AllowedMentions:
|
||||
"""A class that represents what mentions are allowed in a message.
|
||||
@@ -68,16 +78,23 @@ class AllowedMentions:
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
__slots__ = ('everyone', 'users', 'roles', 'replied_user')
|
||||
__slots__ = ("everyone", "users", "roles", "replied_user")
|
||||
|
||||
def __init__(self, *, everyone=default, users=default, roles=default, replied_user=default):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
everyone: bool = default,
|
||||
users: Union[bool, List[Snowflake]] = default,
|
||||
roles: Union[bool, List[Snowflake]] = default,
|
||||
replied_user: bool = default,
|
||||
):
|
||||
self.everyone = everyone
|
||||
self.users = users
|
||||
self.roles = roles
|
||||
self.replied_user = replied_user
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[A]) -> A:
|
||||
"""A factory method that returns a :class:`AllowedMentions` with all fields explicitly set to ``True``
|
||||
|
||||
.. versionadded:: 1.5
|
||||
@@ -85,37 +102,37 @@ class AllowedMentions:
|
||||
return cls(everyone=True, users=True, roles=True, replied_user=True)
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[A]) -> A:
|
||||
"""A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
return cls(everyone=False, users=False, roles=False, replied_user=False)
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> AllowedMentionsPayload:
|
||||
parse = []
|
||||
data = {}
|
||||
|
||||
if self.everyone:
|
||||
parse.append('everyone')
|
||||
parse.append("everyone")
|
||||
|
||||
if self.users == True:
|
||||
parse.append('users')
|
||||
parse.append("users")
|
||||
elif self.users != False:
|
||||
data['users'] = [x.id for x in self.users]
|
||||
data["users"] = [x.id for x in self.users]
|
||||
|
||||
if self.roles == True:
|
||||
parse.append('roles')
|
||||
parse.append("roles")
|
||||
elif self.roles != False:
|
||||
data['roles'] = [x.id for x in self.roles]
|
||||
data["roles"] = [x.id for x in self.roles]
|
||||
|
||||
if self.replied_user:
|
||||
data['replied_user'] = True
|
||||
data["replied_user"] = True
|
||||
|
||||
data['parse'] = parse
|
||||
return data
|
||||
data["parse"] = parse
|
||||
return data # type: ignore
|
||||
|
||||
def merge(self, other):
|
||||
def merge(self, other: AllowedMentions) -> AllowedMentions:
|
||||
# Creates a new AllowedMentions by merging from another one.
|
||||
# Merge is done by using the 'self' values unless explicitly
|
||||
# overridden by the 'other' values.
|
||||
@@ -125,5 +142,8 @@ class AllowedMentions:
|
||||
replied_user = self.replied_user if other.replied_user is default else other.replied_user
|
||||
return AllowedMentions(everyone=everyone, roles=roles, users=users, replied_user=replied_user)
|
||||
|
||||
def __repr__(self):
|
||||
return '{0.__class__.__qualname__}(everyone={0.everyone}, users={0.users}, roles={0.roles}, replied_user={0.replied_user})'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__name__}(everyone={self.everyone}, "
|
||||
f"users={self.users}, roles={self.roles}, replied_user={self.replied_user})"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,23 +23,30 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'EqualityComparable',
|
||||
'Hashable',
|
||||
"EqualityComparable",
|
||||
"Hashable",
|
||||
)
|
||||
|
||||
|
||||
class EqualityComparable:
|
||||
__slots__ = ()
|
||||
|
||||
def __eq__(self, other):
|
||||
id: int
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__) and other.id == self.id
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: object) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
return other.id != self.id
|
||||
return True
|
||||
|
||||
|
||||
class Hashable(EqualityComparable):
|
||||
__slots__ = ()
|
||||
|
||||
def __hash__(self):
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self.id >> 22
|
||||
|
||||
@@ -22,13 +22,25 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import utils
|
||||
from .mixins import Hashable
|
||||
|
||||
__all__ = (
|
||||
'Object',
|
||||
from typing import (
|
||||
SupportsInt,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
||||
SupportsIntCast = Union[SupportsInt, str, bytes, bytearray]
|
||||
|
||||
__all__ = ("Object",)
|
||||
|
||||
|
||||
class Object(Hashable):
|
||||
"""Represents a generic Discord object.
|
||||
|
||||
@@ -57,24 +69,28 @@ class Object(Hashable):
|
||||
|
||||
Returns the object's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the object's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The ID of the object.
|
||||
"""
|
||||
|
||||
def __init__(self, id):
|
||||
def __init__(self, id: SupportsIntCast):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
raise TypeError(f'id parameter must be convertable to int not {id.__class__!r}') from None
|
||||
raise TypeError(f"id parameter must be convertable to int not {id.__class__!r}") from None
|
||||
else:
|
||||
self.id = id
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Object id={self.id!r}>'
|
||||
def __repr__(self) -> str:
|
||||
return f"<Object id={self.id!r}>"
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the snowflake's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@@ -22,40 +22,54 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar, IO, Generator, Tuple, Optional
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'OggError',
|
||||
'OggPage',
|
||||
'OggStream',
|
||||
"OggError",
|
||||
"OggPage",
|
||||
"OggStream",
|
||||
)
|
||||
|
||||
|
||||
class OggError(DiscordException):
|
||||
"""An exception that is thrown for Ogg stream parsing errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# https://tools.ietf.org/html/rfc3533
|
||||
# https://tools.ietf.org/html/rfc7845
|
||||
|
||||
class OggPage:
|
||||
_header = struct.Struct('<xBQIIIB')
|
||||
|
||||
def __init__(self, stream):
|
||||
class OggPage:
|
||||
_header: ClassVar[struct.Struct] = struct.Struct("<xBQIIIB")
|
||||
if TYPE_CHECKING:
|
||||
flag: int
|
||||
gran_pos: int
|
||||
serial: int
|
||||
pagenum: int
|
||||
crc: int
|
||||
segnum: int
|
||||
|
||||
def __init__(self, stream: IO[bytes]) -> None:
|
||||
try:
|
||||
header = stream.read(struct.calcsize(self._header.format))
|
||||
|
||||
self.flag, self.gran_pos, self.serial, \
|
||||
self.pagenum, self.crc, self.segnum = self._header.unpack(header)
|
||||
self.flag, self.gran_pos, self.serial, self.pagenum, self.crc, self.segnum = self._header.unpack(header)
|
||||
|
||||
self.segtable = stream.read(self.segnum)
|
||||
bodylen = sum(struct.unpack('B'*self.segnum, self.segtable))
|
||||
self.data = stream.read(bodylen)
|
||||
self.segtable: bytes = stream.read(self.segnum)
|
||||
bodylen = sum(struct.unpack("B" * self.segnum, self.segtable))
|
||||
self.data: bytes = stream.read(bodylen)
|
||||
except Exception:
|
||||
raise OggError('bad data stream') from None
|
||||
raise OggError("bad data stream") from None
|
||||
|
||||
def iter_packets(self):
|
||||
def iter_packets(self) -> Generator[Tuple[bytes, bool], None, None]:
|
||||
packetlen = offset = 0
|
||||
partial = True
|
||||
|
||||
@@ -65,7 +79,7 @@ class OggPage:
|
||||
partial = True
|
||||
else:
|
||||
packetlen += seg
|
||||
yield self.data[offset:offset+packetlen], True
|
||||
yield self.data[offset : offset + packetlen], True
|
||||
offset += packetlen
|
||||
packetlen = 0
|
||||
partial = False
|
||||
@@ -73,30 +87,31 @@ class OggPage:
|
||||
if partial:
|
||||
yield self.data[offset:], False
|
||||
|
||||
class OggStream:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
def _next_page(self):
|
||||
class OggStream:
|
||||
def __init__(self, stream: IO[bytes]) -> None:
|
||||
self.stream: IO[bytes] = stream
|
||||
|
||||
def _next_page(self) -> Optional[OggPage]:
|
||||
head = self.stream.read(4)
|
||||
if head == b'OggS':
|
||||
if head == b"OggS":
|
||||
return OggPage(self.stream)
|
||||
elif not head:
|
||||
return None
|
||||
else:
|
||||
raise OggError('invalid header magic')
|
||||
raise OggError("invalid header magic")
|
||||
|
||||
def _iter_pages(self):
|
||||
def _iter_pages(self) -> Generator[OggPage, None, None]:
|
||||
page = self._next_page()
|
||||
while page:
|
||||
yield page
|
||||
page = self._next_page()
|
||||
|
||||
def iter_packets(self):
|
||||
partial = b''
|
||||
def iter_packets(self) -> Generator[bytes, None, None]:
|
||||
partial = b""
|
||||
for page in self._iter_pages():
|
||||
for data, complete in page.iter_packets():
|
||||
partial += data
|
||||
if complete:
|
||||
yield partial
|
||||
partial = b''
|
||||
partial = b""
|
||||
|
||||
294
discord/opus.py
294
discord/opus.py
@@ -22,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple, TypedDict, Any, TYPE_CHECKING, Callable, TypeVar, Literal, Optional, overload
|
||||
|
||||
import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
@@ -31,134 +35,157 @@ import os.path
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from .errors import DiscordException
|
||||
from .errors import DiscordException, InvalidArgument
|
||||
|
||||
if TYPE_CHECKING:
|
||||
T = TypeVar("T")
|
||||
BAND_CTL = Literal["narrow", "medium", "wide", "superwide", "full"]
|
||||
SIGNAL_CTL = Literal["auto", "voice", "music"]
|
||||
|
||||
|
||||
class BandCtl(TypedDict):
|
||||
narrow: int
|
||||
medium: int
|
||||
wide: int
|
||||
superwide: int
|
||||
full: int
|
||||
|
||||
|
||||
class SignalCtl(TypedDict):
|
||||
auto: int
|
||||
voice: int
|
||||
music: int
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Encoder',
|
||||
'OpusError',
|
||||
'OpusNotLoaded',
|
||||
"Encoder",
|
||||
"OpusError",
|
||||
"OpusNotLoaded",
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
|
||||
c_float_ptr = ctypes.POINTER(ctypes.c_float)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class EncoderStruct(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
class DecoderStruct(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
EncoderStructPtr = ctypes.POINTER(EncoderStruct)
|
||||
DecoderStructPtr = ctypes.POINTER(DecoderStruct)
|
||||
|
||||
## Some constants from opus_defines.h
|
||||
# Error codes
|
||||
OK = 0
|
||||
OK = 0
|
||||
BAD_ARG = -1
|
||||
|
||||
# Encoder CTLs
|
||||
APPLICATION_AUDIO = 2049
|
||||
APPLICATION_VOIP = 2048
|
||||
APPLICATION_AUDIO = 2049
|
||||
APPLICATION_VOIP = 2048
|
||||
APPLICATION_LOWDELAY = 2051
|
||||
|
||||
CTL_SET_BITRATE = 4002
|
||||
CTL_SET_BANDWIDTH = 4008
|
||||
CTL_SET_FEC = 4012
|
||||
CTL_SET_PLP = 4014
|
||||
CTL_SET_SIGNAL = 4024
|
||||
CTL_SET_BITRATE = 4002
|
||||
CTL_SET_BANDWIDTH = 4008
|
||||
CTL_SET_FEC = 4012
|
||||
CTL_SET_PLP = 4014
|
||||
CTL_SET_SIGNAL = 4024
|
||||
|
||||
# Decoder CTLs
|
||||
CTL_SET_GAIN = 4034
|
||||
CTL_SET_GAIN = 4034
|
||||
CTL_LAST_PACKET_DURATION = 4039
|
||||
|
||||
band_ctl = {
|
||||
'narrow': 1101,
|
||||
'medium': 1102,
|
||||
'wide': 1103,
|
||||
'superwide': 1104,
|
||||
'full': 1105,
|
||||
band_ctl: BandCtl = {
|
||||
"narrow": 1101,
|
||||
"medium": 1102,
|
||||
"wide": 1103,
|
||||
"superwide": 1104,
|
||||
"full": 1105,
|
||||
}
|
||||
|
||||
signal_ctl = {
|
||||
'auto': -1000,
|
||||
'voice': 3001,
|
||||
'music': 3002,
|
||||
signal_ctl: SignalCtl = {
|
||||
"auto": -1000,
|
||||
"voice": 3001,
|
||||
"music": 3002,
|
||||
}
|
||||
|
||||
def _err_lt(result, func, args):
|
||||
|
||||
def _err_lt(result: int, func: Callable, args: List) -> int:
|
||||
if result < OK:
|
||||
log.info('error has happened in %s', func.__name__)
|
||||
_log.info("error has happened in %s", func.__name__)
|
||||
raise OpusError(result)
|
||||
return result
|
||||
|
||||
def _err_ne(result, func, args):
|
||||
|
||||
def _err_ne(result: T, func: Callable, args: List) -> T:
|
||||
ret = args[-1]._obj
|
||||
if ret.value != OK:
|
||||
log.info('error has happened in %s', func.__name__)
|
||||
_log.info("error has happened in %s", func.__name__)
|
||||
raise OpusError(ret.value)
|
||||
return result
|
||||
|
||||
|
||||
# A list of exported functions.
|
||||
# The first argument is obviously the name.
|
||||
# The second one are the types of arguments it takes.
|
||||
# The third is the result type.
|
||||
# The fourth is the error handler.
|
||||
exported_functions = [
|
||||
exported_functions: List[Tuple[Any, ...]] = [
|
||||
# Generic
|
||||
('opus_get_version_string',
|
||||
None, ctypes.c_char_p, None),
|
||||
('opus_strerror',
|
||||
[ctypes.c_int], ctypes.c_char_p, None),
|
||||
|
||||
("opus_get_version_string", None, ctypes.c_char_p, None),
|
||||
("opus_strerror", [ctypes.c_int], ctypes.c_char_p, None),
|
||||
# Encoder functions
|
||||
('opus_encoder_get_size',
|
||||
[ctypes.c_int], ctypes.c_int, None),
|
||||
('opus_encoder_create',
|
||||
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne),
|
||||
('opus_encode',
|
||||
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
|
||||
('opus_encode_float',
|
||||
[EncoderStructPtr, c_float_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
|
||||
('opus_encoder_ctl',
|
||||
None, ctypes.c_int32, _err_lt),
|
||||
('opus_encoder_destroy',
|
||||
[EncoderStructPtr], None, None),
|
||||
|
||||
("opus_encoder_get_size", [ctypes.c_int], ctypes.c_int, None),
|
||||
("opus_encoder_create", [ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne),
|
||||
(
|
||||
"opus_encode",
|
||||
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32],
|
||||
ctypes.c_int32,
|
||||
_err_lt,
|
||||
),
|
||||
(
|
||||
"opus_encode_float",
|
||||
[EncoderStructPtr, c_float_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32],
|
||||
ctypes.c_int32,
|
||||
_err_lt,
|
||||
),
|
||||
("opus_encoder_ctl", None, ctypes.c_int32, _err_lt),
|
||||
("opus_encoder_destroy", [EncoderStructPtr], None, None),
|
||||
# Decoder functions
|
||||
('opus_decoder_get_size',
|
||||
[ctypes.c_int], ctypes.c_int, None),
|
||||
('opus_decoder_create',
|
||||
[ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
|
||||
('opus_decode',
|
||||
("opus_decoder_get_size", [ctypes.c_int], ctypes.c_int, None),
|
||||
("opus_decoder_create", [ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
|
||||
(
|
||||
"opus_decode",
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_int16_ptr, ctypes.c_int, ctypes.c_int],
|
||||
ctypes.c_int, _err_lt),
|
||||
('opus_decode_float',
|
||||
ctypes.c_int,
|
||||
_err_lt,
|
||||
),
|
||||
(
|
||||
"opus_decode_float",
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_float_ptr, ctypes.c_int, ctypes.c_int],
|
||||
ctypes.c_int, _err_lt),
|
||||
('opus_decoder_ctl',
|
||||
None, ctypes.c_int32, _err_lt),
|
||||
('opus_decoder_destroy',
|
||||
[DecoderStructPtr], None, None),
|
||||
('opus_decoder_get_nb_samples',
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
|
||||
|
||||
ctypes.c_int,
|
||||
_err_lt,
|
||||
),
|
||||
("opus_decoder_ctl", None, ctypes.c_int32, _err_lt),
|
||||
("opus_decoder_destroy", [DecoderStructPtr], None, None),
|
||||
("opus_decoder_get_nb_samples", [DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
|
||||
# Packet functions
|
||||
('opus_packet_get_bandwidth',
|
||||
[ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_nb_channels',
|
||||
[ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_nb_frames',
|
||||
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_samples_per_frame',
|
||||
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
("opus_packet_get_bandwidth", [ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
("opus_packet_get_nb_channels", [ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
("opus_packet_get_nb_frames", [ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
("opus_packet_get_samples_per_frame", [ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
]
|
||||
|
||||
def libopus_loader(name):
|
||||
|
||||
def libopus_loader(name: str) -> Any:
|
||||
# create the library...
|
||||
lib = ctypes.cdll.LoadLibrary(name)
|
||||
|
||||
@@ -178,27 +205,29 @@ def libopus_loader(name):
|
||||
if item[3]:
|
||||
func.errcheck = item[3]
|
||||
except KeyError:
|
||||
log.exception("Error assigning check function to %s", func)
|
||||
_log.exception("Error assigning check function to %s", func)
|
||||
|
||||
return lib
|
||||
|
||||
def _load_default():
|
||||
|
||||
def _load_default() -> bool:
|
||||
global _lib
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == "win32":
|
||||
_basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
_bitness = struct.calcsize('P') * 8
|
||||
_target = 'x64' if _bitness > 32 else 'x86'
|
||||
_filename = os.path.join(_basedir, 'bin', f'libopus-0.{_target}.dll')
|
||||
_bitness = struct.calcsize("P") * 8
|
||||
_target = "x64" if _bitness > 32 else "x86"
|
||||
_filename = os.path.join(_basedir, "bin", f"libopus-0.{_target}.dll")
|
||||
_lib = libopus_loader(_filename)
|
||||
else:
|
||||
_lib = libopus_loader(ctypes.util.find_library('opus'))
|
||||
_lib = libopus_loader(ctypes.util.find_library("opus"))
|
||||
except Exception:
|
||||
_lib = None
|
||||
|
||||
return _lib is not None
|
||||
|
||||
def load_opus(name):
|
||||
|
||||
def load_opus(name: str) -> None:
|
||||
"""Loads the libopus shared library for use with voice.
|
||||
|
||||
If this function is not called then the library uses the function
|
||||
@@ -236,7 +265,8 @@ def load_opus(name):
|
||||
global _lib
|
||||
_lib = libopus_loader(name)
|
||||
|
||||
def is_loaded():
|
||||
|
||||
def is_loaded() -> bool:
|
||||
"""Function to check if opus lib is successfully loaded either
|
||||
via the :func:`ctypes.util.find_library` call of :func:`load_opus`.
|
||||
|
||||
@@ -250,6 +280,7 @@ def is_loaded():
|
||||
global _lib
|
||||
return _lib is not None
|
||||
|
||||
|
||||
class OpusError(DiscordException):
|
||||
"""An exception that is thrown for libopus related errors.
|
||||
|
||||
@@ -259,21 +290,24 @@ class OpusError(DiscordException):
|
||||
The error code returned.
|
||||
"""
|
||||
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
msg = _lib.opus_strerror(self.code).decode('utf-8')
|
||||
log.info('"%s" has happened', msg)
|
||||
def __init__(self, code: int):
|
||||
self.code: int = code
|
||||
msg = _lib.opus_strerror(self.code).decode("utf-8")
|
||||
_log.info('"%s" has happened', msg)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class OpusNotLoaded(DiscordException):
|
||||
"""An exception that is thrown for when libopus is not loaded."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class _OpusStruct:
|
||||
SAMPLING_RATE = 48000
|
||||
CHANNELS = 2
|
||||
FRAME_LENGTH = 20 # in milliseconds
|
||||
SAMPLE_SIZE = struct.calcsize('h') * CHANNELS
|
||||
SAMPLE_SIZE = struct.calcsize("h") * CHANNELS
|
||||
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
|
||||
|
||||
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
|
||||
@@ -283,95 +317,101 @@ class _OpusStruct:
|
||||
if not is_loaded() and not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
return _lib.opus_get_version_string().decode('utf-8')
|
||||
return _lib.opus_get_version_string().decode("utf-8")
|
||||
|
||||
|
||||
class Encoder(_OpusStruct):
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
def __init__(self, application: int = APPLICATION_AUDIO):
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self.application = application
|
||||
self._state = self._create_state()
|
||||
self.application: int = application
|
||||
self._state: EncoderStruct = self._create_state()
|
||||
self.set_bitrate(128)
|
||||
self.set_fec(True)
|
||||
self.set_expected_packet_loss_percent(0.15)
|
||||
self.set_bandwidth('full')
|
||||
self.set_signal_type('auto')
|
||||
self.set_bandwidth("full")
|
||||
self.set_signal_type("auto")
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, '_state'):
|
||||
def __del__(self) -> None:
|
||||
if hasattr(self, "_state"):
|
||||
_lib.opus_encoder_destroy(self._state)
|
||||
self._state = None
|
||||
# This is a destructor, so it's okay to assign None
|
||||
self._state = None # type: ignore
|
||||
|
||||
def _create_state(self):
|
||||
def _create_state(self) -> EncoderStruct:
|
||||
ret = ctypes.c_int()
|
||||
return _lib.opus_encoder_create(self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret))
|
||||
|
||||
def set_bitrate(self, kbps):
|
||||
def set_bitrate(self, kbps: int) -> int:
|
||||
kbps = min(512, max(16, int(kbps)))
|
||||
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024)
|
||||
return kbps
|
||||
|
||||
def set_bandwidth(self, req):
|
||||
def set_bandwidth(self, req: BAND_CTL) -> None:
|
||||
if req not in band_ctl:
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}')
|
||||
|
||||
k = band_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
|
||||
|
||||
def set_signal_type(self, req):
|
||||
def set_signal_type(self, req: SIGNAL_CTL) -> None:
|
||||
if req not in signal_ctl:
|
||||
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
|
||||
|
||||
k = signal_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
|
||||
|
||||
def set_fec(self, enabled=True):
|
||||
def set_fec(self, enabled: bool = True) -> None:
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_FEC, 1 if enabled else 0)
|
||||
|
||||
def set_expected_packet_loss_percent(self, percentage):
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100))))
|
||||
def set_expected_packet_loss_percent(self, percentage: float) -> None:
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100)))) # type: ignore
|
||||
|
||||
def encode(self, pcm, frame_size):
|
||||
def encode(self, pcm: bytes, frame_size: int) -> bytes:
|
||||
max_data_bytes = len(pcm)
|
||||
pcm = ctypes.cast(pcm, c_int16_ptr)
|
||||
# bytes can be used to reference pointer
|
||||
pcm_ptr = ctypes.cast(pcm, c_int16_ptr) # type: ignore
|
||||
data = (ctypes.c_char * max_data_bytes)()
|
||||
|
||||
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
|
||||
ret = _lib.opus_encode(self._state, pcm_ptr, frame_size, data, max_data_bytes)
|
||||
|
||||
# array can be initialized with bytes but mypy doesn't know
|
||||
return array.array("b", data[:ret]).tobytes() # type: ignore
|
||||
|
||||
return array.array('b', data[:ret]).tobytes()
|
||||
|
||||
class Decoder(_OpusStruct):
|
||||
def __init__(self):
|
||||
_OpusStruct.get_opus_version()
|
||||
|
||||
self._state = self._create_state()
|
||||
self._state: DecoderStruct = self._create_state()
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, '_state'):
|
||||
def __del__(self) -> None:
|
||||
if hasattr(self, "_state"):
|
||||
_lib.opus_decoder_destroy(self._state)
|
||||
self._state = None
|
||||
# This is a destructor, so it's okay to assign None
|
||||
self._state = None # type: ignore
|
||||
|
||||
def _create_state(self):
|
||||
def _create_state(self) -> DecoderStruct:
|
||||
ret = ctypes.c_int()
|
||||
return _lib.opus_decoder_create(self.SAMPLING_RATE, self.CHANNELS, ctypes.byref(ret))
|
||||
|
||||
@staticmethod
|
||||
def packet_get_nb_frames(data):
|
||||
def packet_get_nb_frames(data: bytes) -> int:
|
||||
"""Gets the number of frames in an Opus packet"""
|
||||
return _lib.opus_packet_get_nb_frames(data, len(data))
|
||||
|
||||
@staticmethod
|
||||
def packet_get_nb_channels(data):
|
||||
def packet_get_nb_channels(data: bytes) -> int:
|
||||
"""Gets the number of channels in an Opus packet"""
|
||||
return _lib.opus_packet_get_nb_channels(data)
|
||||
|
||||
@classmethod
|
||||
def packet_get_samples_per_frame(cls, data):
|
||||
def packet_get_samples_per_frame(cls, data: bytes) -> int:
|
||||
"""Gets the number of samples per frame from an Opus packet"""
|
||||
return _lib.opus_packet_get_samples_per_frame(data, cls.SAMPLING_RATE)
|
||||
|
||||
def _set_gain(self, adjustment):
|
||||
def _set_gain(self, adjustment: int) -> int:
|
||||
"""Configures decoder gain adjustment.
|
||||
|
||||
Scales the decoded output by a factor specified in Q8 dB units.
|
||||
@@ -383,26 +423,34 @@ class Decoder(_OpusStruct):
|
||||
"""
|
||||
return _lib.opus_decoder_ctl(self._state, CTL_SET_GAIN, adjustment)
|
||||
|
||||
def set_gain(self, dB):
|
||||
def set_gain(self, dB: float) -> int:
|
||||
"""Sets the decoder gain in dB, from -128 to 128."""
|
||||
|
||||
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
|
||||
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
|
||||
return self._set_gain(dB_Q8)
|
||||
|
||||
def set_volume(self, mult):
|
||||
def set_volume(self, mult: float) -> int:
|
||||
"""Sets the output volume as a float percent, i.e. 0.5 for 50%, 1.75 for 175%, etc."""
|
||||
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
|
||||
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
|
||||
|
||||
def _get_last_packet_duration(self):
|
||||
def _get_last_packet_duration(self) -> int:
|
||||
"""Gets the duration (in samples) of the last packet successfully decoded or concealed."""
|
||||
|
||||
ret = ctypes.c_int32()
|
||||
_lib.opus_decoder_ctl(self._state, CTL_LAST_PACKET_DURATION, ctypes.byref(ret))
|
||||
return ret.value
|
||||
|
||||
def decode(self, data, *, fec=False):
|
||||
@overload
|
||||
def decode(self, data: bytes, *, fec: bool) -> bytes:
|
||||
...
|
||||
|
||||
@overload
|
||||
def decode(self, data: Literal[None], *, fec: Literal[False]) -> bytes:
|
||||
...
|
||||
|
||||
def decode(self, data: Optional[bytes], *, fec: bool = False) -> bytes:
|
||||
if data is None and fec:
|
||||
raise OpusError("Invalid arguments: FEC cannot be used with null data")
|
||||
raise InvalidArgument("Invalid arguments: FEC cannot be used with null data")
|
||||
|
||||
if data is None:
|
||||
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME
|
||||
@@ -418,4 +466,4 @@ class Decoder(_OpusStruct):
|
||||
|
||||
ret = _lib.opus_decode(self._state, data, len(data) if data else 0, pcm_ptr, frame_size, fec)
|
||||
|
||||
return array.array('h', pcm[:ret * channel_count]).tobytes()
|
||||
return array.array("h", pcm[: ret * channel_count]).tobytes()
|
||||
|
||||
@@ -22,17 +22,36 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .asset import Asset
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar, Union
|
||||
import re
|
||||
|
||||
from .asset import Asset, AssetMixin
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
__all__ = (
|
||||
'PartialEmoji',
|
||||
)
|
||||
__all__ = ("PartialEmoji",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import ConnectionState
|
||||
from datetime import datetime
|
||||
from .types.message import PartialEmoji as PartialEmojiPayload
|
||||
|
||||
|
||||
class _EmojiTag:
|
||||
__slots__ = ()
|
||||
|
||||
class PartialEmoji(_EmojiTag):
|
||||
id: int
|
||||
|
||||
def _to_partial(self) -> PartialEmoji:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
PE = TypeVar("PE", bound="PartialEmoji")
|
||||
|
||||
|
||||
class PartialEmoji(_EmojiTag, AssetMixin):
|
||||
"""Represents a "partial" emoji.
|
||||
|
||||
This model will be given in two scenarios:
|
||||
@@ -70,47 +89,92 @@ class PartialEmoji(_EmojiTag):
|
||||
The ID of the custom emoji, if applicable.
|
||||
"""
|
||||
|
||||
__slots__ = ('animated', 'name', 'id', '_state')
|
||||
__slots__ = ("animated", "name", "id", "_state")
|
||||
|
||||
def __init__(self, *, name, animated=False, id=None):
|
||||
_CUSTOM_EMOJI_RE = re.compile(r"<?(?P<animated>a)?:?(?P<name>[A-Za-z0-9\_]+):(?P<id>[0-9]{13,20})>?")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
id: Optional[int]
|
||||
|
||||
def __init__(self, *, name: str, animated: bool = False, id: Optional[int] = None):
|
||||
self.animated = animated
|
||||
self.name = name
|
||||
self.id = id
|
||||
self._state = None
|
||||
self._state: Optional[ConnectionState] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls: Type[PE], data: Union[PartialEmojiPayload, Dict[str, Any]]) -> PE:
|
||||
return cls(
|
||||
animated=data.get('animated', False),
|
||||
id=utils._get_as_snowflake(data, 'id'),
|
||||
name=data.get('name'),
|
||||
animated=data.get("animated", False),
|
||||
id=utils._get_as_snowflake(data, "id"),
|
||||
name=data.get("name") or "",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
o = { 'name': self.name }
|
||||
@classmethod
|
||||
def from_str(cls: Type[PE], value: str) -> PE:
|
||||
"""Converts a Discord string representation of an emoji to a :class:`PartialEmoji`.
|
||||
|
||||
The formats accepted are:
|
||||
|
||||
- ``a:name:id``
|
||||
- ``<a:name:id>``
|
||||
- ``name:id``
|
||||
- ``<:name:id>``
|
||||
|
||||
If the format does not match then it is assumed to be a unicode emoji.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
------------
|
||||
value: :class:`str`
|
||||
The string representation of an emoji.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`PartialEmoji`
|
||||
The partial emoji from this string.
|
||||
"""
|
||||
match = cls._CUSTOM_EMOJI_RE.match(value)
|
||||
if match is not None:
|
||||
groups = match.groupdict()
|
||||
animated = bool(groups["animated"])
|
||||
emoji_id = int(groups["id"])
|
||||
name = groups["name"]
|
||||
return cls(name=name, animated=animated, id=emoji_id)
|
||||
|
||||
return cls(name=value, id=None, animated=False)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
o: Dict[str, Any] = {"name": self.name}
|
||||
if self.id:
|
||||
o['id'] = self.id
|
||||
o["id"] = self.id
|
||||
if self.animated:
|
||||
o['animated'] = self.animated
|
||||
o["animated"] = self.animated
|
||||
return o
|
||||
|
||||
def _to_partial(self) -> PartialEmoji:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def with_state(cls, state, *, name, animated=False, id=None):
|
||||
def with_state(
|
||||
cls: Type[PE], state: ConnectionState, *, name: str, animated: bool = False, id: Optional[int] = None
|
||||
) -> PE:
|
||||
self = cls(name=name, animated=animated, id=id)
|
||||
self._state = state
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.id is None:
|
||||
return self.name
|
||||
if self.animated:
|
||||
return f'<a:{self.name}:{self.id}>'
|
||||
return f'<:{self.name}:{self.id}>'
|
||||
return f"<a:{self.name}:{self.id}>"
|
||||
return f"<:{self.name}:{self.id}>"
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
|
||||
return f"<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if self.is_unicode_emoji():
|
||||
return isinstance(other, PartialEmoji) and self.name == other.name
|
||||
|
||||
@@ -118,75 +182,50 @@ class PartialEmoji(_EmojiTag):
|
||||
return self.id == other.id
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.name))
|
||||
|
||||
def is_custom_emoji(self):
|
||||
def is_custom_emoji(self) -> bool:
|
||||
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
|
||||
return self.id is not None
|
||||
|
||||
def is_unicode_emoji(self):
|
||||
def is_unicode_emoji(self) -> bool:
|
||||
""":class:`bool`: Checks if this is a Unicode emoji."""
|
||||
return self.id is None
|
||||
|
||||
def _as_reaction(self):
|
||||
def _as_reaction(self) -> str:
|
||||
if self.id is None:
|
||||
return self.name
|
||||
return f'{self.name}:{self.id}'
|
||||
return f"{self.name}:{self.id}"
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> Optional[datetime]:
|
||||
"""Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
if self.is_unicode_emoji():
|
||||
if self.id is None:
|
||||
return None
|
||||
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
""":class:`Asset`: Returns the asset of the emoji, if it is custom.
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns the URL of the emoji, if it is custom.
|
||||
|
||||
This is equivalent to calling :meth:`url_as` with
|
||||
the default parameters (i.e. png/gif detection).
|
||||
"""
|
||||
return self.url_as(format=None)
|
||||
|
||||
def url_as(self, *, format=None, static_format="png"):
|
||||
"""Returns an :class:`Asset` for the emoji's url, if it is custom.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
|
||||
'gif' is only valid for animated emojis.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[:class:`str`]
|
||||
The format to attempt to convert the emojis to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected as either 'gif' or static_format, depending on whether the
|
||||
emoji is animated or not.
|
||||
static_format: Optional[:class:`str`]
|
||||
Format to attempt to convert only non-animated emoji's to.
|
||||
Defaults to 'png'
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or ``static_format``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
If this isn't a custom emoji then an empty string is returned
|
||||
"""
|
||||
if self.is_unicode_emoji():
|
||||
return Asset(self._state)
|
||||
return ""
|
||||
|
||||
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
|
||||
fmt = "gif" if self.animated else "png"
|
||||
return f"{Asset.BASE}/emojis/{self.id}.{fmt}"
|
||||
|
||||
async def read(self) -> bytes:
|
||||
if self.is_unicode_emoji():
|
||||
raise InvalidArgument("PartialEmoji is not a custom emoji")
|
||||
|
||||
return await super().read()
|
||||
|
||||
@@ -22,25 +22,34 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Optional
|
||||
from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value
|
||||
|
||||
__all__ = (
|
||||
'Permissions',
|
||||
'PermissionOverwrite',
|
||||
"Permissions",
|
||||
"PermissionOverwrite",
|
||||
)
|
||||
|
||||
# A permission alias works like a regular flag but is marked
|
||||
# So the PermissionOverwrite knows to work with it
|
||||
class permission_alias(alias_flag_value):
|
||||
pass
|
||||
alias: str
|
||||
|
||||
def make_permission_alias(alias):
|
||||
def decorator(func):
|
||||
|
||||
def make_permission_alias(alias: str) -> Callable[[Callable[[Any], int]], permission_alias]:
|
||||
def decorator(func: Callable[[Any], int]) -> permission_alias:
|
||||
ret = permission_alias(func)
|
||||
ret.alias = alias
|
||||
return ret
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
P = TypeVar("P", bound="Permissions")
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class Permissions(BaseFlags):
|
||||
"""Wraps up the Discord permission value.
|
||||
@@ -92,35 +101,35 @@ class Permissions(BaseFlags):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, permissions=0, **kwargs):
|
||||
def __init__(self, permissions: int = 0, **kwargs: bool):
|
||||
if not isinstance(permissions, int):
|
||||
raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
|
||||
raise TypeError(f"Expected int parameter, received {permissions.__class__.__name__} instead.")
|
||||
|
||||
self.value = permissions
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError(f'{key!r} is not a valid permission name.')
|
||||
raise TypeError(f"{key!r} is not a valid permission name.")
|
||||
setattr(self, key, value)
|
||||
|
||||
def is_subset(self, other):
|
||||
def is_subset(self, other: Permissions) -> bool:
|
||||
"""Returns ``True`` if self has the same or fewer permissions as other."""
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value & other.value) == self.value
|
||||
else:
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_superset(self, other):
|
||||
def is_superset(self, other: Permissions) -> bool:
|
||||
"""Returns ``True`` if self has the same or more permissions as other."""
|
||||
if isinstance(other, Permissions):
|
||||
return (self.value | other.value) == self.value
|
||||
else:
|
||||
raise TypeError(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def is_strict_subset(self, other):
|
||||
def is_strict_subset(self, other: Permissions) -> bool:
|
||||
"""Returns ``True`` if the permissions on other are a strict subset of those on self."""
|
||||
return self.is_subset(other) and self != other
|
||||
|
||||
def is_strict_superset(self, other):
|
||||
def is_strict_superset(self, other: Permissions) -> bool:
|
||||
"""Returns ``True`` if the permissions on other are a strict superset of those on self."""
|
||||
return self.is_superset(other) and self != other
|
||||
|
||||
@@ -130,20 +139,20 @@ class Permissions(BaseFlags):
|
||||
__gt__ = is_strict_superset
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
def none(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to ``False``."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
def all(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to ``True``.
|
||||
"""
|
||||
return cls(0b111111111111111111111111111111111)
|
||||
return cls(0b111111111111111111111111111111111111111)
|
||||
|
||||
@classmethod
|
||||
def all_channel(cls):
|
||||
def all_channel(cls: Type[P]) -> P:
|
||||
"""A :class:`Permissions` with all channel-specific permissions set to
|
||||
``True`` and the guild-specific ones set to ``False``. The guild-specific
|
||||
permissions are currently:
|
||||
@@ -160,11 +169,16 @@ class Permissions(BaseFlags):
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_slash_commands` permissions.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
|
||||
:attr:`use_external_stickers`, :attr:`send_messages_in_threads` and
|
||||
:attr:`request_to_speak` permissions.
|
||||
"""
|
||||
return cls(0b10110011111101111111111101010001)
|
||||
return cls(0b111110110110011111101111111111101010001)
|
||||
|
||||
@classmethod
|
||||
def general(cls):
|
||||
def general(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"General" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
@@ -177,7 +191,7 @@ class Permissions(BaseFlags):
|
||||
return cls(0b01110000000010000000010010110000)
|
||||
|
||||
@classmethod
|
||||
def membership(cls):
|
||||
def membership(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Membership" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
@@ -186,24 +200,28 @@ class Permissions(BaseFlags):
|
||||
return cls(0b00001100000000000000000000000111)
|
||||
|
||||
@classmethod
|
||||
def text(cls):
|
||||
def text(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Text" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Permission :attr:`read_messages` is no longer part of the text permissions.
|
||||
Added :attr:`use_slash_commands` permission.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
|
||||
:attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions.
|
||||
"""
|
||||
return cls(0b10000000000001111111100001000000)
|
||||
return cls(0b111110010000000000001111111100001000000)
|
||||
|
||||
@classmethod
|
||||
def voice(cls):
|
||||
def voice(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Voice" permissions from the official Discord UI set to ``True``."""
|
||||
return cls(0b00000011111100000000001100000000)
|
||||
|
||||
@classmethod
|
||||
def stage(cls):
|
||||
def stage(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Channel" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
@@ -212,7 +230,7 @@ class Permissions(BaseFlags):
|
||||
return cls(1 << 32)
|
||||
|
||||
@classmethod
|
||||
def stage_moderator(cls):
|
||||
def stage_moderator(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Stage Moderator" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
@@ -221,7 +239,7 @@ class Permissions(BaseFlags):
|
||||
return cls(0b100000001010000000000000000000000)
|
||||
|
||||
@classmethod
|
||||
def advanced(cls):
|
||||
def advanced(cls: Type[P]) -> P:
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Advanced" permissions from the official Discord UI set to ``True``.
|
||||
|
||||
@@ -229,7 +247,7 @@ class Permissions(BaseFlags):
|
||||
"""
|
||||
return cls(1 << 3)
|
||||
|
||||
def update(self, **kwargs):
|
||||
def update(self, **kwargs: bool) -> None:
|
||||
r"""Bulk updates this permission object.
|
||||
|
||||
Allows you to set multiple attributes by using keyword
|
||||
@@ -245,7 +263,7 @@ class Permissions(BaseFlags):
|
||||
if key in self.VALID_FLAGS:
|
||||
setattr(self, key, value)
|
||||
|
||||
def handle_overwrite(self, allow, deny):
|
||||
def handle_overwrite(self, allow: int, deny: int) -> None:
|
||||
# Basically this is what's happening here.
|
||||
# We have an original bit array, e.g. 1010
|
||||
# Then we have another bit array that is 'denied', e.g. 1111
|
||||
@@ -261,67 +279,74 @@ class Permissions(BaseFlags):
|
||||
self.value = (self.value & ~deny) | allow
|
||||
|
||||
@flag_value
|
||||
def create_instant_invite(self):
|
||||
def create_instant_invite(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if the user can create instant invites."""
|
||||
return 1 << 0
|
||||
|
||||
@flag_value
|
||||
def kick_members(self):
|
||||
def kick_members(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if the user can kick users from the guild."""
|
||||
return 1 << 1
|
||||
|
||||
@flag_value
|
||||
def ban_members(self):
|
||||
def ban_members(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can ban users from the guild."""
|
||||
return 1 << 2
|
||||
|
||||
@flag_value
|
||||
def administrator(self):
|
||||
def administrator(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user is an administrator. This role overrides all other permissions.
|
||||
|
||||
This also bypasses all channel-specific overrides.
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@make_permission_alias("administrator")
|
||||
def admin(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`administrator`.
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@flag_value
|
||||
def manage_channels(self):
|
||||
def manage_channels(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can edit, delete, or create channels in the guild.
|
||||
|
||||
This also corresponds to the "Manage Channel" channel-specific override."""
|
||||
return 1 << 4
|
||||
|
||||
@flag_value
|
||||
def manage_guild(self):
|
||||
def manage_guild(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can edit guild properties."""
|
||||
return 1 << 5
|
||||
|
||||
@flag_value
|
||||
def add_reactions(self):
|
||||
def add_reactions(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can add reactions to messages."""
|
||||
return 1 << 6
|
||||
|
||||
@flag_value
|
||||
def view_audit_log(self):
|
||||
def view_audit_log(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can view the guild's audit log."""
|
||||
return 1 << 7
|
||||
|
||||
@flag_value
|
||||
def priority_speaker(self):
|
||||
def priority_speaker(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can be more easily heard while talking."""
|
||||
return 1 << 8
|
||||
|
||||
@flag_value
|
||||
def stream(self):
|
||||
def stream(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can stream in a voice channel."""
|
||||
return 1 << 9
|
||||
|
||||
@flag_value
|
||||
def read_messages(self):
|
||||
def read_messages(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can read messages from all or specific text channels."""
|
||||
return 1 << 10
|
||||
|
||||
@make_permission_alias('read_messages')
|
||||
def view_channel(self):
|
||||
@make_permission_alias("read_messages")
|
||||
def view_channel(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`read_messages`.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -329,17 +354,17 @@ class Permissions(BaseFlags):
|
||||
return 1 << 10
|
||||
|
||||
@flag_value
|
||||
def send_messages(self):
|
||||
def send_messages(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can send messages from all or specific text channels."""
|
||||
return 1 << 11
|
||||
|
||||
@flag_value
|
||||
def send_tts_messages(self):
|
||||
def send_tts_messages(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can send TTS messages from all or specific text channels."""
|
||||
return 1 << 12
|
||||
|
||||
@flag_value
|
||||
def manage_messages(self):
|
||||
def manage_messages(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
|
||||
|
||||
.. note::
|
||||
@@ -349,32 +374,32 @@ class Permissions(BaseFlags):
|
||||
return 1 << 13
|
||||
|
||||
@flag_value
|
||||
def embed_links(self):
|
||||
def embed_links(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user's messages will automatically be embedded by Discord."""
|
||||
return 1 << 14
|
||||
|
||||
@flag_value
|
||||
def attach_files(self):
|
||||
def attach_files(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can send files in their messages."""
|
||||
return 1 << 15
|
||||
|
||||
@flag_value
|
||||
def read_message_history(self):
|
||||
def read_message_history(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can read a text channel's previous messages."""
|
||||
return 1 << 16
|
||||
|
||||
@flag_value
|
||||
def mention_everyone(self):
|
||||
def mention_everyone(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user's @everyone or @here will mention everyone in the text channel."""
|
||||
return 1 << 17
|
||||
|
||||
@flag_value
|
||||
def external_emojis(self):
|
||||
def external_emojis(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can use emojis from other guilds."""
|
||||
return 1 << 18
|
||||
|
||||
@make_permission_alias('external_emojis')
|
||||
def use_external_emojis(self):
|
||||
@make_permission_alias("external_emojis")
|
||||
def use_external_emojis(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`external_emojis`.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -382,7 +407,7 @@ class Permissions(BaseFlags):
|
||||
return 1 << 18
|
||||
|
||||
@flag_value
|
||||
def view_guild_insights(self):
|
||||
def view_guild_insights(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can view the guild's insights.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -390,55 +415,55 @@ class Permissions(BaseFlags):
|
||||
return 1 << 19
|
||||
|
||||
@flag_value
|
||||
def connect(self):
|
||||
def connect(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can connect to a voice channel."""
|
||||
return 1 << 20
|
||||
|
||||
@flag_value
|
||||
def speak(self):
|
||||
def speak(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can speak in a voice channel."""
|
||||
return 1 << 21
|
||||
|
||||
@flag_value
|
||||
def mute_members(self):
|
||||
def mute_members(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can mute other users."""
|
||||
return 1 << 22
|
||||
|
||||
@flag_value
|
||||
def deafen_members(self):
|
||||
def deafen_members(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can deafen other users."""
|
||||
return 1 << 23
|
||||
|
||||
@flag_value
|
||||
def move_members(self):
|
||||
def move_members(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can move users between other voice channels."""
|
||||
return 1 << 24
|
||||
|
||||
@flag_value
|
||||
def use_voice_activation(self):
|
||||
def use_voice_activation(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can use voice activation in voice channels."""
|
||||
return 1 << 25
|
||||
|
||||
@flag_value
|
||||
def change_nickname(self):
|
||||
def change_nickname(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can change their nickname in the guild."""
|
||||
return 1 << 26
|
||||
|
||||
@flag_value
|
||||
def manage_nicknames(self):
|
||||
def manage_nicknames(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can change other user's nickname in the guild."""
|
||||
return 1 << 27
|
||||
|
||||
@flag_value
|
||||
def manage_roles(self):
|
||||
def manage_roles(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can create or edit roles less than their role's position.
|
||||
|
||||
This also corresponds to the "Manage Permissions" channel-specific override.
|
||||
"""
|
||||
return 1 << 28
|
||||
|
||||
@make_permission_alias('manage_roles')
|
||||
def manage_permissions(self):
|
||||
@make_permission_alias("manage_roles")
|
||||
def manage_permissions(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`manage_roles`.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -446,17 +471,25 @@ class Permissions(BaseFlags):
|
||||
return 1 << 28
|
||||
|
||||
@flag_value
|
||||
def manage_webhooks(self):
|
||||
def manage_webhooks(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete webhooks."""
|
||||
return 1 << 29
|
||||
|
||||
@flag_value
|
||||
def manage_emojis(self):
|
||||
def manage_emojis(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis."""
|
||||
return 1 << 30
|
||||
|
||||
@make_permission_alias("manage_emojis")
|
||||
def manage_emojis_and_stickers(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`manage_emojis`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 30
|
||||
|
||||
@flag_value
|
||||
def use_slash_commands(self):
|
||||
def use_slash_commands(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can use slash commands.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
@@ -464,14 +497,74 @@ class Permissions(BaseFlags):
|
||||
return 1 << 31
|
||||
|
||||
@flag_value
|
||||
def request_to_speak(self):
|
||||
def request_to_speak(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can request to speak in a stage channel.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return 1 << 32
|
||||
|
||||
def augment_from_permissions(cls):
|
||||
@flag_value
|
||||
def manage_events(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can manage guild events.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 33
|
||||
|
||||
@flag_value
|
||||
def manage_threads(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can manage threads.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 34
|
||||
|
||||
@flag_value
|
||||
def create_public_threads(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can create public threads.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 35
|
||||
|
||||
@flag_value
|
||||
def create_private_threads(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can create private threads.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 36
|
||||
|
||||
@flag_value
|
||||
def external_stickers(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can use stickers from other guilds.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 37
|
||||
|
||||
@make_permission_alias("external_stickers")
|
||||
def use_external_stickers(self) -> int:
|
||||
""":class:`bool`: An alias for :attr:`external_stickers`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 37
|
||||
|
||||
@flag_value
|
||||
def send_messages_in_threads(self) -> int:
|
||||
""":class:`bool`: Returns ``True`` if a user can send messages in threads.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return 1 << 38
|
||||
|
||||
|
||||
PO = TypeVar("PO", bound="PermissionOverwrite")
|
||||
|
||||
|
||||
def _augment_from_permissions(cls):
|
||||
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
|
||||
aliases = set()
|
||||
|
||||
@@ -488,6 +581,7 @@ def augment_from_permissions(cls):
|
||||
# god bless Python
|
||||
def getter(self, x=key):
|
||||
return self._values.get(x)
|
||||
|
||||
def setter(self, value, x=key):
|
||||
self._set(x, value)
|
||||
|
||||
@@ -497,7 +591,8 @@ def augment_from_permissions(cls):
|
||||
cls.PURE_FLAGS = cls.VALID_NAMES - aliases
|
||||
return cls
|
||||
|
||||
@augment_from_permissions
|
||||
|
||||
@_augment_from_permissions
|
||||
class PermissionOverwrite:
|
||||
r"""A type that is used to represent a channel specific permission.
|
||||
|
||||
@@ -530,30 +625,79 @@ class PermissionOverwrite:
|
||||
Set the value of permissions by their name.
|
||||
"""
|
||||
|
||||
__slots__ = ('_values',)
|
||||
__slots__ = ("_values",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._values = {}
|
||||
if TYPE_CHECKING:
|
||||
VALID_NAMES: ClassVar[Set[str]]
|
||||
PURE_FLAGS: ClassVar[Set[str]]
|
||||
# I wish I didn't have to do this
|
||||
create_instant_invite: Optional[bool]
|
||||
kick_members: Optional[bool]
|
||||
ban_members: Optional[bool]
|
||||
administrator: Optional[bool]
|
||||
manage_channels: Optional[bool]
|
||||
manage_guild: Optional[bool]
|
||||
add_reactions: Optional[bool]
|
||||
view_audit_log: Optional[bool]
|
||||
priority_speaker: Optional[bool]
|
||||
stream: Optional[bool]
|
||||
read_messages: Optional[bool]
|
||||
view_channel: Optional[bool]
|
||||
send_messages: Optional[bool]
|
||||
send_tts_messages: Optional[bool]
|
||||
manage_messages: Optional[bool]
|
||||
embed_links: Optional[bool]
|
||||
attach_files: Optional[bool]
|
||||
read_message_history: Optional[bool]
|
||||
mention_everyone: Optional[bool]
|
||||
external_emojis: Optional[bool]
|
||||
use_external_emojis: Optional[bool]
|
||||
view_guild_insights: Optional[bool]
|
||||
connect: Optional[bool]
|
||||
speak: Optional[bool]
|
||||
mute_members: Optional[bool]
|
||||
deafen_members: Optional[bool]
|
||||
move_members: Optional[bool]
|
||||
use_voice_activation: Optional[bool]
|
||||
change_nickname: Optional[bool]
|
||||
manage_nicknames: Optional[bool]
|
||||
manage_roles: Optional[bool]
|
||||
manage_permissions: Optional[bool]
|
||||
manage_webhooks: Optional[bool]
|
||||
manage_emojis: Optional[bool]
|
||||
manage_emojis_and_stickers: Optional[bool]
|
||||
use_slash_commands: Optional[bool]
|
||||
request_to_speak: Optional[bool]
|
||||
manage_events: Optional[bool]
|
||||
manage_threads: Optional[bool]
|
||||
create_public_threads: Optional[bool]
|
||||
create_private_threads: Optional[bool]
|
||||
send_messages_in_threads: Optional[bool]
|
||||
external_stickers: Optional[bool]
|
||||
use_external_stickers: Optional[bool]
|
||||
|
||||
def __init__(self, **kwargs: Optional[bool]):
|
||||
self._values: Dict[str, Optional[bool]] = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_NAMES:
|
||||
raise ValueError(f'no permission called {key}.')
|
||||
raise ValueError(f"no permission called {key}.")
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, PermissionOverwrite) and self._values == other._values
|
||||
|
||||
def _set(self, key, value):
|
||||
def _set(self, key: str, value: Optional[bool]) -> None:
|
||||
if value not in (True, None, False):
|
||||
raise TypeError(f'Expected bool or NoneType, received {value.__class__.__name__}')
|
||||
raise TypeError(f"Expected bool or NoneType, received {value.__class__.__name__}")
|
||||
|
||||
if value is None:
|
||||
self._values.pop(key, None)
|
||||
else:
|
||||
self._values[key] = value
|
||||
|
||||
def pair(self):
|
||||
def pair(self) -> Tuple[Permissions, Permissions]:
|
||||
"""Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite."""
|
||||
|
||||
allow = Permissions.none()
|
||||
@@ -568,7 +712,7 @@ class PermissionOverwrite:
|
||||
return allow, deny
|
||||
|
||||
@classmethod
|
||||
def from_pair(cls, allow, deny):
|
||||
def from_pair(cls: Type[PO], allow: Permissions, deny: Permissions) -> PO:
|
||||
"""Creates an overwrite from an allow/deny pair of :class:`Permissions`."""
|
||||
ret = cls()
|
||||
for key, value in allow:
|
||||
@@ -581,7 +725,7 @@ class PermissionOverwrite:
|
||||
|
||||
return ret
|
||||
|
||||
def is_empty(self):
|
||||
def is_empty(self) -> bool:
|
||||
"""Checks if the permission overwrite is currently empty.
|
||||
|
||||
An empty permission overwrite is one that has no overwrites set
|
||||
@@ -594,7 +738,7 @@ class PermissionOverwrite:
|
||||
"""
|
||||
return len(self._values) == 0
|
||||
|
||||
def update(self, **kwargs):
|
||||
def update(self, **kwargs: bool) -> None:
|
||||
r"""Bulk updates this permission overwrite object.
|
||||
|
||||
Allows you to set multiple attributes by using keyword
|
||||
@@ -612,6 +756,6 @@ class PermissionOverwrite:
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tuple[str, Optional[bool]]]:
|
||||
for key in self.PURE_FLAGS:
|
||||
yield key, self._values.get(key)
|
||||
|
||||
@@ -21,6 +21,7 @@ LIABILITY, WHETHER 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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import traceback
|
||||
@@ -33,27 +34,41 @@ import time
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import io
|
||||
|
||||
from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||
|
||||
from .errors import ClientException
|
||||
from .opus import Encoder as OpusEncoder
|
||||
from .oggparse import OggStream
|
||||
from .utils import MISSING
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from .voice_client import VoiceClient
|
||||
|
||||
|
||||
AT = TypeVar("AT", bound="AudioSource")
|
||||
FT = TypeVar("FT", bound="FFmpegOpusAudio")
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = (
|
||||
'AudioSource',
|
||||
'PCMAudio',
|
||||
'FFmpegAudio',
|
||||
'FFmpegPCMAudio',
|
||||
'FFmpegOpusAudio',
|
||||
'PCMVolumeTransformer',
|
||||
"AudioSource",
|
||||
"PCMAudio",
|
||||
"FFmpegAudio",
|
||||
"FFmpegPCMAudio",
|
||||
"FFmpegOpusAudio",
|
||||
"PCMVolumeTransformer",
|
||||
)
|
||||
|
||||
if sys.platform != 'win32':
|
||||
CREATE_NO_WINDOW: int
|
||||
|
||||
if sys.platform != "win32":
|
||||
CREATE_NO_WINDOW = 0
|
||||
else:
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
|
||||
|
||||
class AudioSource:
|
||||
"""Represents an audio stream.
|
||||
|
||||
@@ -65,7 +80,7 @@ class AudioSource:
|
||||
The audio source reads are done in a separate thread.
|
||||
"""
|
||||
|
||||
def read(self):
|
||||
def read(self) -> bytes:
|
||||
"""Reads 20ms worth of audio.
|
||||
|
||||
Subclasses must implement this.
|
||||
@@ -73,7 +88,7 @@ class AudioSource:
|
||||
If the audio is complete, then returning an empty
|
||||
:term:`py:bytes-like object` to signal this is the way to do so.
|
||||
|
||||
If :meth:`is_opus` method returns ``True``, then it must return
|
||||
If :meth:`~AudioSource.is_opus` method returns ``True``, then it must return
|
||||
20ms worth of Opus encoded audio. Otherwise, it must be 20ms
|
||||
worth of 16-bit 48KHz stereo PCM, which is about 3,840 bytes
|
||||
per frame (20ms worth of audio).
|
||||
@@ -85,11 +100,11 @@ class AudioSource:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_opus(self):
|
||||
def is_opus(self) -> bool:
|
||||
"""Checks if the audio source is already encoded in Opus."""
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
"""Called when clean-up is needed to be done.
|
||||
|
||||
Useful for clearing buffer data or processes after
|
||||
@@ -97,9 +112,10 @@ class AudioSource:
|
||||
"""
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
def __del__(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class PCMAudio(AudioSource):
|
||||
"""Represents raw 16-bit 48KHz stereo PCM audio source.
|
||||
|
||||
@@ -108,15 +124,17 @@ class PCMAudio(AudioSource):
|
||||
stream: :term:`py:file object`
|
||||
A file-like object that reads byte data representing raw PCM.
|
||||
"""
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
def read(self):
|
||||
def __init__(self, stream: io.BufferedIOBase) -> None:
|
||||
self.stream: io.BufferedIOBase = stream
|
||||
|
||||
def read(self) -> bytes:
|
||||
ret = self.stream.read(OpusEncoder.FRAME_SIZE)
|
||||
if len(ret) != OpusEncoder.FRAME_SIZE:
|
||||
return b''
|
||||
return b""
|
||||
return ret
|
||||
|
||||
|
||||
class FFmpegAudio(AudioSource):
|
||||
"""Represents an FFmpeg (or AVConv) based AudioSource.
|
||||
|
||||
@@ -126,48 +144,78 @@ class FFmpegAudio(AudioSource):
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
|
||||
def __init__(self, source, *, executable='ffmpeg', args, **subprocess_kwargs):
|
||||
self._process = self._stdout = None
|
||||
def __init__(
|
||||
self, source: Union[str, io.BufferedIOBase], *, executable: str = "ffmpeg", args: Any, **subprocess_kwargs: Any
|
||||
):
|
||||
piping = subprocess_kwargs.get("stdin") == subprocess.PIPE
|
||||
if piping and isinstance(source, str):
|
||||
raise TypeError("parameter conflict: 'source' parameter cannot be a string when piping to stdin")
|
||||
|
||||
args = [executable, *args]
|
||||
kwargs = {'stdout': subprocess.PIPE}
|
||||
kwargs = {"stdout": subprocess.PIPE}
|
||||
kwargs.update(subprocess_kwargs)
|
||||
|
||||
self._process = self._spawn_process(args, **kwargs)
|
||||
self._stdout = self._process.stdout
|
||||
self._process: subprocess.Popen = self._spawn_process(args, **kwargs)
|
||||
self._stdout: IO[bytes] = self._process.stdout # type: ignore
|
||||
self._stdin: Optional[IO[Bytes]] = None
|
||||
self._pipe_thread: Optional[threading.Thread] = None
|
||||
|
||||
def _spawn_process(self, args, **subprocess_kwargs):
|
||||
if piping:
|
||||
n = f"popen-stdin-writer:{id(self):#x}"
|
||||
self._stdin = self._process.stdin
|
||||
self._pipe_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n)
|
||||
self._pipe_thread.start()
|
||||
|
||||
def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen:
|
||||
process = None
|
||||
try:
|
||||
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)
|
||||
except FileNotFoundError:
|
||||
executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
|
||||
raise ClientException(executable + ' was not found.') from None
|
||||
executable = args.partition(" ")[0] if isinstance(args, str) else args[0]
|
||||
raise ClientException(executable + " was not found.") from None
|
||||
except subprocess.SubprocessError as exc:
|
||||
raise ClientException('Popen failed: {0.__class__.__name__}: {0}'.format(exc)) from exc
|
||||
raise ClientException(f"Popen failed: {exc.__class__.__name__}: {exc}") from exc
|
||||
else:
|
||||
return process
|
||||
|
||||
def cleanup(self):
|
||||
def _kill_process(self) -> None:
|
||||
proc = self._process
|
||||
if proc is None:
|
||||
if proc is MISSING:
|
||||
return
|
||||
|
||||
log.info('Preparing to terminate ffmpeg process %s.', proc.pid)
|
||||
_log.info("Preparing to terminate ffmpeg process %s.", proc.pid)
|
||||
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
log.exception("Ignoring error attempting to kill ffmpeg process %s", proc.pid)
|
||||
_log.exception("Ignoring error attempting to kill ffmpeg process %s", proc.pid)
|
||||
|
||||
if proc.poll() is None:
|
||||
log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid)
|
||||
_log.info("ffmpeg process %s has not terminated. Waiting to terminate...", proc.pid)
|
||||
proc.communicate()
|
||||
log.info('ffmpeg process %s should have terminated with a return code of %s.', proc.pid, proc.returncode)
|
||||
_log.info("ffmpeg process %s should have terminated with a return code of %s.", proc.pid, proc.returncode)
|
||||
else:
|
||||
log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode)
|
||||
_log.info("ffmpeg process %s successfully terminated with return code of %s.", proc.pid, proc.returncode)
|
||||
|
||||
def _pipe_writer(self, source: io.BufferedIOBase) -> None:
|
||||
while self._process:
|
||||
# arbitrarily large read size
|
||||
data = source.read(8192)
|
||||
if not data:
|
||||
self._process.terminate()
|
||||
return
|
||||
try:
|
||||
self._stdin.write(data)
|
||||
except Exception:
|
||||
_log.debug("Write error for %s, this is probably not a problem", self, exc_info=True)
|
||||
# at this point the source data is either exhausted or the process is fubar
|
||||
self._process.terminate()
|
||||
return
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self._kill_process()
|
||||
self._process = self._stdout = self._stdin = MISSING
|
||||
|
||||
self._process = self._stdout = None
|
||||
|
||||
class FFmpegPCMAudio(FFmpegAudio):
|
||||
"""An audio source from FFmpeg (or AVConv).
|
||||
@@ -204,33 +252,43 @@ class FFmpegPCMAudio(FFmpegAudio):
|
||||
The subprocess failed to be created.
|
||||
"""
|
||||
|
||||
def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None):
|
||||
def __init__(
|
||||
self,
|
||||
source: Union[str, io.BufferedIOBase],
|
||||
*,
|
||||
executable: str = "ffmpeg",
|
||||
pipe: bool = False,
|
||||
stderr: Optional[IO[str]] = None,
|
||||
before_options: Optional[str] = None,
|
||||
options: Optional[str] = None,
|
||||
) -> None:
|
||||
args = []
|
||||
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
|
||||
subprocess_kwargs = {"stdin": subprocess.PIPE if pipe else subprocess.DEVNULL, "stderr": stderr}
|
||||
|
||||
if isinstance(before_options, str):
|
||||
args.extend(shlex.split(before_options))
|
||||
|
||||
args.append('-i')
|
||||
args.append('-' if pipe else source)
|
||||
args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning'))
|
||||
args.append("-i")
|
||||
args.append("-" if pipe else source)
|
||||
args.extend(("-f", "s16le", "-ar", "48000", "-ac", "2", "-loglevel", "warning"))
|
||||
|
||||
if isinstance(options, str):
|
||||
args.extend(shlex.split(options))
|
||||
|
||||
args.append('pipe:1')
|
||||
args.append("pipe:1")
|
||||
|
||||
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
|
||||
|
||||
def read(self):
|
||||
def read(self) -> bytes:
|
||||
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
|
||||
if len(ret) != OpusEncoder.FRAME_SIZE:
|
||||
return b''
|
||||
return b""
|
||||
return ret
|
||||
|
||||
def is_opus(self):
|
||||
def is_opus(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class FFmpegOpusAudio(FFmpegAudio):
|
||||
"""An audio source from FFmpeg (or AVConv).
|
||||
|
||||
@@ -292,38 +350,65 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
The subprocess failed to be created.
|
||||
"""
|
||||
|
||||
def __init__(self, source, *, bitrate=128, codec=None, executable='ffmpeg',
|
||||
pipe=False, stderr=None, before_options=None, options=None):
|
||||
def __init__(
|
||||
self,
|
||||
source: Union[str, io.BufferedIOBase],
|
||||
*,
|
||||
bitrate: int = 128,
|
||||
codec: Optional[str] = None,
|
||||
executable: str = "ffmpeg",
|
||||
pipe=False,
|
||||
stderr=None,
|
||||
before_options=None,
|
||||
options=None,
|
||||
) -> None:
|
||||
|
||||
args = []
|
||||
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
|
||||
subprocess_kwargs = {"stdin": subprocess.PIPE if pipe else subprocess.DEVNULL, "stderr": stderr}
|
||||
|
||||
if isinstance(before_options, str):
|
||||
args.extend(shlex.split(before_options))
|
||||
|
||||
args.append('-i')
|
||||
args.append('-' if pipe else source)
|
||||
args.append("-i")
|
||||
args.append("-" if pipe else source)
|
||||
|
||||
codec = 'copy' if codec in ('opus', 'libopus') else 'libopus'
|
||||
codec = "copy" if codec in ("opus", "libopus") else "libopus"
|
||||
|
||||
args.extend(('-map_metadata', '-1',
|
||||
'-f', 'opus',
|
||||
'-c:a', codec,
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
'-b:a', f'{bitrate}k',
|
||||
'-loglevel', 'warning'))
|
||||
args.extend(
|
||||
(
|
||||
"-map_metadata",
|
||||
"-1",
|
||||
"-f",
|
||||
"opus",
|
||||
"-c:a",
|
||||
codec,
|
||||
"-ar",
|
||||
"48000",
|
||||
"-ac",
|
||||
"2",
|
||||
"-b:a",
|
||||
f"{bitrate}k",
|
||||
"-loglevel",
|
||||
"warning",
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(options, str):
|
||||
args.extend(shlex.split(options))
|
||||
|
||||
args.append('pipe:1')
|
||||
args.append("pipe:1")
|
||||
|
||||
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
|
||||
self._packet_iter = OggStream(self._stdout).iter_packets()
|
||||
|
||||
@classmethod
|
||||
async def from_probe(cls, source, *, method=None, **kwargs):
|
||||
async def from_probe(
|
||||
cls: Type[FT],
|
||||
source: str,
|
||||
*,
|
||||
method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> FT:
|
||||
"""|coro|
|
||||
|
||||
A factory method that creates a :class:`FFmpegOpusAudio` after probing
|
||||
@@ -347,7 +432,6 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
|
||||
def custom_probe(source, executable):
|
||||
# some analysis code here
|
||||
|
||||
return codec, bitrate
|
||||
|
||||
source = await discord.FFmpegOpusAudio.from_probe("song.webm", method=custom_probe)
|
||||
@@ -380,12 +464,18 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
An instance of this class.
|
||||
"""
|
||||
|
||||
executable = kwargs.get('executable')
|
||||
executable = kwargs.get("executable")
|
||||
codec, bitrate = await cls.probe(source, method=method, executable=executable)
|
||||
return cls(source, bitrate=bitrate, codec=codec, **kwargs)
|
||||
return cls(source, bitrate=bitrate, codec=codec, **kwargs) # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def probe(cls, source, *, method=None, executable=None):
|
||||
async def probe(
|
||||
cls,
|
||||
source: str,
|
||||
*,
|
||||
method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None,
|
||||
executable: Optional[str] = None,
|
||||
) -> Tuple[Optional[str], Optional[int]]:
|
||||
"""|coro|
|
||||
|
||||
Probes the input source for bitrate and codec information.
|
||||
@@ -408,16 +498,16 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
|
||||
Returns
|
||||
---------
|
||||
Tuple[Optional[:class:`str`], Optional[:class:`int`]]
|
||||
Optional[Tuple[Optional[:class:`str`], Optional[:class:`int`]]]
|
||||
A 2-tuple with the codec and bitrate of the input source.
|
||||
"""
|
||||
|
||||
method = method or 'native'
|
||||
executable = executable or 'ffmpeg'
|
||||
method = method or "native"
|
||||
executable = executable or "ffmpeg"
|
||||
probefunc = fallback = None
|
||||
|
||||
if isinstance(method, str):
|
||||
probefunc = getattr(cls, '_probe_codec_' + method, None)
|
||||
probefunc = getattr(cls, "_probe_codec_" + method, None)
|
||||
if probefunc is None:
|
||||
raise AttributeError(f"Invalid probe method {method!r}")
|
||||
|
||||
@@ -428,53 +518,52 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
probefunc = method
|
||||
fallback = cls._probe_codec_fallback
|
||||
else:
|
||||
raise TypeError("Expected str or callable for parameter 'probe', " \
|
||||
f"not '{method.__class__.__name__}'")
|
||||
raise TypeError("Expected str or callable for parameter 'probe', " f"not '{method.__class__.__name__}'")
|
||||
|
||||
codec = bitrate = None
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable))
|
||||
codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) # type: ignore
|
||||
except Exception:
|
||||
if not fallback:
|
||||
log.exception("Probe '%s' using '%s' failed", method, executable)
|
||||
return
|
||||
_log.exception("Probe '%s' using '%s' failed", method, executable)
|
||||
return # type: ignore
|
||||
|
||||
log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable)
|
||||
_log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable)
|
||||
try:
|
||||
codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable))
|
||||
codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) # type: ignore
|
||||
except Exception:
|
||||
log.exception("Fallback probe using '%s' failed", executable)
|
||||
_log.exception("Fallback probe using '%s' failed", executable)
|
||||
else:
|
||||
log.info("Fallback probe found codec=%s, bitrate=%s", codec, bitrate)
|
||||
_log.info("Fallback probe found codec=%s, bitrate=%s", codec, bitrate)
|
||||
else:
|
||||
log.info("Probe found codec=%s, bitrate=%s", codec, bitrate)
|
||||
_log.info("Probe found codec=%s, bitrate=%s", codec, bitrate)
|
||||
finally:
|
||||
return codec, bitrate
|
||||
|
||||
@staticmethod
|
||||
def _probe_codec_native(source, executable='ffmpeg'):
|
||||
exe = executable[:2] + 'probe' if executable in ('ffmpeg', 'avconv') else executable
|
||||
args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source]
|
||||
def _probe_codec_native(source, executable: str = "ffmpeg") -> Tuple[Optional[str], Optional[int]]:
|
||||
exe = executable[:2] + "probe" if executable in ("ffmpeg", "avconv") else executable
|
||||
args = [exe, "-v", "quiet", "-print_format", "json", "-show_streams", "-select_streams", "a:0", source]
|
||||
output = subprocess.check_output(args, timeout=20)
|
||||
codec = bitrate = None
|
||||
|
||||
if output:
|
||||
data = json.loads(output)
|
||||
streamdata = data['streams'][0]
|
||||
streamdata = data["streams"][0]
|
||||
|
||||
codec = streamdata.get('codec_name')
|
||||
bitrate = int(streamdata.get('bit_rate', 0))
|
||||
bitrate = max(round(bitrate/1000, 0), 512)
|
||||
codec = streamdata.get("codec_name")
|
||||
bitrate = int(streamdata.get("bit_rate", 0))
|
||||
bitrate = max(round(bitrate / 1000), 512)
|
||||
|
||||
return codec, bitrate
|
||||
|
||||
@staticmethod
|
||||
def _probe_codec_fallback(source, executable='ffmpeg'):
|
||||
args = [executable, '-hide_banner', '-i', source]
|
||||
def _probe_codec_fallback(source, executable: str = "ffmpeg") -> Tuple[Optional[str], Optional[int]]:
|
||||
args = [executable, "-hide_banner", "-i", source]
|
||||
proc = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
out, _ = proc.communicate(timeout=20)
|
||||
output = out.decode('utf8')
|
||||
output = out.decode("utf8")
|
||||
codec = bitrate = None
|
||||
|
||||
codec_match = re.search(r"Stream #0.*?Audio: (\w+)", output)
|
||||
@@ -487,13 +576,14 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
|
||||
return codec, bitrate
|
||||
|
||||
def read(self):
|
||||
return next(self._packet_iter, b'')
|
||||
def read(self) -> bytes:
|
||||
return next(self._packet_iter, b"")
|
||||
|
||||
def is_opus(self):
|
||||
def is_opus(self) -> bool:
|
||||
return True
|
||||
|
||||
class PCMVolumeTransformer(AudioSource):
|
||||
|
||||
class PCMVolumeTransformer(AudioSource, Generic[AT]):
|
||||
"""Transforms a previous :class:`AudioSource` to have volume controls.
|
||||
|
||||
This does not work on audio sources that have :meth:`AudioSource.is_opus`
|
||||
@@ -515,53 +605,54 @@ class PCMVolumeTransformer(AudioSource):
|
||||
The audio source is opus encoded.
|
||||
"""
|
||||
|
||||
def __init__(self, original, volume=1.0):
|
||||
def __init__(self, original: AT, volume: float = 1.0):
|
||||
if not isinstance(original, AudioSource):
|
||||
raise TypeError(f'expected AudioSource not {original.__class__.__name__}.')
|
||||
raise TypeError(f"expected AudioSource not {original.__class__.__name__}.")
|
||||
|
||||
if original.is_opus():
|
||||
raise ClientException('AudioSource must not be Opus encoded.')
|
||||
raise ClientException("AudioSource must not be Opus encoded.")
|
||||
|
||||
self.original = original
|
||||
self.original: AT = original
|
||||
self.volume = volume
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
def volume(self) -> float:
|
||||
"""Retrieves or sets the volume as a floating point percentage (e.g. ``1.0`` for 100%)."""
|
||||
return self._volume
|
||||
|
||||
@volume.setter
|
||||
def volume(self, value):
|
||||
def volume(self, value: float) -> None:
|
||||
self._volume = max(value, 0.0)
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
self.original.cleanup()
|
||||
|
||||
def read(self):
|
||||
def read(self) -> bytes:
|
||||
ret = self.original.read()
|
||||
return audioop.mul(ret, 2, min(self._volume, 2.0))
|
||||
|
||||
|
||||
class AudioPlayer(threading.Thread):
|
||||
DELAY = OpusEncoder.FRAME_LENGTH / 1000.0
|
||||
DELAY: float = OpusEncoder.FRAME_LENGTH / 1000.0
|
||||
|
||||
def __init__(self, source, client, *, after=None):
|
||||
def __init__(self, source: AudioSource, client: VoiceClient, *, after=None):
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.source = source
|
||||
self.client = client
|
||||
self.after = after
|
||||
self.daemon: bool = True
|
||||
self.source: AudioSource = source
|
||||
self.client: VoiceClient = client
|
||||
self.after: Optional[Callable[[Optional[Exception]], Any]] = after
|
||||
|
||||
self._end = threading.Event()
|
||||
self._resumed = threading.Event()
|
||||
self._resumed.set() # we are not paused
|
||||
self._current_error = None
|
||||
self._connected = client._connected
|
||||
self._lock = threading.Lock()
|
||||
self._end: threading.Event = threading.Event()
|
||||
self._resumed: threading.Event = threading.Event()
|
||||
self._resumed.set() # we are not paused
|
||||
self._current_error: Optional[Exception] = None
|
||||
self._connected: threading.Event = client._connected
|
||||
self._lock: threading.Lock = threading.Lock()
|
||||
|
||||
if after is not None and not callable(after):
|
||||
raise TypeError('Expected a callable for the "after" parameter.')
|
||||
|
||||
def _do_run(self):
|
||||
def _do_run(self) -> None:
|
||||
self.loops = 0
|
||||
self._start = time.perf_counter()
|
||||
|
||||
@@ -596,7 +687,7 @@ class AudioPlayer(threading.Thread):
|
||||
delay = max(0, self.DELAY + (next_time - time.perf_counter()))
|
||||
time.sleep(delay)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._do_run()
|
||||
except Exception as exc:
|
||||
@@ -606,53 +697,53 @@ class AudioPlayer(threading.Thread):
|
||||
self.source.cleanup()
|
||||
self._call_after()
|
||||
|
||||
def _call_after(self):
|
||||
def _call_after(self) -> None:
|
||||
error = self._current_error
|
||||
|
||||
if self.after is not None:
|
||||
try:
|
||||
self.after(error)
|
||||
except Exception as exc:
|
||||
log.exception('Calling the after function failed.')
|
||||
_log.exception("Calling the after function failed.")
|
||||
exc.__context__ = error
|
||||
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
||||
elif error:
|
||||
msg = f'Exception in voice thread {self.name}'
|
||||
log.exception(msg, exc_info=error)
|
||||
msg = f"Exception in voice thread {self.name}"
|
||||
_log.exception(msg, exc_info=error)
|
||||
print(msg, file=sys.stderr)
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
self._end.set()
|
||||
self._resumed.set()
|
||||
self._speak(False)
|
||||
|
||||
def pause(self, *, update_speaking=True):
|
||||
def pause(self, *, update_speaking: bool = True) -> None:
|
||||
self._resumed.clear()
|
||||
if update_speaking:
|
||||
self._speak(False)
|
||||
|
||||
def resume(self, *, update_speaking=True):
|
||||
def resume(self, *, update_speaking: bool = True) -> None:
|
||||
self.loops = 0
|
||||
self._start = time.perf_counter()
|
||||
self._resumed.set()
|
||||
if update_speaking:
|
||||
self._speak(True)
|
||||
|
||||
def is_playing(self):
|
||||
def is_playing(self) -> bool:
|
||||
return self._resumed.is_set() and not self._end.is_set()
|
||||
|
||||
def is_paused(self):
|
||||
def is_paused(self) -> bool:
|
||||
return not self._end.is_set() and not self._resumed.is_set()
|
||||
|
||||
def _set_source(self, source):
|
||||
def _set_source(self, source: AudioSource) -> None:
|
||||
with self._lock:
|
||||
self.pause(update_speaking=False)
|
||||
self.source = source
|
||||
self.resume(update_speaking=False)
|
||||
|
||||
def _speak(self, speaking):
|
||||
def _speak(self, speaking: bool) -> None:
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop)
|
||||
except Exception as e:
|
||||
log.info("Speaking call in player failed: %s", e)
|
||||
_log.info("Speaking call in player failed: %s", e)
|
||||
|
||||
0
discord/py.typed
Normal file
0
discord/py.typed
Normal file
@@ -22,19 +22,44 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Optional, Set, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.raw_models import (
|
||||
MessageDeleteEvent,
|
||||
BulkMessageDeleteEvent,
|
||||
ReactionActionEvent,
|
||||
MessageUpdateEvent,
|
||||
ReactionClearEvent,
|
||||
ReactionClearEmojiEvent,
|
||||
IntegrationDeleteEvent,
|
||||
TypingEvent,
|
||||
)
|
||||
from .message import Message
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .member import Member
|
||||
|
||||
|
||||
__all__ = (
|
||||
'RawMessageDeleteEvent',
|
||||
'RawBulkMessageDeleteEvent',
|
||||
'RawMessageUpdateEvent',
|
||||
'RawReactionActionEvent',
|
||||
'RawReactionClearEvent',
|
||||
'RawReactionClearEmojiEvent',
|
||||
"RawMessageDeleteEvent",
|
||||
"RawBulkMessageDeleteEvent",
|
||||
"RawMessageUpdateEvent",
|
||||
"RawReactionActionEvent",
|
||||
"RawReactionClearEvent",
|
||||
"RawReactionClearEmojiEvent",
|
||||
"RawIntegrationDeleteEvent",
|
||||
"RawTypingEvent",
|
||||
)
|
||||
|
||||
|
||||
class _RawReprMixin:
|
||||
def __repr__(self):
|
||||
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
|
||||
return f'<{self.__class__.__name__} {value}>'
|
||||
def __repr__(self) -> str:
|
||||
value = " ".join(f"{attr}={getattr(self, attr)!r}" for attr in self.__slots__)
|
||||
return f"<{self.__class__.__name__} {value}>"
|
||||
|
||||
|
||||
class RawMessageDeleteEvent(_RawReprMixin):
|
||||
"""Represents the event payload for a :func:`on_raw_message_delete` event.
|
||||
@@ -51,16 +76,17 @@ class RawMessageDeleteEvent(_RawReprMixin):
|
||||
The cached message, if found in the internal message cache.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'cached_message')
|
||||
__slots__ = ("message_id", "channel_id", "guild_id", "cached_message")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data['id'])
|
||||
self.channel_id = int(data['channel_id'])
|
||||
self.cached_message = None
|
||||
def __init__(self, data: MessageDeleteEvent) -> None:
|
||||
self.message_id: int = int(data["id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.cached_message: Optional[Message] = None
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawBulkMessageDeleteEvent(_RawReprMixin):
|
||||
"""Represents the event payload for a :func:`on_raw_bulk_message_delete` event.
|
||||
@@ -77,17 +103,18 @@ class RawBulkMessageDeleteEvent(_RawReprMixin):
|
||||
The cached messages, if found in the internal message cache.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_ids', 'channel_id', 'guild_id', 'cached_messages')
|
||||
__slots__ = ("message_ids", "channel_id", "guild_id", "cached_messages")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_ids = {int(x) for x in data.get('ids', [])}
|
||||
self.channel_id = int(data['channel_id'])
|
||||
self.cached_messages = []
|
||||
def __init__(self, data: BulkMessageDeleteEvent) -> None:
|
||||
self.message_ids: Set[int] = {int(x) for x in data.get("ids", [])}
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.cached_messages: List[Message] = []
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawMessageUpdateEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_message_edit` event.
|
||||
@@ -112,18 +139,19 @@ class RawMessageUpdateEvent(_RawReprMixin):
|
||||
it is modified by the data in :attr:`RawMessageUpdateEvent.data`.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message')
|
||||
__slots__ = ("message_id", "channel_id", "guild_id", "data", "cached_message")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data['id'])
|
||||
self.channel_id = int(data['channel_id'])
|
||||
self.data = data
|
||||
self.cached_message = None
|
||||
def __init__(self, data: MessageUpdateEvent) -> None:
|
||||
self.message_id: int = int(data["id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.data: MessageUpdateEvent = data
|
||||
self.cached_message: Optional[Message] = None
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawReactionActionEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_reaction_add` or
|
||||
@@ -154,21 +182,21 @@ class RawReactionActionEvent(_RawReprMixin):
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'user_id', 'channel_id', 'guild_id', 'emoji',
|
||||
'event_type', 'member')
|
||||
__slots__ = ("message_id", "user_id", "channel_id", "guild_id", "emoji", "event_type", "member")
|
||||
|
||||
def __init__(self, data, emoji, event_type):
|
||||
self.message_id = int(data['message_id'])
|
||||
self.channel_id = int(data['channel_id'])
|
||||
self.user_id = int(data['user_id'])
|
||||
self.emoji = emoji
|
||||
self.event_type = event_type
|
||||
self.member = None
|
||||
def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: str) -> None:
|
||||
self.message_id: int = int(data["message_id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.user_id: int = int(data["user_id"])
|
||||
self.emoji: PartialEmoji = emoji
|
||||
self.event_type: str = event_type
|
||||
self.member: Optional[Member] = None
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawReactionClearEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_reaction_clear` event.
|
||||
@@ -183,16 +211,17 @@ class RawReactionClearEvent(_RawReprMixin):
|
||||
The guild ID where the reactions got cleared.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id')
|
||||
__slots__ = ("message_id", "channel_id", "guild_id")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data['message_id'])
|
||||
self.channel_id = int(data['channel_id'])
|
||||
def __init__(self, data: ReactionClearEvent) -> None:
|
||||
self.message_id: int = int(data["message_id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawReactionClearEmojiEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_reaction_clear_emoji` event.
|
||||
@@ -211,14 +240,74 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
|
||||
The custom or unicode emoji being removed.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', 'emoji')
|
||||
__slots__ = ("message_id", "channel_id", "guild_id", "emoji")
|
||||
|
||||
def __init__(self, data, emoji):
|
||||
self.emoji = emoji
|
||||
self.message_id = int(data['message_id'])
|
||||
self.channel_id = int(data['channel_id'])
|
||||
def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None:
|
||||
self.emoji: PartialEmoji = emoji
|
||||
self.message_id: int = int(data["message_id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
|
||||
try:
|
||||
self.guild_id = int(data['guild_id'])
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawIntegrationDeleteEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_integration_delete` event.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
integration_id: :class:`int`
|
||||
The ID of the integration that got deleted.
|
||||
application_id: Optional[:class:`int`]
|
||||
The ID of the bot/OAuth2 application for this deleted integration.
|
||||
guild_id: :class:`int`
|
||||
The guild ID where the integration got deleted.
|
||||
"""
|
||||
|
||||
__slots__ = ("integration_id", "application_id", "guild_id")
|
||||
|
||||
def __init__(self, data: IntegrationDeleteEvent) -> None:
|
||||
self.integration_id: int = int(data["id"])
|
||||
self.guild_id: int = int(data["guild_id"])
|
||||
|
||||
try:
|
||||
self.application_id: Optional[int] = int(data["application_id"])
|
||||
except KeyError:
|
||||
self.application_id: Optional[int] = None
|
||||
|
||||
|
||||
class RawTypingEvent(_RawReprMixin):
|
||||
"""Represents the payload for a :func:`on_raw_typing` event.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
channel_id: :class:`int`
|
||||
The channel ID where the typing originated from.
|
||||
user_id: :class:`int`
|
||||
The ID of the user that started typing.
|
||||
when: :class:`datetime.datetime`
|
||||
When the typing started as an aware datetime in UTC.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the typing originated from, if applicable.
|
||||
member: Optional[:class:`Member`]
|
||||
The member who started typing. Only available if the member started typing in a guild.
|
||||
"""
|
||||
|
||||
__slots__ = ("channel_id", "user_id", "when", "guild_id", "member")
|
||||
|
||||
def __init__(self, data: TypingEvent) -> None:
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.user_id: int = int(data["user_id"])
|
||||
self.when: datetime.datetime = datetime.datetime.fromtimestamp(data.get("timestamp"), tz=datetime.timezone.utc)
|
||||
self.member: Optional[Member] = None
|
||||
|
||||
try:
|
||||
self.guild_id: Optional[int] = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id: Optional[int] = None
|
||||
|
||||
@@ -22,11 +22,20 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, TYPE_CHECKING, Union, Optional
|
||||
|
||||
from .iterators import ReactionIterator
|
||||
|
||||
__all__ = (
|
||||
'Reaction',
|
||||
)
|
||||
__all__ = ("Reaction",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.message import Reaction as ReactionPayload
|
||||
from .message import Message
|
||||
from .partial_emoji import PartialEmoji
|
||||
from .emoji import Emoji
|
||||
from .abc import Snowflake
|
||||
|
||||
|
||||
class Reaction:
|
||||
"""Represents a reaction to a message.
|
||||
@@ -65,37 +74,40 @@ class Reaction:
|
||||
message: :class:`Message`
|
||||
Message this reaction is for.
|
||||
"""
|
||||
__slots__ = ('message', 'count', 'emoji', 'me')
|
||||
|
||||
def __init__(self, *, message, data, emoji=None):
|
||||
self.message = message
|
||||
self.emoji = emoji or message._state.get_reaction_emoji(data['emoji'])
|
||||
self.count = data.get('count', 1)
|
||||
self.me = data.get('me')
|
||||
__slots__ = ("message", "count", "emoji", "me")
|
||||
|
||||
@property
|
||||
def custom_emoji(self):
|
||||
def __init__(
|
||||
self, *, message: Message, data: ReactionPayload, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None
|
||||
):
|
||||
self.message: Message = message
|
||||
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data["emoji"])
|
||||
self.count: int = data.get("count", 1)
|
||||
self.me: bool = data.get("me")
|
||||
|
||||
# TODO: typeguard
|
||||
def is_custom_emoji(self) -> bool:
|
||||
""":class:`bool`: If this is a custom emoji."""
|
||||
return not isinstance(self.emoji, str)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, self.__class__) and other.emoji == self.emoji
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
return other.emoji != self.emoji
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.emoji)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.emoji)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>"
|
||||
|
||||
async def remove(self, user):
|
||||
async def remove(self, user: Snowflake) -> None:
|
||||
"""|coro|
|
||||
|
||||
Remove the reaction by the provided :class:`User` from the message.
|
||||
@@ -123,7 +135,7 @@ class Reaction:
|
||||
|
||||
await self.message.remove_reaction(self.emoji, user)
|
||||
|
||||
async def clear(self):
|
||||
async def clear(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Clears this reaction from the message.
|
||||
@@ -145,7 +157,7 @@ class Reaction:
|
||||
"""
|
||||
await self.message.clear_reaction(self.emoji)
|
||||
|
||||
def users(self, limit=None, after=None):
|
||||
def users(self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None) -> ReactionIterator:
|
||||
"""Returns an :class:`AsyncIterator` representing the users that have reacted to the message.
|
||||
|
||||
The ``after`` parameter must represent a member
|
||||
@@ -158,22 +170,22 @@ class Reaction:
|
||||
|
||||
# I do not actually recommend doing this.
|
||||
async for user in reaction.users():
|
||||
await channel.send('{0} has reacted with {1.emoji}!'.format(user, reaction))
|
||||
await channel.send(f'{user} has reacted with {reaction.emoji}!')
|
||||
|
||||
Flattening into a list: ::
|
||||
|
||||
users = await reaction.users().flatten()
|
||||
# users is now a list of User...
|
||||
winner = random.choice(users)
|
||||
await channel.send('{} has won the raffle.'.format(winner))
|
||||
await channel.send(f'{winner} has won the raffle.')
|
||||
|
||||
Parameters
|
||||
------------
|
||||
limit: :class:`int`
|
||||
limit: Optional[:class:`int`]
|
||||
The maximum number of results to return.
|
||||
If not provided, returns all the users who
|
||||
reacted to the message.
|
||||
after: :class:`abc.Snowflake`
|
||||
after: Optional[:class:`abc.Snowflake`]
|
||||
For pagination, reactions are sorted by member.
|
||||
|
||||
Raises
|
||||
@@ -190,8 +202,8 @@ class Reaction:
|
||||
if the member has left the guild.
|
||||
"""
|
||||
|
||||
if self.custom_emoji:
|
||||
emoji = '{0.name}:{0.id}'.format(self.emoji)
|
||||
if not isinstance(self.emoji, str):
|
||||
emoji = f"{self.emoji.name}:{self.emoji.id}"
|
||||
else:
|
||||
emoji = self.emoji
|
||||
|
||||
|
||||
229
discord/role.py
229
discord/role.py
@@ -22,17 +22,32 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional, TypeVar, Union, overload, TYPE_CHECKING
|
||||
|
||||
from .permissions import Permissions
|
||||
from .errors import InvalidArgument
|
||||
from .colour import Colour
|
||||
from .mixins import Hashable
|
||||
from .utils import snowflake_time, _get_as_snowflake
|
||||
from .utils import snowflake_time, _get_as_snowflake, MISSING
|
||||
|
||||
__all__ = (
|
||||
'RoleTags',
|
||||
'Role',
|
||||
"RoleTags",
|
||||
"Role",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
from .types.role import (
|
||||
Role as RolePayload,
|
||||
RoleTags as RoleTagPayload,
|
||||
)
|
||||
from .types.guild import RolePositionUpdate
|
||||
from .guild import Guild
|
||||
from .member import Member
|
||||
from .state import ConnectionState
|
||||
|
||||
|
||||
class RoleTags:
|
||||
"""Represents tags on a role.
|
||||
|
||||
@@ -52,32 +67,42 @@ class RoleTags:
|
||||
The integration ID that manages the role.
|
||||
"""
|
||||
|
||||
__slots__ = ('bot_id', 'integration_id', '_premium_subscriber',)
|
||||
__slots__ = (
|
||||
"bot_id",
|
||||
"integration_id",
|
||||
"_premium_subscriber",
|
||||
)
|
||||
|
||||
def __init__(self, data):
|
||||
self.bot_id = _get_as_snowflake(data, 'bot_id')
|
||||
self.integration_id = _get_as_snowflake(data, 'integration_id')
|
||||
def __init__(self, data: RoleTagPayload):
|
||||
self.bot_id: Optional[int] = _get_as_snowflake(data, "bot_id")
|
||||
self.integration_id: Optional[int] = _get_as_snowflake(data, "integration_id")
|
||||
# NOTE: The API returns "null" for this if it's valid, which corresponds to None.
|
||||
# This is different from other fields where "null" means "not there".
|
||||
# So in this case, a value of None is the same as True.
|
||||
# Which means we would need a different sentinel. For this purpose I used ellipsis.
|
||||
self._premium_subscriber = data.get('premium_subscriber', ...)
|
||||
# Which means we would need a different sentinel.
|
||||
self._premium_subscriber: Optional[Any] = data.get("premium_subscriber", MISSING)
|
||||
|
||||
def is_bot_managed(self):
|
||||
def is_bot_managed(self) -> bool:
|
||||
""":class:`bool`: Whether the role is associated with a bot."""
|
||||
return self.bot_id is not None
|
||||
|
||||
def is_premium_subscriber(self):
|
||||
def is_premium_subscriber(self) -> bool:
|
||||
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild."""
|
||||
return self._premium_subscriber is None
|
||||
|
||||
def is_integration(self):
|
||||
def is_integration(self) -> bool:
|
||||
""":class:`bool`: Whether the role is managed by an integration."""
|
||||
return self.integration_id is not None
|
||||
|
||||
def __repr__(self):
|
||||
return '<RoleTags bot_id={0.bot_id} integration_id={0.integration_id} ' \
|
||||
'premium_subscriber={1}>'.format(self, self.is_premium_subscriber())
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} "
|
||||
f"premium_subscriber={self.is_premium_subscriber()}>"
|
||||
)
|
||||
|
||||
|
||||
R = TypeVar("R", bound="Role")
|
||||
|
||||
|
||||
class Role(Hashable):
|
||||
"""Represents a Discord role in a :class:`Guild`.
|
||||
@@ -116,6 +141,14 @@ class Role(Hashable):
|
||||
|
||||
Returns the role's name.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the role's ID.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the role's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
@@ -129,6 +162,15 @@ class Role(Hashable):
|
||||
position: :class:`int`
|
||||
The position of the role. This number is usually positive. The bottom
|
||||
role has a position of 0.
|
||||
|
||||
.. warning::
|
||||
|
||||
Multiple roles can have the same position number. As a consequence
|
||||
of this, comparing via role position is prone to subtle bugs if
|
||||
checking for role hierarchy. The recommended and correct way to
|
||||
compare for roles in the hierarchy is using the comparison
|
||||
operators on the role objects themselves.
|
||||
|
||||
managed: :class:`bool`
|
||||
Indicates if the role is managed by the guild through some form of
|
||||
integrations such as Twitch.
|
||||
@@ -138,27 +180,41 @@ class Role(Hashable):
|
||||
The role tags associated with this role.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'name', '_permissions', '_colour', 'position',
|
||||
'managed', 'mentionable', 'hoist', 'guild', 'tags', '_state')
|
||||
__slots__ = (
|
||||
"id",
|
||||
"name",
|
||||
"_permissions",
|
||||
"_colour",
|
||||
"position",
|
||||
"managed",
|
||||
"mentionable",
|
||||
"hoist",
|
||||
"guild",
|
||||
"tags",
|
||||
"_state",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild, state, data):
|
||||
self.guild = guild
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload):
|
||||
self.guild: Guild = guild
|
||||
self._state: ConnectionState = state
|
||||
self.id: int = int(data["id"])
|
||||
self._update(data)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<Role id={0.id} name={0.name!r}>'.format(self)
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __lt__(self, other):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Role id={self.id} name={self.name!r}>"
|
||||
|
||||
def __lt__(self: R, other: R) -> bool:
|
||||
if not isinstance(other, Role) or not isinstance(self, Role):
|
||||
return NotImplemented
|
||||
|
||||
if self.guild != other.guild:
|
||||
raise RuntimeError('cannot compare roles from two different guilds.')
|
||||
raise RuntimeError("cannot compare roles from two different guilds.")
|
||||
|
||||
# the @everyone role is always the lowest role in hierarchy
|
||||
guild_id = self.guild.id
|
||||
@@ -174,87 +230,96 @@ class Role(Hashable):
|
||||
|
||||
return False
|
||||
|
||||
def __le__(self, other):
|
||||
def __le__(self: R, other: R) -> bool:
|
||||
r = Role.__lt__(other, self)
|
||||
if r is NotImplemented:
|
||||
return NotImplemented
|
||||
return not r
|
||||
|
||||
def __gt__(self, other):
|
||||
def __gt__(self: R, other: R) -> bool:
|
||||
return Role.__lt__(other, self)
|
||||
|
||||
def __ge__(self, other):
|
||||
def __ge__(self: R, other: R) -> bool:
|
||||
r = Role.__lt__(self, other)
|
||||
if r is NotImplemented:
|
||||
return NotImplemented
|
||||
return not r
|
||||
|
||||
def _update(self, data):
|
||||
self.name = data['name']
|
||||
self._permissions = int(data.get('permissions_new', 0))
|
||||
self.position = data.get('position', 0)
|
||||
self._colour = data.get('color', 0)
|
||||
self.hoist = data.get('hoist', False)
|
||||
self.managed = data.get('managed', False)
|
||||
self.mentionable = data.get('mentionable', False)
|
||||
def _update(self, data: RolePayload):
|
||||
self.name: str = data["name"]
|
||||
self._permissions: int = int(data.get("permissions", 0))
|
||||
self.position: int = data.get("position", 0)
|
||||
self._colour: int = data.get("color", 0)
|
||||
self.hoist: bool = data.get("hoist", False)
|
||||
self.managed: bool = data.get("managed", False)
|
||||
self.mentionable: bool = data.get("mentionable", False)
|
||||
self.tags: Optional[RoleTags]
|
||||
|
||||
try:
|
||||
self.tags = RoleTags(data['tags'])
|
||||
self.tags = RoleTags(data["tags"])
|
||||
except KeyError:
|
||||
self.tags = None
|
||||
|
||||
def is_default(self):
|
||||
def is_default(self) -> bool:
|
||||
""":class:`bool`: Checks if the role is the default role."""
|
||||
return self.guild.id == self.id
|
||||
|
||||
def is_bot_managed(self):
|
||||
def is_bot_managed(self) -> bool:
|
||||
""":class:`bool`: Whether the role is associated with a bot.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
return self.tags is not None and self.tags.is_bot_managed()
|
||||
|
||||
def is_premium_subscriber(self):
|
||||
def is_premium_subscriber(self) -> bool:
|
||||
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
return self.tags is not None and self.tags.is_premium_subscriber()
|
||||
|
||||
def is_integration(self):
|
||||
def is_integration(self) -> bool:
|
||||
""":class:`bool`: Whether the role is managed by an integration.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
return self.tags is not None and self.tags.is_integration()
|
||||
|
||||
def is_assignable(self) -> bool:
|
||||
""":class:`bool`: Whether the role is able to be assigned or removed by the bot.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
me = self.guild.me
|
||||
return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id)
|
||||
|
||||
@property
|
||||
def permissions(self):
|
||||
def permissions(self) -> Permissions:
|
||||
""":class:`Permissions`: Returns the role's permissions."""
|
||||
return Permissions(self._permissions)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
def colour(self) -> Colour:
|
||||
""":class:`Colour`: Returns the role colour. An alias exists under ``color``."""
|
||||
return Colour(self._colour)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
def color(self) -> Colour:
|
||||
""":class:`Colour`: Returns the role color. An alias exists under ``colour``."""
|
||||
return self.colour
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the role's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: Returns a string that allows you to mention a role."""
|
||||
return f'<@&{self.id}>'
|
||||
return f"<@&{self.id}>"
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
def members(self) -> List[Member]:
|
||||
"""List[:class:`Member`]: Returns all the members with this role."""
|
||||
all_members = self.guild.members
|
||||
if self.is_default():
|
||||
@@ -263,7 +328,7 @@ class Role(Hashable):
|
||||
role_id = self.id
|
||||
return [member for member in all_members if member._roles.has(role_id)]
|
||||
|
||||
async def _move(self, position, reason):
|
||||
async def _move(self, position: int, reason: Optional[str]) -> None:
|
||||
if position <= 0:
|
||||
raise InvalidArgument("Cannot move role to position 0 or below")
|
||||
|
||||
@@ -283,10 +348,21 @@ class Role(Hashable):
|
||||
else:
|
||||
roles.append(self.id)
|
||||
|
||||
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
|
||||
payload: List[RolePositionUpdate] = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
|
||||
await http.move_role_position(self.guild.id, payload, reason=reason)
|
||||
|
||||
async def edit(self, *, reason=None, **fields):
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
permissions: Permissions = MISSING,
|
||||
colour: Union[Colour, int] = MISSING,
|
||||
color: Union[Colour, int] = MISSING,
|
||||
hoist: bool = MISSING,
|
||||
mentionable: bool = MISSING,
|
||||
position: int = MISSING,
|
||||
reason: Optional[str] = MISSING,
|
||||
) -> Optional[Role]:
|
||||
"""|coro|
|
||||
|
||||
Edits the role.
|
||||
@@ -299,6 +375,9 @@ class Role(Hashable):
|
||||
.. versionchanged:: 1.4
|
||||
Can now pass ``int`` to ``colour`` keyword-only parameter.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Edits are no longer in-place, the newly edited role is returned instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@@ -326,33 +405,41 @@ class Role(Hashable):
|
||||
InvalidArgument
|
||||
An invalid position was given or the default
|
||||
role was asked to be moved.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Role`
|
||||
The newly edited role.
|
||||
"""
|
||||
|
||||
position = fields.get('position')
|
||||
if position is not None:
|
||||
if position is not MISSING:
|
||||
await self._move(position, reason=reason)
|
||||
self.position = position
|
||||
|
||||
try:
|
||||
colour = fields['colour']
|
||||
except KeyError:
|
||||
colour = fields.get('color', self.colour)
|
||||
payload: Dict[str, Any] = {}
|
||||
if color is not MISSING:
|
||||
colour = color
|
||||
|
||||
if isinstance(colour, int):
|
||||
colour = Colour(value=colour)
|
||||
if colour is not MISSING:
|
||||
if isinstance(colour, int):
|
||||
payload["color"] = colour
|
||||
else:
|
||||
payload["color"] = colour.value
|
||||
|
||||
payload = {
|
||||
'name': fields.get('name', self.name),
|
||||
'permissions': str(fields.get('permissions', self.permissions).value),
|
||||
'color': colour.value,
|
||||
'hoist': fields.get('hoist', self.hoist),
|
||||
'mentionable': fields.get('mentionable', self.mentionable)
|
||||
}
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
|
||||
if permissions is not MISSING:
|
||||
payload["permissions"] = permissions.value
|
||||
|
||||
if hoist is not MISSING:
|
||||
payload["hoist"] = hoist
|
||||
|
||||
if mentionable is not MISSING:
|
||||
payload["mentionable"] = mentionable
|
||||
|
||||
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
|
||||
self._update(data)
|
||||
return Role(guild=self.guild, data=data, state=self._state)
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the role.
|
||||
|
||||
249
discord/shard.py
249
discord/shard.py
@@ -22,8 +22,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
@@ -34,22 +35,30 @@ from .backoff import ExponentialBackoff
|
||||
from .gateway import *
|
||||
from .errors import (
|
||||
ClientException,
|
||||
InvalidArgument,
|
||||
HTTPException,
|
||||
GatewayNotFound,
|
||||
ConnectionClosed,
|
||||
PrivilegedIntentsRequired,
|
||||
)
|
||||
|
||||
from . import utils
|
||||
from .enums import Status
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, Optional, List, Dict, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .gateway import DiscordWebSocket
|
||||
from .activity import BaseActivity
|
||||
from .enums import Status
|
||||
|
||||
EI = TypeVar("EI", bound="EventItem")
|
||||
|
||||
__all__ = (
|
||||
'AutoShardedClient',
|
||||
'ShardInfo',
|
||||
"AutoShardedClient",
|
||||
"ShardInfo",
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventType:
|
||||
close = 0
|
||||
@@ -59,39 +68,41 @@ class EventType:
|
||||
terminate = 4
|
||||
clean_close = 5
|
||||
|
||||
|
||||
class EventItem:
|
||||
__slots__ = ('type', 'shard', 'error')
|
||||
__slots__ = ("type", "shard", "error")
|
||||
|
||||
def __init__(self, etype, shard, error):
|
||||
self.type = etype
|
||||
self.shard = shard
|
||||
self.error = error
|
||||
def __init__(self, etype: int, shard: Optional["Shard"], error: Optional[Exception]) -> None:
|
||||
self.type: int = etype
|
||||
self.shard: Optional["Shard"] = shard
|
||||
self.error: Optional[Exception] = error
|
||||
|
||||
def __lt__(self, other):
|
||||
def __lt__(self: EI, other: EI) -> bool:
|
||||
if not isinstance(other, EventItem):
|
||||
return NotImplemented
|
||||
return self.type < other.type
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self: EI, other: EI) -> bool:
|
||||
if not isinstance(other, EventItem):
|
||||
return NotImplemented
|
||||
return self.type == other.type
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.type)
|
||||
|
||||
|
||||
class Shard:
|
||||
def __init__(self, ws, client, queue_put):
|
||||
self.ws = ws
|
||||
self._client = client
|
||||
self._dispatch = client.dispatch
|
||||
self._queue_put = queue_put
|
||||
self.loop = self._client.loop
|
||||
self._disconnect = False
|
||||
def __init__(self, ws: DiscordWebSocket, client: AutoShardedClient, queue_put: Callable[[EventItem], None]) -> None:
|
||||
self.ws: DiscordWebSocket = ws
|
||||
self._client: Client = client
|
||||
self._dispatch: Callable[..., None] = client.dispatch
|
||||
self._queue_put: Callable[[EventItem], None] = queue_put
|
||||
self.loop: asyncio.AbstractEventLoop = self._client.loop
|
||||
self._disconnect: bool = False
|
||||
self._reconnect = client._reconnect
|
||||
self._backoff = ExponentialBackoff()
|
||||
self._task = None
|
||||
self._handled_exceptions = (
|
||||
self._backoff: ExponentialBackoff = ExponentialBackoff()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._handled_exceptions: Tuple[Type[Exception], ...] = (
|
||||
OSError,
|
||||
HTTPException,
|
||||
GatewayNotFound,
|
||||
@@ -101,27 +112,28 @@ class Shard:
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.ws.shard_id
|
||||
def id(self) -> int:
|
||||
# DiscordWebSocket.shard_id is set in the from_client classmethod
|
||||
return self.ws.shard_id # type: ignore
|
||||
|
||||
def launch(self):
|
||||
def launch(self) -> None:
|
||||
self._task = self.loop.create_task(self.worker())
|
||||
|
||||
def _cancel_task(self):
|
||||
def _cancel_task(self) -> None:
|
||||
if self._task is not None and not self._task.done():
|
||||
self._task.cancel()
|
||||
|
||||
async def close(self):
|
||||
async def close(self) -> None:
|
||||
self._cancel_task()
|
||||
await self.ws.close(code=1000)
|
||||
|
||||
async def disconnect(self):
|
||||
async def disconnect(self) -> None:
|
||||
await self.close()
|
||||
self._dispatch('shard_disconnect', self.id)
|
||||
self._dispatch("shard_disconnect", self.id)
|
||||
|
||||
async def _handle_disconnect(self, e):
|
||||
self._dispatch('disconnect')
|
||||
self._dispatch('shard_disconnect', self.id)
|
||||
async def _handle_disconnect(self, e: Exception) -> None:
|
||||
self._dispatch("disconnect")
|
||||
self._dispatch("shard_disconnect", self.id)
|
||||
if not self._reconnect:
|
||||
self._queue_put(EventItem(EventType.close, self, e))
|
||||
return
|
||||
@@ -144,11 +156,11 @@ class Shard:
|
||||
return
|
||||
|
||||
retry = self._backoff.delay()
|
||||
log.error('Attempting a reconnect for shard ID %s in %.2fs', self.id, retry, exc_info=e)
|
||||
_log.error("Attempting a reconnect for shard ID %s in %.2fs", self.id, retry, exc_info=e)
|
||||
await asyncio.sleep(retry)
|
||||
self._queue_put(EventItem(EventType.reconnect, self, e))
|
||||
|
||||
async def worker(self):
|
||||
async def worker(self) -> None:
|
||||
while not self._client.is_closed():
|
||||
try:
|
||||
await self.ws.poll_event()
|
||||
@@ -165,14 +177,19 @@ class Shard:
|
||||
self._queue_put(EventItem(EventType.terminate, self, e))
|
||||
break
|
||||
|
||||
async def reidentify(self, exc):
|
||||
async def reidentify(self, exc: ReconnectWebSocket) -> None:
|
||||
self._cancel_task()
|
||||
self._dispatch('disconnect')
|
||||
self._dispatch('shard_disconnect', self.id)
|
||||
log.info('Got a request to %s the websocket at Shard ID %s.', exc.op, self.id)
|
||||
self._dispatch("disconnect")
|
||||
self._dispatch("shard_disconnect", self.id)
|
||||
_log.info("Got a request to %s the websocket at Shard ID %s.", exc.op, self.id)
|
||||
try:
|
||||
coro = DiscordWebSocket.from_client(self._client, resume=exc.resume, shard_id=self.id,
|
||||
session=self.ws.session_id, sequence=self.ws.sequence)
|
||||
coro = DiscordWebSocket.from_client(
|
||||
self._client,
|
||||
resume=exc.resume,
|
||||
shard_id=self.id,
|
||||
session=self.ws.session_id,
|
||||
sequence=self.ws.sequence,
|
||||
)
|
||||
self.ws = await asyncio.wait_for(coro, timeout=60.0)
|
||||
except self._handled_exceptions as e:
|
||||
await self._handle_disconnect(e)
|
||||
@@ -183,7 +200,7 @@ class Shard:
|
||||
else:
|
||||
self.launch()
|
||||
|
||||
async def reconnect(self):
|
||||
async def reconnect(self) -> None:
|
||||
self._cancel_task()
|
||||
try:
|
||||
coro = DiscordWebSocket.from_client(self._client, shard_id=self.id)
|
||||
@@ -197,6 +214,7 @@ class Shard:
|
||||
else:
|
||||
self.launch()
|
||||
|
||||
|
||||
class ShardInfo:
|
||||
"""A class that gives information and control over a specific shard.
|
||||
|
||||
@@ -213,18 +231,18 @@ class ShardInfo:
|
||||
The shard count for this cluster. If this is ``None`` then the bot has not started yet.
|
||||
"""
|
||||
|
||||
__slots__ = ('_parent', 'id', 'shard_count')
|
||||
__slots__ = ("_parent", "id", "shard_count")
|
||||
|
||||
def __init__(self, parent, shard_count):
|
||||
self._parent = parent
|
||||
self.id = parent.id
|
||||
self.shard_count = shard_count
|
||||
def __init__(self, parent: Shard, shard_count: Optional[int]) -> None:
|
||||
self._parent: Shard = parent
|
||||
self.id: int = parent.id
|
||||
self.shard_count: Optional[int] = shard_count
|
||||
|
||||
def is_closed(self):
|
||||
def is_closed(self) -> bool:
|
||||
""":class:`bool`: Whether the shard connection is currently closed."""
|
||||
return not self._parent.ws.open
|
||||
|
||||
async def disconnect(self):
|
||||
async def disconnect(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Disconnects a shard. When this is called, the shard connection will no
|
||||
@@ -237,7 +255,7 @@ class ShardInfo:
|
||||
|
||||
await self._parent.disconnect()
|
||||
|
||||
async def reconnect(self):
|
||||
async def reconnect(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Disconnects and then connects the shard again.
|
||||
@@ -246,7 +264,7 @@ class ShardInfo:
|
||||
await self._parent.disconnect()
|
||||
await self._parent.reconnect()
|
||||
|
||||
async def connect(self):
|
||||
async def connect(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Connects a shard. If the shard is already connected this does nothing.
|
||||
@@ -257,11 +275,11 @@ class ShardInfo:
|
||||
await self._parent.reconnect()
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
def latency(self) -> float:
|
||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard."""
|
||||
return self._parent.ws.latency
|
||||
|
||||
def is_ws_ratelimited(self):
|
||||
def is_ws_ratelimited(self) -> bool:
|
||||
""":class:`bool`: Whether the websocket is currently rate limited.
|
||||
|
||||
This can be useful to know when deciding whether you should query members
|
||||
@@ -271,6 +289,7 @@ class ShardInfo:
|
||||
"""
|
||||
return self._parent.ws.is_ratelimited()
|
||||
|
||||
|
||||
class AutoShardedClient(Client):
|
||||
"""A client similar to :class:`Client` except it handles the complications
|
||||
of sharding for the user into a more manageable and transparent single
|
||||
@@ -297,16 +316,20 @@ class AutoShardedClient(Client):
|
||||
shard_ids: Optional[List[:class:`int`]]
|
||||
An optional list of shard_ids to launch the shards with.
|
||||
"""
|
||||
def __init__(self, *args, loop=None, **kwargs):
|
||||
kwargs.pop('shard_id', None)
|
||||
self.shard_ids = kwargs.pop('shard_ids', None)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_connection: AutoShardedConnectionState
|
||||
|
||||
def __init__(self, *args: Any, loop: Optional[asyncio.AbstractEventLoop] = None, **kwargs: Any) -> None:
|
||||
kwargs.pop("shard_id", None)
|
||||
self.shard_ids: Optional[List[int]] = kwargs.pop("shard_ids", None)
|
||||
super().__init__(*args, loop=loop, **kwargs)
|
||||
|
||||
if self.shard_ids is not None:
|
||||
if self.shard_count is None:
|
||||
raise ClientException('When passing manual shard_ids, you must provide a shard_count.')
|
||||
raise ClientException("When passing manual shard_ids, you must provide a shard_count.")
|
||||
elif not isinstance(self.shard_ids, (list, tuple)):
|
||||
raise ClientException('shard_ids parameter must be a list or a tuple.')
|
||||
raise ClientException("shard_ids parameter must be a list or a tuple.")
|
||||
|
||||
# instead of a single websocket, we have multiple
|
||||
# the key is the shard_id
|
||||
@@ -315,18 +338,24 @@ class AutoShardedClient(Client):
|
||||
self._connection._get_client = lambda: self
|
||||
self.__queue = asyncio.PriorityQueue()
|
||||
|
||||
def _get_websocket(self, guild_id=None, *, shard_id=None):
|
||||
def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket:
|
||||
if shard_id is None:
|
||||
shard_id = (guild_id >> 22) % self.shard_count
|
||||
# guild_id won't be None if shard_id is None and shard_count won't be None here
|
||||
shard_id = (guild_id >> 22) % self.shard_count # type: ignore
|
||||
return self.__shards[shard_id].ws
|
||||
|
||||
def _get_state(self, **options):
|
||||
return AutoShardedConnectionState(dispatch=self.dispatch,
|
||||
handlers=self._handlers,
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
def _get_state(self, **options: Any) -> AutoShardedConnectionState:
|
||||
return AutoShardedConnectionState(
|
||||
dispatch=self.dispatch,
|
||||
handlers=self._handlers,
|
||||
hooks=self._hooks,
|
||||
http=self.http,
|
||||
loop=self.loop,
|
||||
**options,
|
||||
)
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
def latency(self) -> float:
|
||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
|
||||
|
||||
This operates similarly to :meth:`Client.latency` except it uses the average
|
||||
@@ -334,18 +363,18 @@ class AutoShardedClient(Client):
|
||||
:attr:`latencies` property. Returns ``nan`` if there are no shards ready.
|
||||
"""
|
||||
if not self.__shards:
|
||||
return float('nan')
|
||||
return float("nan")
|
||||
return sum(latency for _, latency in self.latencies) / len(self.__shards)
|
||||
|
||||
@property
|
||||
def latencies(self):
|
||||
def latencies(self) -> List[Tuple[int, float]]:
|
||||
"""List[Tuple[:class:`int`, :class:`float`]]: A list of latencies between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
|
||||
|
||||
This returns a list of tuples with elements ``(shard_id, latency)``.
|
||||
"""
|
||||
return [(shard_id, shard.ws.latency) for shard_id, shard in self.__shards.items()]
|
||||
|
||||
def get_shard(self, shard_id):
|
||||
def get_shard(self, shard_id: int) -> Optional[ShardInfo]:
|
||||
"""Optional[:class:`ShardInfo`]: Gets the shard information at a given shard ID or ``None`` if not found."""
|
||||
try:
|
||||
parent = self.__shards[shard_id]
|
||||
@@ -355,52 +384,16 @@ class AutoShardedClient(Client):
|
||||
return ShardInfo(parent, self.shard_count)
|
||||
|
||||
@property
|
||||
def shards(self):
|
||||
def shards(self) -> Dict[int, ShardInfo]:
|
||||
"""Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object."""
|
||||
return { shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items() }
|
||||
return {shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items()}
|
||||
|
||||
@utils.deprecated('Guild.chunk')
|
||||
async def request_offline_members(self, *guilds):
|
||||
r"""|coro|
|
||||
|
||||
Requests previously offline members from the guild to be filled up
|
||||
into the :attr:`Guild.members` cache. This function is usually not
|
||||
called. It should only be used if you have the ``fetch_offline_members``
|
||||
parameter set to ``False``.
|
||||
|
||||
When the client logs on and connects to the websocket, Discord does
|
||||
not provide the library with offline members if the number of members
|
||||
in the guild is larger than 250. You can check if a guild is large
|
||||
if :attr:`Guild.large` is ``True``.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is deprecated. Use :meth:`Guild.chunk` instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*guilds: :class:`Guild`
|
||||
An argument list of guilds to request offline members for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
If any guild is unavailable in the collection.
|
||||
"""
|
||||
if any(g.unavailable for g in guilds):
|
||||
raise InvalidArgument('An unavailable or non-large guild was passed.')
|
||||
|
||||
_guilds = sorted(guilds, key=lambda g: g.shard_id)
|
||||
for shard_id, sub_guilds in itertools.groupby(_guilds, key=lambda g: g.shard_id):
|
||||
for guild in sub_guilds:
|
||||
await self._connection.chunk_guild(guild)
|
||||
|
||||
async def launch_shard(self, gateway, shard_id, *, initial=False):
|
||||
async def launch_shard(self, gateway: str, shard_id: int, *, initial: bool = False) -> None:
|
||||
try:
|
||||
coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id)
|
||||
ws = await asyncio.wait_for(coro, timeout=180.0)
|
||||
except Exception:
|
||||
log.exception('Failed to connect for shard_id: %s. Retrying...', shard_id)
|
||||
_log.exception("Failed to connect for shard_id: %s. Retrying...", shard_id)
|
||||
await asyncio.sleep(5.0)
|
||||
return await self.launch_shard(gateway, shard_id)
|
||||
|
||||
@@ -408,7 +401,7 @@ class AutoShardedClient(Client):
|
||||
self.__shards[shard_id] = ret = Shard(ws, self, self.__queue.put_nowait)
|
||||
ret.launch()
|
||||
|
||||
async def launch_shards(self):
|
||||
async def launch_shards(self) -> None:
|
||||
if self.shard_count is None:
|
||||
self.shard_count, gateway = await self.http.get_bot_gateway()
|
||||
else:
|
||||
@@ -425,7 +418,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
self._connection.shards_launched.set()
|
||||
|
||||
async def connect(self, *, reconnect=True):
|
||||
async def connect(self, *, reconnect: bool = True) -> None:
|
||||
self._reconnect = reconnect
|
||||
await self.launch_shards()
|
||||
|
||||
@@ -449,7 +442,7 @@ class AutoShardedClient(Client):
|
||||
elif item.type == EventType.clean_close:
|
||||
return
|
||||
|
||||
async def close(self):
|
||||
async def close(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Closes the connection to Discord.
|
||||
@@ -461,7 +454,7 @@ class AutoShardedClient(Client):
|
||||
|
||||
for vc in self.voice_clients:
|
||||
try:
|
||||
await vc.disconnect()
|
||||
await vc.disconnect(force=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -472,7 +465,13 @@ class AutoShardedClient(Client):
|
||||
await self.http.close()
|
||||
self.__queue.put_nowait(EventItem(EventType.clean_close, None, None))
|
||||
|
||||
async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
|
||||
async def change_presence(
|
||||
self,
|
||||
*,
|
||||
activity: Optional[BaseActivity] = None,
|
||||
status: Optional[Status] = None,
|
||||
shard_id: int = None,
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Changes the client's presence.
|
||||
@@ -482,6 +481,9 @@ class AutoShardedClient(Client):
|
||||
game = discord.Game("with the API")
|
||||
await client.change_presence(status=discord.Status.idle, activity=game)
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Removed the ``afk`` keyword-only parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
activity: Optional[:class:`BaseActivity`]
|
||||
@@ -489,10 +491,6 @@ class AutoShardedClient(Client):
|
||||
status: Optional[:class:`Status`]
|
||||
Indicates what status to change to. If ``None``, then
|
||||
:attr:`Status.online` is used.
|
||||
afk: :class:`bool`
|
||||
Indicates if you are going AFK. This allows the discord
|
||||
client to know how to handle push notifications better
|
||||
for you in case you are actually idle and not lying.
|
||||
shard_id: Optional[:class:`int`]
|
||||
The shard_id to change the presence to. If not specified
|
||||
or ``None``, then it will change the presence of every
|
||||
@@ -505,23 +503,23 @@ class AutoShardedClient(Client):
|
||||
"""
|
||||
|
||||
if status is None:
|
||||
status = 'online'
|
||||
status_value = "online"
|
||||
status_enum = Status.online
|
||||
elif status is Status.offline:
|
||||
status = 'invisible'
|
||||
status_value = "invisible"
|
||||
status_enum = Status.offline
|
||||
else:
|
||||
status_enum = status
|
||||
status = str(status)
|
||||
status_value = str(status)
|
||||
|
||||
if shard_id is None:
|
||||
for shard in self.__shards.values():
|
||||
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
|
||||
await shard.ws.change_presence(activity=activity, status=status_value)
|
||||
|
||||
guilds = self._connection.guilds
|
||||
else:
|
||||
shard = self.__shards[shard_id]
|
||||
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
|
||||
await shard.ws.change_presence(activity=activity, status=status_value)
|
||||
guilds = [g for g in self._connection.guilds if g.shard_id == shard_id]
|
||||
|
||||
activities = () if activity is None else (activity,)
|
||||
@@ -530,10 +528,11 @@ class AutoShardedClient(Client):
|
||||
if me is None:
|
||||
continue
|
||||
|
||||
me.activities = activities
|
||||
# Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...]
|
||||
me.activities = activities # type: ignore
|
||||
me.status = status_enum
|
||||
|
||||
def is_ws_ratelimited(self):
|
||||
def is_ws_ratelimited(self) -> bool:
|
||||
""":class:`bool`: Whether the websocket is currently rate limited.
|
||||
|
||||
This can be useful to know when deciding whether you should query members
|
||||
|
||||
180
discord/stage_instance.py
Normal file
180
discord/stage_instance.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from .utils import MISSING, cached_slot_property
|
||||
from .mixins import Hashable
|
||||
from .errors import InvalidArgument
|
||||
from .enums import StagePrivacyLevel, try_enum
|
||||
|
||||
__all__ = ("StageInstance",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.channel import StageInstance as StageInstancePayload
|
||||
from .state import ConnectionState
|
||||
from .channel import StageChannel
|
||||
from .guild import Guild
|
||||
|
||||
|
||||
class StageInstance(Hashable):
|
||||
"""Represents a stage instance of a stage channel in a guild.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two stage instances are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two stage instances are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the stage instance's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the stage instance's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The stage instance's ID.
|
||||
guild: :class:`Guild`
|
||||
The guild that the stage instance is running in.
|
||||
channel_id: :class:`int`
|
||||
The ID of the channel that the stage instance is running in.
|
||||
topic: :class:`str`
|
||||
The topic of the stage instance.
|
||||
privacy_level: :class:`StagePrivacyLevel`
|
||||
The privacy level of the stage instance.
|
||||
discoverable_disabled: :class:`bool`
|
||||
Whether discoverability for the stage instance is disabled.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"id",
|
||||
"guild",
|
||||
"channel_id",
|
||||
"topic",
|
||||
"privacy_level",
|
||||
"discoverable_disabled",
|
||||
"_cs_channel",
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, guild: Guild, data: StageInstancePayload) -> None:
|
||||
self._state = state
|
||||
self.guild = guild
|
||||
self._update(data)
|
||||
|
||||
def _update(self, data: StageInstancePayload):
|
||||
self.id: int = int(data["id"])
|
||||
self.channel_id: int = int(data["channel_id"])
|
||||
self.topic: str = data["topic"]
|
||||
self.privacy_level: StagePrivacyLevel = try_enum(StagePrivacyLevel, data["privacy_level"])
|
||||
self.discoverable_disabled: bool = data.get("discoverable_disabled", False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<StageInstance id={self.id} guild={self.guild!r} channel_id={self.channel_id} topic={self.topic!r}>"
|
||||
|
||||
@cached_slot_property("_cs_channel")
|
||||
def channel(self) -> Optional[StageChannel]:
|
||||
"""Optional[:class:`StageChannel`]: The channel that stage instance is running in."""
|
||||
# the returned channel will always be a StageChannel or None
|
||||
return self._state.get_channel(self.channel_id) # type: ignore
|
||||
|
||||
def is_public(self) -> bool:
|
||||
return self.privacy_level is StagePrivacyLevel.public
|
||||
|
||||
async def edit(
|
||||
self, *, topic: str = MISSING, privacy_level: StagePrivacyLevel = MISSING, reason: Optional[str] = None
|
||||
) -> None:
|
||||
"""|coro|
|
||||
|
||||
Edits the stage instance.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
topic: :class:`str`
|
||||
The stage instance's new topic.
|
||||
privacy_level: :class:`StagePrivacyLevel`
|
||||
The stage instance's new privacy level.
|
||||
reason: :class:`str`
|
||||
The reason the stage instance was edited. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If the ``privacy_level`` parameter is not the proper type.
|
||||
Forbidden
|
||||
You do not have permissions to edit the stage instance.
|
||||
HTTPException
|
||||
Editing a stage instance failed.
|
||||
"""
|
||||
|
||||
payload = {}
|
||||
|
||||
if topic is not MISSING:
|
||||
payload["topic"] = topic
|
||||
|
||||
if privacy_level is not MISSING:
|
||||
if not isinstance(privacy_level, StagePrivacyLevel):
|
||||
raise InvalidArgument("privacy_level field must be of type PrivacyLevel")
|
||||
|
||||
payload["privacy_level"] = privacy_level.value
|
||||
|
||||
if payload:
|
||||
await self._state.http.edit_stage_instance(self.channel_id, **payload, reason=reason)
|
||||
|
||||
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the stage instance.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: :class:`str`
|
||||
The reason the stage instance was deleted. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have permissions to delete the stage instance.
|
||||
HTTPException
|
||||
Deleting the stage instance failed.
|
||||
"""
|
||||
await self._state.http.delete_stage_instance(self.channel_id, reason=reason)
|
||||
1260
discord/state.py
1260
discord/state.py
File diff suppressed because it is too large
Load Diff
@@ -22,17 +22,228 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Literal, TYPE_CHECKING, List, Optional, Tuple, Type, Union
|
||||
import unicodedata
|
||||
|
||||
from .mixins import Hashable
|
||||
from .asset import Asset
|
||||
from .utils import snowflake_time
|
||||
from .enums import StickerType, try_enum
|
||||
from .asset import Asset, AssetMixin
|
||||
from .utils import cached_slot_property, find, snowflake_time, get, MISSING
|
||||
from .errors import InvalidData
|
||||
from .enums import StickerType, StickerFormatType, try_enum
|
||||
|
||||
__all__ = (
|
||||
'Sticker',
|
||||
"StickerPack",
|
||||
"StickerItem",
|
||||
"Sticker",
|
||||
"StandardSticker",
|
||||
"GuildSticker",
|
||||
)
|
||||
|
||||
class Sticker(Hashable):
|
||||
"""Represents a sticker
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
from .state import ConnectionState
|
||||
from .user import User
|
||||
from .guild import Guild
|
||||
from .types.sticker import (
|
||||
StickerPack as StickerPackPayload,
|
||||
StickerItem as StickerItemPayload,
|
||||
Sticker as StickerPayload,
|
||||
StandardSticker as StandardStickerPayload,
|
||||
GuildSticker as GuildStickerPayload,
|
||||
ListPremiumStickerPacks as ListPremiumStickerPacksPayload,
|
||||
EditGuildSticker,
|
||||
)
|
||||
|
||||
|
||||
class StickerPack(Hashable):
|
||||
"""Represents a sticker pack.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the name of the sticker pack.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the sticker pack.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the ID of the sticker pack.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the sticker pack is equal to another sticker pack.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the sticker pack is not equal to another sticker pack.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the sticker pack.
|
||||
description: :class:`str`
|
||||
The description of the sticker pack.
|
||||
id: :class:`int`
|
||||
The id of the sticker pack.
|
||||
stickers: List[:class:`StandardSticker`]
|
||||
The stickers of this sticker pack.
|
||||
sku_id: :class:`int`
|
||||
The SKU ID of the sticker pack.
|
||||
cover_sticker_id: :class:`int`
|
||||
The ID of the sticker used for the cover of the sticker pack.
|
||||
cover_sticker: :class:`StandardSticker`
|
||||
The sticker used for the cover of the sticker pack.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"id",
|
||||
"stickers",
|
||||
"name",
|
||||
"sku_id",
|
||||
"cover_sticker_id",
|
||||
"cover_sticker",
|
||||
"description",
|
||||
"_banner",
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: StickerPackPayload) -> None:
|
||||
self._state: ConnectionState = state
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data: StickerPackPayload) -> None:
|
||||
self.id: int = int(data["id"])
|
||||
stickers = data["stickers"]
|
||||
self.stickers: List[StandardSticker] = [
|
||||
StandardSticker(state=self._state, data=sticker) for sticker in stickers
|
||||
]
|
||||
self.name: str = data["name"]
|
||||
self.sku_id: int = int(data["sku_id"])
|
||||
self.cover_sticker_id: int = int(data["cover_sticker_id"])
|
||||
self.cover_sticker: StandardSticker = get(self.stickers, id=self.cover_sticker_id) # type: ignore
|
||||
self.description: str = data["description"]
|
||||
self._banner: int = int(data["banner_asset_id"])
|
||||
|
||||
@property
|
||||
def banner(self) -> Asset:
|
||||
""":class:`Asset`: The banner asset of the sticker pack."""
|
||||
return Asset._from_sticker_banner(self._state, self._banner)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<StickerPack id={self.id} name={self.name!r} description={self.description!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class _StickerTag(Hashable, AssetMixin):
|
||||
__slots__ = ()
|
||||
|
||||
id: int
|
||||
format: StickerFormatType
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the content of this sticker as a :class:`bytes` object.
|
||||
|
||||
.. note::
|
||||
|
||||
Stickers that use the :attr:`StickerFormatType.lottie` format cannot be read.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Downloading the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
TypeError
|
||||
The sticker is a lottie type.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bytes`
|
||||
The content of the asset.
|
||||
"""
|
||||
if self.format is StickerFormatType.lottie:
|
||||
raise TypeError('Cannot read stickers of format "lottie".')
|
||||
return await super().read()
|
||||
|
||||
|
||||
class StickerItem(_StickerTag):
|
||||
"""Represents a sticker item.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the name of the sticker item.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the sticker item is equal to another sticker item.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the sticker item is not equal to another sticker item.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The sticker's name.
|
||||
id: :class:`int`
|
||||
The id of the sticker.
|
||||
format: :class:`StickerFormatType`
|
||||
The format for the sticker's image.
|
||||
url: :class:`str`
|
||||
The URL for the sticker's image.
|
||||
"""
|
||||
|
||||
__slots__ = ("_state", "name", "id", "format", "url")
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: StickerItemPayload):
|
||||
self._state: ConnectionState = state
|
||||
self.name: str = data["name"]
|
||||
self.id: int = int(data["id"])
|
||||
self.format: StickerFormatType = try_enum(StickerFormatType, data["format_type"])
|
||||
self.url: str = f"{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<StickerItem id={self.id} name={self.name!r} format={self.format}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
async def fetch(self) -> Union[Sticker, StandardSticker, GuildSticker]:
|
||||
"""|coro|
|
||||
|
||||
Attempts to retrieve the full sticker data of the sticker item.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Retrieving the sticker failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Union[:class:`StandardSticker`, :class:`GuildSticker`]
|
||||
The retrieved sticker.
|
||||
"""
|
||||
data: StickerPayload = await self._state.http.get_sticker(self.id)
|
||||
cls, _ = _sticker_factory(data["type"]) # type: ignore
|
||||
return cls(state=self._state, data=data)
|
||||
|
||||
|
||||
class Sticker(_StickerTag):
|
||||
"""Represents a sticker.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
@@ -40,102 +251,285 @@ class Sticker(Hashable):
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the name of the sticker
|
||||
Returns the name of the sticker.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the sticker is equal to another sticker
|
||||
Checks if the sticker is equal to another sticker.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the sticker is not equal to another sticker
|
||||
Checks if the sticker is not equal to another sticker.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The sticker's name
|
||||
The sticker's name.
|
||||
id: :class:`int`
|
||||
The id of the sticker
|
||||
The id of the sticker.
|
||||
description: :class:`str`
|
||||
The description of the sticker
|
||||
The description of the sticker.
|
||||
pack_id: :class:`int`
|
||||
The id of the sticker's pack
|
||||
format: :class:`StickerType`
|
||||
The format for the sticker's image
|
||||
image: :class:`str`
|
||||
The sticker's image
|
||||
tags: List[:class:`str`]
|
||||
A list of tags for the sticker
|
||||
preview_image: Optional[:class:`str`]
|
||||
The sticker's preview asset hash
|
||||
The id of the sticker's pack.
|
||||
format: :class:`StickerFormatType`
|
||||
The format for the sticker's image.
|
||||
url: :class:`str`
|
||||
The URL for the sticker's image.
|
||||
"""
|
||||
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags', 'preview_image')
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.description = data['description']
|
||||
self.pack_id = int(data['pack_id'])
|
||||
self.format = try_enum(StickerType, data['format_type'])
|
||||
self.image = data['asset']
|
||||
__slots__ = ("_state", "id", "name", "description", "format", "url")
|
||||
|
||||
try:
|
||||
self.tags = [tag.strip() for tag in data['tags'].split(',')]
|
||||
except KeyError:
|
||||
self.tags = []
|
||||
def __init__(self, *, state: ConnectionState, data: StickerPayload) -> None:
|
||||
self._state: ConnectionState = state
|
||||
self._from_data(data)
|
||||
|
||||
self.preview_image = data.get('preview_asset')
|
||||
def _from_data(self, data: StickerPayload) -> None:
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self.description: str = data["description"]
|
||||
self.format: StickerFormatType = try_enum(StickerFormatType, data["format_type"])
|
||||
self.url: str = f"{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}"
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return f"<Sticker id={self.id} name={self.name!r}>"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
def created_at(self) -> datetime.datetime:
|
||||
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
"""Returns an :class:`Asset` for the sticker's image.
|
||||
|
||||
.. note::
|
||||
This will return ``None`` if the format is ``StickerType.lottie``
|
||||
class StandardSticker(Sticker):
|
||||
"""Represents a sticker that is found in a standard sticker pack.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the name of the sticker.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the sticker is equal to another sticker.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the sticker is not equal to another sticker.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The sticker's name.
|
||||
id: :class:`int`
|
||||
The id of the sticker.
|
||||
description: :class:`str`
|
||||
The description of the sticker.
|
||||
pack_id: :class:`int`
|
||||
The id of the sticker's pack.
|
||||
format: :class:`StickerFormatType`
|
||||
The format for the sticker's image.
|
||||
tags: List[:class:`str`]
|
||||
A list of tags for the sticker.
|
||||
sort_value: :class:`int`
|
||||
The sticker's sort order within its pack.
|
||||
"""
|
||||
|
||||
__slots__ = ("sort_value", "pack_id", "type", "tags")
|
||||
|
||||
def _from_data(self, data: StandardStickerPayload) -> None:
|
||||
super()._from_data(data)
|
||||
self.sort_value: int = data["sort_value"]
|
||||
self.pack_id: int = int(data["pack_id"])
|
||||
self.type: StickerType = StickerType.standard
|
||||
|
||||
try:
|
||||
self.tags: List[str] = [tag.strip() for tag in data["tags"].split(",")]
|
||||
except KeyError:
|
||||
self.tags = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<StandardSticker id={self.id} name={self.name!r} pack_id={self.pack_id}>"
|
||||
|
||||
async def pack(self) -> StickerPack:
|
||||
"""|coro|
|
||||
|
||||
Retrieves the sticker pack that this sticker belongs to.
|
||||
|
||||
Raises
|
||||
--------
|
||||
InvalidData
|
||||
The corresponding sticker pack was not found.
|
||||
HTTPException
|
||||
Retrieving the sticker pack failed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Asset`]
|
||||
The resulting CDN asset.
|
||||
--------
|
||||
:class:`StickerPack`
|
||||
The retrieved sticker pack.
|
||||
"""
|
||||
return self.image_url_as()
|
||||
data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs()
|
||||
packs = data["sticker_packs"]
|
||||
pack = find(lambda d: int(d["id"]) == self.pack_id, packs)
|
||||
|
||||
def image_url_as(self, *, size=1024):
|
||||
"""Optionally returns an :class:`Asset` for the sticker's image.
|
||||
if pack:
|
||||
return StickerPack(state=self._state, data=pack)
|
||||
raise InvalidData(f"Could not find corresponding sticker pack for {self!r}")
|
||||
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. note::
|
||||
This will return ``None`` if the format is ``StickerType.lottie``.
|
||||
class GuildSticker(Sticker):
|
||||
"""Represents a sticker that belongs to a guild.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the name of the sticker.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the sticker is equal to another sticker.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the sticker is not equal to another sticker.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
The sticker's name.
|
||||
id: :class:`int`
|
||||
The id of the sticker.
|
||||
description: :class:`str`
|
||||
The description of the sticker.
|
||||
format: :class:`StickerFormatType`
|
||||
The format for the sticker's image.
|
||||
available: :class:`bool`
|
||||
Whether this sticker is available for use.
|
||||
guild_id: :class:`int`
|
||||
The ID of the guild that this sticker is from.
|
||||
user: Optional[:class:`User`]
|
||||
The user that created this sticker. This can only be retrieved using :meth:`Guild.fetch_sticker` and
|
||||
having the :attr:`~Permissions.manage_emojis_and_stickers` permission.
|
||||
emoji: :class:`str`
|
||||
The name of a unicode emoji that represents this sticker.
|
||||
"""
|
||||
|
||||
__slots__ = ("available", "guild_id", "user", "emoji", "type", "_cs_guild")
|
||||
|
||||
def _from_data(self, data: GuildStickerPayload) -> None:
|
||||
super()._from_data(data)
|
||||
self.available: bool = data["available"]
|
||||
self.guild_id: int = int(data["guild_id"])
|
||||
user = data.get("user")
|
||||
self.user: Optional[User] = self._state.store_user(user) if user else None
|
||||
self.emoji: str = data["tags"]
|
||||
self.type: StickerType = StickerType.guild
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GuildSticker name={self.name!r} id={self.id} guild_id={self.guild_id} user={self.user!r}>"
|
||||
|
||||
@cached_slot_property("_cs_guild")
|
||||
def guild(self) -> Optional[Guild]:
|
||||
"""Optional[:class:`Guild`]: The guild that this sticker is from.
|
||||
Could be ``None`` if the bot is not in the guild.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
emoji: str = MISSING,
|
||||
reason: Optional[str] = None,
|
||||
) -> GuildSticker:
|
||||
"""|coro|
|
||||
|
||||
Edits a :class:`GuildSticker` for the guild.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
name: :class:`str`
|
||||
The sticker's new name. Must be at least 2 characters.
|
||||
description: Optional[:class:`str`]
|
||||
The sticker's new description. Can be ``None``.
|
||||
emoji: :class:`str`
|
||||
The name of a unicode emoji that represents the sticker's expression.
|
||||
reason: :class:`str`
|
||||
The reason for editing this sticker. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Invalid ``size``.
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to edit stickers.
|
||||
HTTPException
|
||||
An error occurred editing the sticker.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Asset`]
|
||||
The resulting CDN asset or ``None``.
|
||||
--------
|
||||
:class:`GuildSticker`
|
||||
The newly modified sticker.
|
||||
"""
|
||||
if self.format is StickerType.lottie:
|
||||
return None
|
||||
payload: EditGuildSticker = {}
|
||||
|
||||
return Asset._from_sticker_url(self._state, self, size=size)
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
|
||||
if description is not MISSING:
|
||||
payload["description"] = description
|
||||
|
||||
if emoji is not MISSING:
|
||||
try:
|
||||
emoji = unicodedata.name(emoji)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
emoji = emoji.replace(" ", "_")
|
||||
|
||||
payload["tags"] = emoji
|
||||
|
||||
data: GuildStickerPayload = await self._state.http.modify_guild_sticker(self.guild_id, self.id, payload, reason)
|
||||
return GuildSticker(state=self._state, data=data)
|
||||
|
||||
async def delete(self, *, reason: Optional[str] = None) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes the custom :class:`Sticker` from the guild.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis_and_stickers` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for deleting this sticker. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to delete stickers.
|
||||
HTTPException
|
||||
An error occurred deleting the sticker.
|
||||
"""
|
||||
await self._state.http.delete_guild_sticker(self.guild_id, self.id, reason)
|
||||
|
||||
|
||||
def _sticker_factory(
|
||||
sticker_type: Literal[1, 2]
|
||||
) -> Tuple[Type[Union[StandardSticker, GuildSticker, Sticker]], StickerType]:
|
||||
value = try_enum(StickerType, sticker_type)
|
||||
if value == StickerType.standard:
|
||||
return StandardSticker, value
|
||||
elif value == StickerType.guild:
|
||||
return GuildSticker, value
|
||||
else:
|
||||
return Sticker, value
|
||||
|
||||
103
discord/team.py
103
discord/team.py
@@ -22,16 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import utils
|
||||
from .user import BaseUser
|
||||
from .asset import Asset
|
||||
from .enums import TeamMembershipState, try_enum
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import ConnectionState
|
||||
|
||||
from .types.team import (
|
||||
Team as TeamPayload,
|
||||
TeamMember as TeamMemberPayload,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'Team',
|
||||
'TeamMember',
|
||||
"Team",
|
||||
"TeamMember",
|
||||
)
|
||||
|
||||
|
||||
class Team:
|
||||
"""Represents an application team for a bot provided by Discord.
|
||||
|
||||
@@ -41,8 +54,6 @@ class Team:
|
||||
The team ID.
|
||||
name: :class:`str`
|
||||
The team name
|
||||
icon: Optional[:class:`str`]
|
||||
The icon hash, if it exists.
|
||||
owner_id: :class:`int`
|
||||
The team's owner ID.
|
||||
members: List[:class:`TeamMember`]
|
||||
@@ -50,61 +61,34 @@ class Team:
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
__slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members')
|
||||
|
||||
def __init__(self, state, data):
|
||||
self._state = state
|
||||
__slots__ = ("_state", "id", "name", "_icon", "owner_id", "members")
|
||||
|
||||
self.id = utils._get_as_snowflake(data, 'id')
|
||||
self.name = data['name']
|
||||
self.icon = data['icon']
|
||||
self.owner_id = utils._get_as_snowflake(data, 'owner_user_id')
|
||||
self.members = [TeamMember(self, self._state, member) for member in data['members']]
|
||||
def __init__(self, state: ConnectionState, data: TeamPayload):
|
||||
self._state: ConnectionState = state
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name}>'.format(self)
|
||||
self.id: int = int(data["id"])
|
||||
self.name: str = data["name"]
|
||||
self._icon: Optional[str] = data["icon"]
|
||||
self.owner_id: Optional[int] = utils._get_as_snowflake(data, "owner_user_id")
|
||||
self.members: List[TeamMember] = [TeamMember(self, self._state, member) for member in data["members"]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} id={self.id} name={self.name}>"
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`.Asset`: Retrieves the team's icon asset.
|
||||
|
||||
This is equivalent to calling :meth:`icon_url_as` with
|
||||
the default parameters ('webp' format and a size of 1024).
|
||||
"""
|
||||
return self.icon_url_as()
|
||||
|
||||
def icon_url_as(self, *, format='webp', size=1024):
|
||||
"""Returns an :class:`Asset` for the icon the team has.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
|
||||
The size must be a power of 2 between 16 and 4096.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: :class:`str`
|
||||
The format to attempt to convert the icon to. Defaults to 'webp'.
|
||||
size: :class:`int`
|
||||
The size of the image to display.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
return Asset._from_icon(self._state, self, 'team', format=format, size=size)
|
||||
def icon(self) -> Optional[Asset]:
|
||||
"""Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any."""
|
||||
if self._icon is None:
|
||||
return None
|
||||
return Asset._from_icon(self._state, self.id, self._icon, path="team")
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
def owner(self) -> Optional[TeamMember]:
|
||||
"""Optional[:class:`TeamMember`]: The team's owner."""
|
||||
return utils.get(self.members, id=self.owner_id)
|
||||
|
||||
|
||||
class TeamMember(BaseUser):
|
||||
"""Represents a team member in a team.
|
||||
|
||||
@@ -145,14 +129,17 @@ class TeamMember(BaseUser):
|
||||
membership_state: :class:`TeamMembershipState`
|
||||
The membership state of the member (e.g. invited or accepted)
|
||||
"""
|
||||
__slots__ = BaseUser.__slots__ + ('team', 'membership_state', 'permissions')
|
||||
|
||||
def __init__(self, team, state, data):
|
||||
self.team = team
|
||||
self.membership_state = try_enum(TeamMembershipState, data['membership_state'])
|
||||
self.permissions = data['permissions']
|
||||
super().__init__(state=state, data=data['user'])
|
||||
__slots__ = ("team", "membership_state", "permissions")
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0.__class__.__name__} id={0.id} name={0.name!r} ' \
|
||||
'discriminator={0.discriminator!r} membership_state={0.membership_state!r}>'.format(self)
|
||||
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload):
|
||||
self.team: Team = team
|
||||
self.membership_state: TeamMembershipState = try_enum(TeamMembershipState, data["membership_state"])
|
||||
self.permissions: List[str] = data["permissions"]
|
||||
super().__init__(state=state, data=data["user"])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
|
||||
f"discriminator={self.discriminator!r} membership_state={self.membership_state!r}>"
|
||||
)
|
||||
|
||||
@@ -22,19 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data, MISSING
|
||||
from .enums import VoiceRegion
|
||||
from .guild import Guild
|
||||
|
||||
__all__ = (
|
||||
'Template',
|
||||
)
|
||||
__all__ = ("Template",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
from .types.template import Template as TemplatePayload
|
||||
from .state import ConnectionState
|
||||
from .user import User
|
||||
|
||||
|
||||
class _FriendlyHttpAttributeErrorHelper:
|
||||
__slots__ = ()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
raise AttributeError('PartialTemplateState does not support http methods.')
|
||||
raise AttributeError("PartialTemplateState does not support http methods.")
|
||||
|
||||
|
||||
class _PartialTemplateState:
|
||||
def __init__(self, *, state):
|
||||
@@ -66,11 +75,15 @@ class _PartialTemplateState:
|
||||
def _get_message(self, id):
|
||||
return None
|
||||
|
||||
async def query_members(self, **kwargs):
|
||||
def _get_guild(self, id):
|
||||
return self.__state._get_guild(id)
|
||||
|
||||
async def query_members(self, **kwargs: Any):
|
||||
return []
|
||||
|
||||
def __getattr__(self, attr):
|
||||
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
|
||||
raise AttributeError(f"PartialTemplateState does not support {attr!r}.")
|
||||
|
||||
|
||||
class Template:
|
||||
"""Represents a Discord template.
|
||||
@@ -96,40 +109,62 @@ class Template:
|
||||
This is referred to as "last synced" in the official Discord client.
|
||||
source_guild: :class:`Guild`
|
||||
The source guild.
|
||||
is_dirty: Optional[:class:`bool`]
|
||||
Whether the template has unsynced changes.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
__slots__ = (
|
||||
"code",
|
||||
"uses",
|
||||
"name",
|
||||
"description",
|
||||
"creator",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"source_guild",
|
||||
"is_dirty",
|
||||
"_state",
|
||||
)
|
||||
|
||||
def __init__(self, *, state: ConnectionState, data: TemplatePayload) -> None:
|
||||
self._state = state
|
||||
self._store(data)
|
||||
|
||||
def _store(self, data):
|
||||
self.code = data['code']
|
||||
self.uses = data['usage_count']
|
||||
self.name = data['name']
|
||||
self.description = data['description']
|
||||
creator_data = data.get('creator')
|
||||
self.creator = None if creator_data is None else self._state.store_user(creator_data)
|
||||
def _store(self, data: TemplatePayload) -> None:
|
||||
self.code: str = data["code"]
|
||||
self.uses: int = data["usage_count"]
|
||||
self.name: str = data["name"]
|
||||
self.description: Optional[str] = data["description"]
|
||||
creator_data = data.get("creator")
|
||||
self.creator: Optional[User] = None if creator_data is None else self._state.create_user(creator_data)
|
||||
|
||||
self.created_at = parse_time(data.get('created_at'))
|
||||
self.updated_at = parse_time(data.get('updated_at'))
|
||||
self.created_at: Optional[datetime.datetime] = parse_time(data.get("created_at"))
|
||||
self.updated_at: Optional[datetime.datetime] = parse_time(data.get("updated_at"))
|
||||
|
||||
id = _get_as_snowflake(data, 'source_guild_id')
|
||||
|
||||
guild = self._state._get_guild(id)
|
||||
guild_id = int(data["source_guild_id"])
|
||||
guild: Optional[Guild] = self._state._get_guild(guild_id)
|
||||
|
||||
self.source_guild: Guild
|
||||
if guild is None:
|
||||
source_serialised = data['serialized_source_guild']
|
||||
source_serialised['id'] = id
|
||||
source_serialised = data["serialized_source_guild"]
|
||||
source_serialised["id"] = guild_id
|
||||
state = _PartialTemplateState(state=self._state)
|
||||
guild = Guild(data=source_serialised, state=state)
|
||||
# Guild expects a ConnectionState, we're passing a _PartialTemplateState
|
||||
self.source_guild = Guild(data=source_serialised, state=state) # type: ignore
|
||||
else:
|
||||
self.source_guild = guild
|
||||
|
||||
self.source_guild = guild
|
||||
self.is_dirty: Optional[bool] = data.get("is_dirty", None)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Template code={0.code!r} uses={0.uses} name={0.name!r}' \
|
||||
' creator={0.creator!r} source_guild={0.source_guild!r}>'.format(self)
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Template code={self.code!r} uses={self.uses} name={self.name!r}"
|
||||
f" creator={self.creator!r} source_guild={self.source_guild!r} is_dirty={self.is_dirty}>"
|
||||
)
|
||||
|
||||
async def create_guild(self, name, region=None, icon=None):
|
||||
async def create_guild(self, name: str, region: Optional[VoiceRegion] = None, icon: Any = None) -> Guild:
|
||||
"""|coro|
|
||||
|
||||
Creates a :class:`.Guild` using the template.
|
||||
@@ -169,7 +204,7 @@ class Template:
|
||||
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
|
||||
return Guild(data=data, state=self._state)
|
||||
|
||||
async def sync(self):
|
||||
async def sync(self) -> Template:
|
||||
"""|coro|
|
||||
|
||||
Sync the template to the guild's current state.
|
||||
@@ -179,6 +214,9 @@ class Template:
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The template is no longer edited in-place, instead it is returned.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
@@ -187,12 +225,22 @@ class Template:
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Template`
|
||||
The newly edited template.
|
||||
"""
|
||||
|
||||
data = await self._state.http.sync_template(self.source_guild.id, self.code)
|
||||
self._store(data)
|
||||
return Template(state=self._state, data=data)
|
||||
|
||||
async def edit(self, **kwargs):
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: Optional[str] = MISSING,
|
||||
) -> Template:
|
||||
"""|coro|
|
||||
|
||||
Edit the template metadata.
|
||||
@@ -202,12 +250,15 @@ class Template:
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The template is no longer edited in-place, instead it is returned.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: Optional[:class:`str`]
|
||||
name: :class:`str`
|
||||
The template's new name.
|
||||
description: Optional[:class:`str`]
|
||||
The template's description.
|
||||
The template's new description.
|
||||
|
||||
Raises
|
||||
-------
|
||||
@@ -217,11 +268,23 @@ class Template:
|
||||
You don't have permissions to edit the template.
|
||||
NotFound
|
||||
This template does not exist.
|
||||
"""
|
||||
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
|
||||
self._store(data)
|
||||
|
||||
async def delete(self):
|
||||
Returns
|
||||
--------
|
||||
:class:`Template`
|
||||
The newly edited template.
|
||||
"""
|
||||
payload = {}
|
||||
|
||||
if name is not MISSING:
|
||||
payload["name"] = name
|
||||
if description is not MISSING:
|
||||
payload["description"] = description
|
||||
|
||||
data = await self._state.http.edit_template(self.source_guild.id, self.code, payload)
|
||||
return Template(state=self._state, data=data)
|
||||
|
||||
async def delete(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
Delete the template.
|
||||
@@ -241,3 +304,11 @@ class Template:
|
||||
This template does not exist.
|
||||
"""
|
||||
await self._state.http.delete_template(self.source_guild.id, self.code)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: The template url.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
return f"https://discord.new/{self.code}"
|
||||
|
||||
846
discord/threads.py
Normal file
846
discord/threads.py
Normal file
@@ -0,0 +1,846 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Dict, Iterable, List, Optional, Union, TYPE_CHECKING
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
from .mixins import Hashable
|
||||
from .abc import Messageable
|
||||
from .enums import ChannelType, try_enum
|
||||
from .errors import ClientException
|
||||
from .utils import MISSING, parse_time, _get_as_snowflake
|
||||
|
||||
__all__ = (
|
||||
"Thread",
|
||||
"ThreadMember",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types.threads import (
|
||||
Thread as ThreadPayload,
|
||||
ThreadMember as ThreadMemberPayload,
|
||||
ThreadMetadata,
|
||||
ThreadArchiveDuration,
|
||||
)
|
||||
from .types.snowflake import SnowflakeList
|
||||
from .guild import Guild
|
||||
from .channel import TextChannel, CategoryChannel
|
||||
from .member import Member
|
||||
from .message import Message, PartialMessage
|
||||
from .abc import Snowflake, SnowflakeTime
|
||||
from .role import Role
|
||||
from .permissions import Permissions
|
||||
from .state import ConnectionState
|
||||
|
||||
|
||||
class Thread(Messageable, Hashable):
|
||||
"""Represents a Discord thread.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two threads are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two threads are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the thread's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the thread's ID.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the thread's name.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The thread name.
|
||||
guild: :class:`Guild`
|
||||
The guild the thread belongs to.
|
||||
id: :class:`int`
|
||||
The thread ID.
|
||||
parent_id: :class:`int`
|
||||
The parent :class:`TextChannel` ID this thread belongs to.
|
||||
owner_id: :class:`int`
|
||||
The user's ID that created this thread.
|
||||
last_message_id: Optional[:class:`int`]
|
||||
The last message ID of the message sent to this thread. It may
|
||||
*not* point to an existing or valid message.
|
||||
slowmode_delay: :class:`int`
|
||||
The number of seconds a member must wait between sending messages
|
||||
in this thread. A value of `0` denotes that it is disabled.
|
||||
Bots and users with :attr:`~Permissions.manage_channels` or
|
||||
:attr:`~Permissions.manage_messages` bypass slowmode.
|
||||
message_count: :class:`int`
|
||||
An approximate number of messages in this thread. This caps at 50.
|
||||
member_count: :class:`int`
|
||||
An approximate number of members in this thread. This caps at 50.
|
||||
me: Optional[:class:`ThreadMember`]
|
||||
A thread member representing yourself, if you've joined the thread.
|
||||
This could not be available.
|
||||
archived: :class:`bool`
|
||||
Whether the thread is archived.
|
||||
locked: :class:`bool`
|
||||
Whether the thread is locked.
|
||||
invitable: :class:`bool`
|
||||
Whether non-moderators can add other non-moderators to this thread.
|
||||
This is always ``True`` for public threads.
|
||||
archiver_id: Optional[:class:`int`]
|
||||
The user's ID that archived this thread.
|
||||
auto_archive_duration: :class:`int`
|
||||
The duration in minutes until the thread is automatically archived due to inactivity.
|
||||
Usually a value of 60, 1440, 4320 and 10080.
|
||||
archive_timestamp: :class:`datetime.datetime`
|
||||
An aware timestamp of when the thread's archived status was last updated in UTC.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"name",
|
||||
"id",
|
||||
"guild",
|
||||
"_type",
|
||||
"_state",
|
||||
"_members",
|
||||
"owner_id",
|
||||
"parent_id",
|
||||
"last_message_id",
|
||||
"message_count",
|
||||
"member_count",
|
||||
"slowmode_delay",
|
||||
"me",
|
||||
"locked",
|
||||
"archived",
|
||||
"invitable",
|
||||
"archiver_id",
|
||||
"auto_archive_duration",
|
||||
"archive_timestamp",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild: Guild, state: ConnectionState, data: ThreadPayload):
|
||||
self._state: ConnectionState = state
|
||||
self.guild = guild
|
||||
self._members: Dict[int, ThreadMember] = {}
|
||||
self._from_data(data)
|
||||
|
||||
async def _get_channel(self):
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Thread id={self.id!r} name={self.name!r} parent={self.parent}"
|
||||
f" owner_id={self.owner_id!r} locked={self.locked} archived={self.archived}>"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def _from_data(self, data: ThreadPayload):
|
||||
self.id = int(data["id"])
|
||||
self.parent_id = int(data["parent_id"])
|
||||
self.owner_id = int(data["owner_id"])
|
||||
self.name = data["name"]
|
||||
self._type = try_enum(ChannelType, data["type"])
|
||||
self.last_message_id = _get_as_snowflake(data, "last_message_id")
|
||||
self.slowmode_delay = data.get("rate_limit_per_user", 0)
|
||||
self.message_count = data["message_count"]
|
||||
self.member_count = data["member_count"]
|
||||
self._unroll_metadata(data["thread_metadata"])
|
||||
|
||||
try:
|
||||
member = data["member"]
|
||||
except KeyError:
|
||||
self.me = None
|
||||
else:
|
||||
self.me = ThreadMember(self, member)
|
||||
|
||||
def _unroll_metadata(self, data: ThreadMetadata):
|
||||
self.archived = data["archived"]
|
||||
self.archiver_id = _get_as_snowflake(data, "archiver_id")
|
||||
self.auto_archive_duration = data["auto_archive_duration"]
|
||||
self.archive_timestamp = parse_time(data["archive_timestamp"])
|
||||
self.locked = data.get("locked", False)
|
||||
self.invitable = data.get("invitable", True)
|
||||
|
||||
def _update(self, data):
|
||||
try:
|
||||
self.name = data["name"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.slowmode_delay = data.get("rate_limit_per_user", 0)
|
||||
|
||||
try:
|
||||
self._unroll_metadata(data["thread_metadata"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def type(self) -> ChannelType:
|
||||
""":class:`ChannelType`: The channel's Discord type."""
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def parent(self) -> Optional[TextChannel]:
|
||||
"""Optional[:class:`TextChannel`]: The parent channel this thread belongs to."""
|
||||
return self.guild.get_channel(self.parent_id) # type: ignore
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[Member]:
|
||||
"""Optional[:class:`Member`]: The member this thread belongs to."""
|
||||
return self.guild.get_member(self.owner_id)
|
||||
|
||||
@property
|
||||
def mention(self) -> str:
|
||||
""":class:`str`: The string that allows you to mention the thread."""
|
||||
return f"<#{self.id}>"
|
||||
|
||||
@property
|
||||
def members(self) -> List[ThreadMember]:
|
||||
"""List[:class:`ThreadMember`]: A list of thread members in this thread.
|
||||
|
||||
This requires :attr:`Intents.members` to be properly filled. Most of the time however,
|
||||
this data is not provided by the gateway and a call to :meth:`fetch_members` is
|
||||
needed.
|
||||
"""
|
||||
return list(self._members.values())
|
||||
|
||||
@property
|
||||
def last_message(self) -> Optional[Message]:
|
||||
"""Fetches the last message from this channel in cache.
|
||||
|
||||
The message might not be valid or point to an existing message.
|
||||
|
||||
.. admonition:: Reliable Fetching
|
||||
:class: helpful
|
||||
|
||||
For a slightly more reliable method of fetching the
|
||||
last message, consider using either :meth:`history`
|
||||
or :meth:`fetch_message` with the :attr:`last_message_id`
|
||||
attribute.
|
||||
|
||||
Returns
|
||||
---------
|
||||
Optional[:class:`Message`]
|
||||
The last message in this channel or ``None`` if not found.
|
||||
"""
|
||||
return self._state._get_message(self.last_message_id) if self.last_message_id else None
|
||||
|
||||
@property
|
||||
def category(self) -> Optional[CategoryChannel]:
|
||||
"""The category channel the parent channel belongs to, if applicable.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ClientException
|
||||
The parent channel was not cached and returned ``None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`CategoryChannel`]
|
||||
The parent channel's category.
|
||||
"""
|
||||
|
||||
parent = self.parent
|
||||
if parent is None:
|
||||
raise ClientException("Parent channel not found")
|
||||
return parent.category
|
||||
|
||||
@property
|
||||
def category_id(self) -> Optional[int]:
|
||||
"""The category channel ID the parent channel belongs to, if applicable.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ClientException
|
||||
The parent channel was not cached and returned ``None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`int`]
|
||||
The parent channel's category ID.
|
||||
"""
|
||||
|
||||
parent = self.parent
|
||||
if parent is None:
|
||||
raise ClientException("Parent channel not found")
|
||||
return parent.category_id
|
||||
|
||||
def is_private(self) -> bool:
|
||||
""":class:`bool`: Whether the thread is a private thread.
|
||||
|
||||
A private thread is only viewable by those that have been explicitly
|
||||
invited or have :attr:`~.Permissions.manage_threads`.
|
||||
"""
|
||||
return self._type is ChannelType.private_thread
|
||||
|
||||
def is_news(self) -> bool:
|
||||
""":class:`bool`: Whether the thread is a news thread.
|
||||
|
||||
A news thread is a thread that has a parent that is a news channel,
|
||||
i.e. :meth:`.TextChannel.is_news` is ``True``.
|
||||
"""
|
||||
return self._type is ChannelType.news_thread
|
||||
|
||||
def is_nsfw(self) -> bool:
|
||||
""":class:`bool`: Whether the thread is NSFW or not.
|
||||
|
||||
An NSFW thread is a thread that has a parent that is an NSFW channel,
|
||||
i.e. :meth:`.TextChannel.is_nsfw` is ``True``.
|
||||
"""
|
||||
parent = self.parent
|
||||
return parent is not None and parent.is_nsfw()
|
||||
|
||||
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
|
||||
"""Handles permission resolution for the :class:`~discord.Member`
|
||||
or :class:`~discord.Role`.
|
||||
|
||||
Since threads do not have their own permissions, they inherit them
|
||||
from the parent channel. This is a convenience method for
|
||||
calling :meth:`~discord.TextChannel.permissions_for` on the
|
||||
parent channel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Union[:class:`~discord.Member`, :class:`~discord.Role`]
|
||||
The object to resolve permissions for. This could be either
|
||||
a member or a role. If it's a role then member overwrites
|
||||
are not computed.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ClientException
|
||||
The parent channel was not cached and returned ``None``
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`~discord.Permissions`
|
||||
The resolved permissions for the member or role.
|
||||
"""
|
||||
|
||||
parent = self.parent
|
||||
if parent is None:
|
||||
raise ClientException("Parent channel not found")
|
||||
return parent.permissions_for(obj)
|
||||
|
||||
async def delete_messages(self, messages: Iterable[Snowflake]) -> None:
|
||||
"""|coro|
|
||||
|
||||
Deletes a list of messages. This is similar to :meth:`Message.delete`
|
||||
except it bulk deletes multiple messages.
|
||||
|
||||
As a special case, if the number of messages is 0, then nothing
|
||||
is done. If the number of messages is 1 then single message
|
||||
delete is done. If it's more than two, then bulk delete is used.
|
||||
|
||||
You cannot bulk delete more than 100 messages or messages that
|
||||
are older than 14 days old.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||
use this.
|
||||
|
||||
Usable only by bot accounts.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
messages: Iterable[:class:`abc.Snowflake`]
|
||||
An iterable of messages denoting which ones to bulk delete.
|
||||
|
||||
Raises
|
||||
------
|
||||
ClientException
|
||||
The number of messages to delete was more than 100.
|
||||
Forbidden
|
||||
You do not have proper permissions to delete the messages or
|
||||
you're not using a bot account.
|
||||
NotFound
|
||||
If single delete, then the message was already deleted.
|
||||
HTTPException
|
||||
Deleting the messages failed.
|
||||
"""
|
||||
if not isinstance(messages, (list, tuple)):
|
||||
messages = list(messages)
|
||||
|
||||
if len(messages) == 0:
|
||||
return # do nothing
|
||||
|
||||
if len(messages) == 1:
|
||||
message_id = messages[0].id
|
||||
await self._state.http.delete_message(self.id, message_id)
|
||||
return
|
||||
|
||||
if len(messages) > 100:
|
||||
raise ClientException("Can only bulk delete messages up to 100 messages")
|
||||
|
||||
message_ids: SnowflakeList = [m.id for m in messages]
|
||||
await self._state.http.delete_messages(self.id, message_ids)
|
||||
|
||||
async def purge(
|
||||
self,
|
||||
*,
|
||||
limit: Optional[int] = 100,
|
||||
check: Callable[[Message], bool] = MISSING,
|
||||
before: Optional[SnowflakeTime] = None,
|
||||
after: Optional[SnowflakeTime] = None,
|
||||
around: Optional[SnowflakeTime] = None,
|
||||
oldest_first: Optional[bool] = False,
|
||||
bulk: bool = True,
|
||||
) -> List[Message]:
|
||||
"""|coro|
|
||||
|
||||
Purges a list of messages that meet the criteria given by the predicate
|
||||
``check``. If a ``check`` is not provided then all messages are deleted
|
||||
without discrimination.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_messages` permission to
|
||||
delete messages even if they are your own (unless you are a user
|
||||
account). The :attr:`~Permissions.read_message_history` permission is
|
||||
also needed to retrieve message history.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Deleting bot's messages ::
|
||||
|
||||
def is_me(m):
|
||||
return m.author == client.user
|
||||
|
||||
deleted = await thread.purge(limit=100, check=is_me)
|
||||
await thread.send(f'Deleted {len(deleted)} message(s)')
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
limit: Optional[:class:`int`]
|
||||
The number of messages to search through. This is not the number
|
||||
of messages that will be deleted, though it can be.
|
||||
check: Callable[[:class:`Message`], :class:`bool`]
|
||||
The function used to check if a message should be deleted.
|
||||
It must take a :class:`Message` as its sole parameter.
|
||||
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Same as ``before`` in :meth:`history`.
|
||||
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Same as ``after`` in :meth:`history`.
|
||||
around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
|
||||
Same as ``around`` in :meth:`history`.
|
||||
oldest_first: Optional[:class:`bool`]
|
||||
Same as ``oldest_first`` in :meth:`history`.
|
||||
bulk: :class:`bool`
|
||||
If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting
|
||||
a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will
|
||||
fall back to single delete if messages are older than two weeks.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have proper permissions to do the actions required.
|
||||
HTTPException
|
||||
Purging the messages failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`.Message`]
|
||||
The list of messages that were deleted.
|
||||
"""
|
||||
|
||||
if check is MISSING:
|
||||
check = lambda m: True
|
||||
|
||||
iterator = self.history(limit=limit, before=before, after=after, oldest_first=oldest_first, around=around)
|
||||
ret: List[Message] = []
|
||||
count = 0
|
||||
|
||||
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
|
||||
|
||||
async def _single_delete_strategy(messages: Iterable[Message]):
|
||||
for m in messages:
|
||||
await m.delete()
|
||||
|
||||
strategy = self.delete_messages if bulk else _single_delete_strategy
|
||||
|
||||
async for message in iterator:
|
||||
if count == 100:
|
||||
to_delete = ret[-100:]
|
||||
await strategy(to_delete)
|
||||
count = 0
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not check(message):
|
||||
continue
|
||||
|
||||
if message.id < minimum_time:
|
||||
# older than 14 days old
|
||||
if count == 1:
|
||||
await ret[-1].delete()
|
||||
elif count >= 2:
|
||||
to_delete = ret[-count:]
|
||||
await strategy(to_delete)
|
||||
|
||||
count = 0
|
||||
strategy = _single_delete_strategy
|
||||
|
||||
count += 1
|
||||
ret.append(message)
|
||||
|
||||
# SOme messages remaining to poll
|
||||
if count >= 2:
|
||||
# more than 2 messages -> bulk delete
|
||||
to_delete = ret[-count:]
|
||||
await strategy(to_delete)
|
||||
elif count == 1:
|
||||
# delete a single message
|
||||
await ret[-1].delete()
|
||||
|
||||
return ret
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
archived: bool = MISSING,
|
||||
locked: bool = MISSING,
|
||||
invitable: bool = MISSING,
|
||||
slowmode_delay: int = MISSING,
|
||||
auto_archive_duration: ThreadArchiveDuration = MISSING,
|
||||
) -> Thread:
|
||||
"""|coro|
|
||||
|
||||
Edits the thread.
|
||||
|
||||
Editing the thread requires :attr:`.Permissions.manage_threads`. The thread
|
||||
creator can also edit ``name``, ``archived`` or ``auto_archive_duration``.
|
||||
Note that if the thread is locked then only those with :attr:`.Permissions.manage_threads`
|
||||
can unarchive a thread.
|
||||
|
||||
The thread must be unarchived to be edited.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
The new name of the thread.
|
||||
archived: :class:`bool`
|
||||
Whether to archive the thread or not.
|
||||
locked: :class:`bool`
|
||||
Whether to lock the thread or not.
|
||||
invitable: :class:`bool`
|
||||
Whether non-moderators can add other non-moderators to this thread.
|
||||
Only available for private threads.
|
||||
auto_archive_duration: :class:`int`
|
||||
The new duration in minutes before a thread is automatically archived for inactivity.
|
||||
Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
|
||||
slowmode_delay: :class:`int`
|
||||
Specifies the slowmode rate limit for user in this thread, in seconds.
|
||||
A value of ``0`` disables slowmode. The maximum value possible is ``21600``.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to edit the thread.
|
||||
HTTPException
|
||||
Editing the thread failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Thread`
|
||||
The newly edited thread.
|
||||
"""
|
||||
payload = {}
|
||||
if name is not MISSING:
|
||||
payload["name"] = str(name)
|
||||
if archived is not MISSING:
|
||||
payload["archived"] = archived
|
||||
if auto_archive_duration is not MISSING:
|
||||
payload["auto_archive_duration"] = auto_archive_duration
|
||||
if locked is not MISSING:
|
||||
payload["locked"] = locked
|
||||
if invitable is not MISSING:
|
||||
payload["invitable"] = invitable
|
||||
if slowmode_delay is not MISSING:
|
||||
payload["rate_limit_per_user"] = slowmode_delay
|
||||
|
||||
data = await self._state.http.edit_channel(self.id, **payload)
|
||||
# The data payload will always be a Thread payload
|
||||
return Thread(data=data, state=self._state, guild=self.guild) # type: ignore
|
||||
|
||||
async def join(self):
|
||||
"""|coro|
|
||||
|
||||
Joins this thread.
|
||||
|
||||
You must have :attr:`~Permissions.send_messages_in_threads` to join a thread.
|
||||
If the thread is private, :attr:`~Permissions.manage_threads` is also needed.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to join the thread.
|
||||
HTTPException
|
||||
Joining the thread failed.
|
||||
"""
|
||||
await self._state.http.join_thread(self.id)
|
||||
|
||||
async def leave(self):
|
||||
"""|coro|
|
||||
|
||||
Leaves this thread.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Leaving the thread failed.
|
||||
"""
|
||||
await self._state.http.leave_thread(self.id)
|
||||
|
||||
async def add_user(self, user: Snowflake):
|
||||
"""|coro|
|
||||
|
||||
Adds a user to this thread.
|
||||
|
||||
You must have :attr:`~Permissions.send_messages` and :attr:`~Permissions.use_threads`
|
||||
to add a user to a public thread. If the thread is private then :attr:`~Permissions.send_messages`
|
||||
and either :attr:`~Permissions.use_private_threads` or :attr:`~Permissions.manage_messages`
|
||||
is required to add a user to the thread.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user: :class:`abc.Snowflake`
|
||||
The user to add to the thread.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to add the user to the thread.
|
||||
HTTPException
|
||||
Adding the user to the thread failed.
|
||||
"""
|
||||
await self._state.http.add_user_to_thread(self.id, user.id)
|
||||
|
||||
async def remove_user(self, user: Snowflake):
|
||||
"""|coro|
|
||||
|
||||
Removes a user from this thread.
|
||||
|
||||
You must have :attr:`~Permissions.manage_threads` or be the creator of the thread to remove a user.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user: :class:`abc.Snowflake`
|
||||
The user to add to the thread.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to remove the user from the thread.
|
||||
HTTPException
|
||||
Removing the user from the thread failed.
|
||||
"""
|
||||
await self._state.http.remove_user_from_thread(self.id, user.id)
|
||||
|
||||
async def fetch_members(self) -> List[ThreadMember]:
|
||||
"""|coro|
|
||||
|
||||
Retrieves all :class:`ThreadMember` that are in this thread.
|
||||
|
||||
This requires :attr:`Intents.members` to get information about members
|
||||
other than yourself.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Retrieving the members failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`ThreadMember`]
|
||||
All thread members in the thread.
|
||||
"""
|
||||
|
||||
members = await self._state.http.get_thread_members(self.id)
|
||||
return [ThreadMember(parent=self, data=data) for data in members]
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes this thread.
|
||||
|
||||
You must have :attr:`~Permissions.manage_threads` to delete threads.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to delete this thread.
|
||||
HTTPException
|
||||
Deleting the thread failed.
|
||||
"""
|
||||
await self._state.http.delete_channel(self.id)
|
||||
|
||||
def get_partial_message(self, message_id: int, /) -> PartialMessage:
|
||||
"""Creates a :class:`PartialMessage` from the message ID.
|
||||
|
||||
This is useful if you want to work with a message and only have its ID without
|
||||
doing an unnecessary API call.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
------------
|
||||
message_id: :class:`int`
|
||||
The message ID to create a partial message for.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`PartialMessage`
|
||||
The partial message.
|
||||
"""
|
||||
|
||||
from .message import PartialMessage
|
||||
|
||||
return PartialMessage(channel=self, id=message_id)
|
||||
|
||||
def _add_member(self, member: ThreadMember) -> None:
|
||||
self._members[member.id] = member
|
||||
|
||||
def _pop_member(self, member_id: int) -> Optional[ThreadMember]:
|
||||
return self._members.pop(member_id, None)
|
||||
|
||||
|
||||
class ThreadMember(Hashable):
|
||||
"""Represents a Discord thread member.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two thread members are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two thread members are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the thread member's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the thread member's ID.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the thread member's name.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
The thread member's ID.
|
||||
thread_id: :class:`int`
|
||||
The thread's ID.
|
||||
joined_at: :class:`datetime.datetime`
|
||||
The time the member joined the thread in UTC.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"id",
|
||||
"thread_id",
|
||||
"joined_at",
|
||||
"flags",
|
||||
"_state",
|
||||
"parent",
|
||||
)
|
||||
|
||||
def __init__(self, parent: Thread, data: ThreadMemberPayload):
|
||||
self.parent = parent
|
||||
self._state = parent._state
|
||||
self._from_data(data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ThreadMember id={self.id} thread_id={self.thread_id} joined_at={self.joined_at!r}>"
|
||||
|
||||
def _from_data(self, data: ThreadMemberPayload):
|
||||
try:
|
||||
self.id = int(data["user_id"])
|
||||
except KeyError:
|
||||
assert self._state.self_id is not None
|
||||
self.id = self._state.self_id
|
||||
|
||||
try:
|
||||
self.thread_id = int(data["id"])
|
||||
except KeyError:
|
||||
self.thread_id = self.parent.id
|
||||
|
||||
self.joined_at = parse_time(data["join_timestamp"])
|
||||
self.flags = data["flags"]
|
||||
|
||||
@property
|
||||
def thread(self) -> Thread:
|
||||
""":class:`Thread`: The thread this member belongs to."""
|
||||
return self.parent
|
||||
|
||||
async def fetch_member(self) -> Member:
|
||||
"""|coro|
|
||||
|
||||
Retrieves a :class:`Member` from the ThreadMember object.
|
||||
|
||||
.. note::
|
||||
|
||||
This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_member` instead.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have access to the guild.
|
||||
HTTPException
|
||||
Fetching the member failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Member`
|
||||
The member.
|
||||
"""
|
||||
|
||||
return await self.thread.guild.fetch_member(self.id)
|
||||
|
||||
def get_member(self) -> Optional[Member]:
|
||||
"""
|
||||
Get the :class:`Member` from cache for the ThreadMember object.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Member`]
|
||||
The member or ``None`` if not found.
|
||||
"""
|
||||
|
||||
return self.thread.guild.get_member(self.id)
|
||||
114
discord/types/activity.py
Normal file
114
discord/types/activity.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
from .user import PartialUser
|
||||
from .snowflake import Snowflake
|
||||
|
||||
|
||||
StatusType = Literal["idle", "dnd", "online", "offline"]
|
||||
|
||||
|
||||
class PartialPresenceUpdate(TypedDict):
|
||||
user: PartialUser
|
||||
guild_id: Snowflake
|
||||
status: StatusType
|
||||
activities: List[Activity]
|
||||
client_status: ClientStatus
|
||||
|
||||
|
||||
class ClientStatus(TypedDict, total=False):
|
||||
desktop: str
|
||||
mobile: str
|
||||
web: str
|
||||
|
||||
|
||||
class ActivityTimestamps(TypedDict, total=False):
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
class ActivityParty(TypedDict, total=False):
|
||||
id: str
|
||||
size: List[int]
|
||||
|
||||
|
||||
class ActivityAssets(TypedDict, total=False):
|
||||
large_image: str
|
||||
large_text: str
|
||||
small_image: str
|
||||
small_text: str
|
||||
|
||||
|
||||
class ActivitySecrets(TypedDict, total=False):
|
||||
join: str
|
||||
spectate: str
|
||||
match: str
|
||||
|
||||
|
||||
class _ActivityEmojiOptional(TypedDict, total=False):
|
||||
id: Snowflake
|
||||
animated: bool
|
||||
|
||||
|
||||
class ActivityEmoji(_ActivityEmojiOptional):
|
||||
name: str
|
||||
|
||||
|
||||
class ActivityButton(TypedDict):
|
||||
label: str
|
||||
url: str
|
||||
|
||||
|
||||
class _SendableActivityOptional(TypedDict, total=False):
|
||||
url: Optional[str]
|
||||
|
||||
|
||||
ActivityType = Literal[0, 1, 2, 4, 5]
|
||||
|
||||
|
||||
class SendableActivity(_SendableActivityOptional):
|
||||
name: str
|
||||
type: ActivityType
|
||||
|
||||
|
||||
class _BaseActivity(SendableActivity):
|
||||
created_at: int
|
||||
|
||||
|
||||
class Activity(_BaseActivity, total=False):
|
||||
state: Optional[str]
|
||||
details: Optional[str]
|
||||
timestamps: ActivityTimestamps
|
||||
assets: ActivityAssets
|
||||
party: ActivityParty
|
||||
application_id: Snowflake
|
||||
flags: int
|
||||
emoji: Optional[ActivityEmoji]
|
||||
secrets: ActivitySecrets
|
||||
session_id: Optional[str]
|
||||
instance: bool
|
||||
buttons: List[ActivityButton]
|
||||
72
discord/types/appinfo.py
Normal file
72
discord/types/appinfo.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict, List, Optional
|
||||
|
||||
from .user import User
|
||||
from .team import Team
|
||||
from .snowflake import Snowflake
|
||||
|
||||
|
||||
class BaseAppInfo(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
verify_key: str
|
||||
icon: Optional[str]
|
||||
summary: str
|
||||
description: str
|
||||
|
||||
|
||||
class _AppInfoOptional(TypedDict, total=False):
|
||||
team: Team
|
||||
guild_id: Snowflake
|
||||
primary_sku_id: Snowflake
|
||||
slug: str
|
||||
terms_of_service_url: str
|
||||
privacy_policy_url: str
|
||||
hook: bool
|
||||
max_participants: int
|
||||
|
||||
|
||||
class AppInfo(BaseAppInfo, _AppInfoOptional):
|
||||
rpc_origins: List[str]
|
||||
owner: User
|
||||
bot_public: bool
|
||||
bot_require_code_grant: bool
|
||||
|
||||
|
||||
class _PartialAppInfoOptional(TypedDict, total=False):
|
||||
rpc_origins: List[str]
|
||||
cover_image: str
|
||||
hook: bool
|
||||
terms_of_service_url: str
|
||||
privacy_policy_url: str
|
||||
max_participants: int
|
||||
flags: int
|
||||
|
||||
|
||||
class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo):
|
||||
pass
|
||||
266
discord/types/audit_log.py
Normal file
266
discord/types/audit_log.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
from .webhook import Webhook
|
||||
from .guild import MFALevel, VerificationLevel, ExplicitContentFilterLevel, DefaultMessageNotificationLevel
|
||||
from .integration import IntegrationExpireBehavior, PartialIntegration
|
||||
from .user import User
|
||||
from .snowflake import Snowflake
|
||||
from .role import Role
|
||||
from .channel import ChannelType, VideoQualityMode, PermissionOverwrite
|
||||
from .threads import Thread
|
||||
|
||||
AuditLogEvent = Literal[
|
||||
1,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
40,
|
||||
41,
|
||||
42,
|
||||
50,
|
||||
51,
|
||||
52,
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
75,
|
||||
80,
|
||||
81,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
85,
|
||||
90,
|
||||
91,
|
||||
92,
|
||||
110,
|
||||
111,
|
||||
112,
|
||||
]
|
||||
|
||||
|
||||
class _AuditLogChange_Str(TypedDict):
|
||||
key: Literal[
|
||||
"name",
|
||||
"description",
|
||||
"preferred_locale",
|
||||
"vanity_url_code",
|
||||
"topic",
|
||||
"code",
|
||||
"allow",
|
||||
"deny",
|
||||
"permissions",
|
||||
"tags",
|
||||
]
|
||||
new_value: str
|
||||
old_value: str
|
||||
|
||||
|
||||
class _AuditLogChange_AssetHash(TypedDict):
|
||||
key: Literal["icon_hash", "splash_hash", "discovery_splash_hash", "banner_hash", "avatar_hash", "asset"]
|
||||
new_value: str
|
||||
old_value: str
|
||||
|
||||
|
||||
class _AuditLogChange_Snowflake(TypedDict):
|
||||
key: Literal[
|
||||
"id",
|
||||
"owner_id",
|
||||
"afk_channel_id",
|
||||
"rules_channel_id",
|
||||
"public_updates_channel_id",
|
||||
"widget_channel_id",
|
||||
"system_channel_id",
|
||||
"application_id",
|
||||
"channel_id",
|
||||
"inviter_id",
|
||||
"guild_id",
|
||||
]
|
||||
new_value: Snowflake
|
||||
old_value: Snowflake
|
||||
|
||||
|
||||
class _AuditLogChange_Bool(TypedDict):
|
||||
key: Literal[
|
||||
"widget_enabled",
|
||||
"nsfw",
|
||||
"hoist",
|
||||
"mentionable",
|
||||
"temporary",
|
||||
"deaf",
|
||||
"mute",
|
||||
"nick",
|
||||
"enabled_emoticons",
|
||||
"region",
|
||||
"rtc_region",
|
||||
"available",
|
||||
"archived",
|
||||
"locked",
|
||||
]
|
||||
new_value: bool
|
||||
old_value: bool
|
||||
|
||||
|
||||
class _AuditLogChange_Int(TypedDict):
|
||||
key: Literal[
|
||||
"afk_timeout",
|
||||
"prune_delete_days",
|
||||
"position",
|
||||
"bitrate",
|
||||
"rate_limit_per_user",
|
||||
"color",
|
||||
"max_uses",
|
||||
"max_age",
|
||||
"user_limit",
|
||||
"auto_archive_duration",
|
||||
"default_auto_archive_duration",
|
||||
]
|
||||
new_value: int
|
||||
old_value: int
|
||||
|
||||
|
||||
class _AuditLogChange_ListRole(TypedDict):
|
||||
key: Literal["$add", "$remove"]
|
||||
new_value: List[Role]
|
||||
old_value: List[Role]
|
||||
|
||||
|
||||
class _AuditLogChange_MFALevel(TypedDict):
|
||||
key: Literal["mfa_level"]
|
||||
new_value: MFALevel
|
||||
old_value: MFALevel
|
||||
|
||||
|
||||
class _AuditLogChange_VerificationLevel(TypedDict):
|
||||
key: Literal["verification_level"]
|
||||
new_value: VerificationLevel
|
||||
old_value: VerificationLevel
|
||||
|
||||
|
||||
class _AuditLogChange_ExplicitContentFilter(TypedDict):
|
||||
key: Literal["explicit_content_filter"]
|
||||
new_value: ExplicitContentFilterLevel
|
||||
old_value: ExplicitContentFilterLevel
|
||||
|
||||
|
||||
class _AuditLogChange_DefaultMessageNotificationLevel(TypedDict):
|
||||
key: Literal["default_message_notifications"]
|
||||
new_value: DefaultMessageNotificationLevel
|
||||
old_value: DefaultMessageNotificationLevel
|
||||
|
||||
|
||||
class _AuditLogChange_ChannelType(TypedDict):
|
||||
key: Literal["type"]
|
||||
new_value: ChannelType
|
||||
old_value: ChannelType
|
||||
|
||||
|
||||
class _AuditLogChange_IntegrationExpireBehaviour(TypedDict):
|
||||
key: Literal["expire_behavior"]
|
||||
new_value: IntegrationExpireBehavior
|
||||
old_value: IntegrationExpireBehavior
|
||||
|
||||
|
||||
class _AuditLogChange_VideoQualityMode(TypedDict):
|
||||
key: Literal["video_quality_mode"]
|
||||
new_value: VideoQualityMode
|
||||
old_value: VideoQualityMode
|
||||
|
||||
|
||||
class _AuditLogChange_Overwrites(TypedDict):
|
||||
key: Literal["permission_overwrites"]
|
||||
new_value: List[PermissionOverwrite]
|
||||
old_value: List[PermissionOverwrite]
|
||||
|
||||
|
||||
AuditLogChange = Union[
|
||||
_AuditLogChange_Str,
|
||||
_AuditLogChange_AssetHash,
|
||||
_AuditLogChange_Snowflake,
|
||||
_AuditLogChange_Int,
|
||||
_AuditLogChange_Bool,
|
||||
_AuditLogChange_ListRole,
|
||||
_AuditLogChange_MFALevel,
|
||||
_AuditLogChange_VerificationLevel,
|
||||
_AuditLogChange_ExplicitContentFilter,
|
||||
_AuditLogChange_DefaultMessageNotificationLevel,
|
||||
_AuditLogChange_ChannelType,
|
||||
_AuditLogChange_IntegrationExpireBehaviour,
|
||||
_AuditLogChange_VideoQualityMode,
|
||||
_AuditLogChange_Overwrites,
|
||||
]
|
||||
|
||||
|
||||
class AuditEntryInfo(TypedDict):
|
||||
delete_member_days: str
|
||||
members_removed: str
|
||||
channel_id: Snowflake
|
||||
message_id: Snowflake
|
||||
count: str
|
||||
id: Snowflake
|
||||
type: Literal["0", "1"]
|
||||
role_name: str
|
||||
|
||||
|
||||
class _AuditLogEntryOptional(TypedDict, total=False):
|
||||
changes: List[AuditLogChange]
|
||||
options: AuditEntryInfo
|
||||
reason: str
|
||||
|
||||
|
||||
class AuditLogEntry(_AuditLogEntryOptional):
|
||||
target_id: Optional[str]
|
||||
user_id: Optional[Snowflake]
|
||||
id: Snowflake
|
||||
action_type: AuditLogEvent
|
||||
|
||||
|
||||
class AuditLog(TypedDict):
|
||||
webhooks: List[Webhook]
|
||||
users: List[User]
|
||||
audit_log_entries: List[AuditLogEntry]
|
||||
integrations: List[PartialIntegration]
|
||||
threads: List[Thread]
|
||||
@@ -22,58 +22,31 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
from .user import PartialUser
|
||||
from .snowflake import Snowflake
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
from .threads import ThreadMetadata, ThreadMember, ThreadArchiveDuration
|
||||
|
||||
|
||||
OverwriteType = Literal[0, 1]
|
||||
|
||||
|
||||
class PermissionOverwrite(TypedDict):
|
||||
id: Snowflake
|
||||
type: Literal[0, 1]
|
||||
type: OverwriteType
|
||||
allow: str
|
||||
deny: str
|
||||
|
||||
|
||||
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
|
||||
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13]
|
||||
|
||||
|
||||
class PartialChannel(TypedDict):
|
||||
id: str
|
||||
type: ChannelType
|
||||
class _BaseChannel(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
|
||||
|
||||
class _TextChannelOptional(PartialChannel, total=False):
|
||||
topic: str
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: int
|
||||
rate_limit_per_user: int
|
||||
|
||||
|
||||
class _VoiceChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
|
||||
|
||||
class _CategoryChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StoreChannelOptional(PartialChannel, total=False):
|
||||
...
|
||||
|
||||
|
||||
class _StageChannelOptional(PartialChannel, total=False):
|
||||
rtc_region: Optional[str]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
topic: str
|
||||
|
||||
|
||||
class GuildChannel(
|
||||
_TextChannelOptional, _VoiceChannelOptional, _CategoryChannelOptional, _StoreChannelOptional, _StageChannelOptional
|
||||
):
|
||||
class _BaseGuildChannel(_BaseChannel):
|
||||
guild_id: Snowflake
|
||||
position: int
|
||||
permission_overwrites: List[PermissionOverwrite]
|
||||
@@ -81,11 +54,104 @@ class GuildChannel(
|
||||
parent_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class DMChannel(PartialChannel):
|
||||
class PartialChannel(_BaseChannel):
|
||||
type: ChannelType
|
||||
|
||||
|
||||
class _TextChannelOptional(TypedDict, total=False):
|
||||
topic: str
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: str
|
||||
rate_limit_per_user: int
|
||||
default_auto_archive_duration: ThreadArchiveDuration
|
||||
|
||||
|
||||
class TextChannel(_BaseGuildChannel, _TextChannelOptional):
|
||||
type: Literal[0]
|
||||
|
||||
|
||||
class NewsChannel(_BaseGuildChannel, _TextChannelOptional):
|
||||
type: Literal[5]
|
||||
|
||||
|
||||
VideoQualityMode = Literal[1, 2]
|
||||
|
||||
|
||||
class _VoiceChannelOptional(TypedDict, total=False):
|
||||
rtc_region: Optional[str]
|
||||
video_quality_mode: VideoQualityMode
|
||||
|
||||
|
||||
class VoiceChannel(_BaseGuildChannel, _VoiceChannelOptional):
|
||||
type: Literal[2]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
|
||||
|
||||
class CategoryChannel(_BaseGuildChannel):
|
||||
type: Literal[4]
|
||||
|
||||
|
||||
class StoreChannel(_BaseGuildChannel):
|
||||
type: Literal[6]
|
||||
|
||||
|
||||
class _StageChannelOptional(TypedDict, total=False):
|
||||
rtc_region: Optional[str]
|
||||
topic: str
|
||||
|
||||
|
||||
class StageChannel(_BaseGuildChannel, _StageChannelOptional):
|
||||
type: Literal[13]
|
||||
bitrate: int
|
||||
user_limit: int
|
||||
|
||||
|
||||
class _ThreadChannelOptional(TypedDict, total=False):
|
||||
member: ThreadMember
|
||||
owner_id: Snowflake
|
||||
rate_limit_per_user: int
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: str
|
||||
|
||||
|
||||
class ThreadChannel(_BaseChannel, _ThreadChannelOptional):
|
||||
type: Literal[10, 11, 12]
|
||||
guild_id: Snowflake
|
||||
parent_id: Snowflake
|
||||
owner_id: Snowflake
|
||||
nsfw: bool
|
||||
last_message_id: Optional[Snowflake]
|
||||
rate_limit_per_user: int
|
||||
message_count: int
|
||||
member_count: int
|
||||
thread_metadata: ThreadMetadata
|
||||
|
||||
|
||||
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StoreChannel, StageChannel, ThreadChannel]
|
||||
|
||||
|
||||
class DMChannel(_BaseChannel):
|
||||
type: Literal[1]
|
||||
last_message_id: Optional[Snowflake]
|
||||
recipients: List[PartialUser]
|
||||
|
||||
|
||||
class GroupDMChannel(DMChannel):
|
||||
class GroupDMChannel(_BaseChannel):
|
||||
type: Literal[3]
|
||||
icon: Optional[str]
|
||||
owner_id: Snowflake
|
||||
|
||||
|
||||
Channel = Union[GuildChannel, DMChannel, GroupDMChannel]
|
||||
|
||||
PrivacyLevel = Literal[1, 2]
|
||||
|
||||
|
||||
class StageInstance(TypedDict):
|
||||
id: Snowflake
|
||||
guild_id: Snowflake
|
||||
channel_id: Snowflake
|
||||
topic: str
|
||||
privacy_level: PrivacyLevel
|
||||
discoverable_disabled: bool
|
||||
|
||||
76
discord/types/components.py
Normal file
76
discord/types/components.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, TypedDict, Union
|
||||
from .emoji import PartialEmoji
|
||||
|
||||
ComponentType = Literal[1, 2, 3]
|
||||
ButtonStyle = Literal[1, 2, 3, 4, 5]
|
||||
|
||||
|
||||
class ActionRow(TypedDict):
|
||||
type: Literal[1]
|
||||
components: List[Component]
|
||||
|
||||
|
||||
class _ButtonComponentOptional(TypedDict, total=False):
|
||||
custom_id: str
|
||||
url: str
|
||||
disabled: bool
|
||||
emoji: PartialEmoji
|
||||
label: str
|
||||
|
||||
|
||||
class ButtonComponent(_ButtonComponentOptional):
|
||||
type: Literal[2]
|
||||
style: ButtonStyle
|
||||
|
||||
|
||||
class _SelectMenuOptional(TypedDict, total=False):
|
||||
placeholder: str
|
||||
min_values: int
|
||||
max_values: int
|
||||
disabled: bool
|
||||
|
||||
|
||||
class _SelectOptionsOptional(TypedDict, total=False):
|
||||
description: str
|
||||
emoji: PartialEmoji
|
||||
|
||||
|
||||
class SelectOption(_SelectOptionsOptional):
|
||||
label: str
|
||||
value: str
|
||||
default: bool
|
||||
|
||||
|
||||
class SelectMenu(_SelectMenuOptional):
|
||||
type: Literal[3]
|
||||
custom_id: str
|
||||
options: List[SelectOption]
|
||||
|
||||
|
||||
Component = Union[ActionRow, ButtonComponent, SelectMenu]
|
||||
@@ -24,49 +24,60 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from typing import List, Literal, TypedDict
|
||||
|
||||
|
||||
class _EmbedFooterOptional(TypedDict, total=False):
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
|
||||
class EmbedFooter(_EmbedFooterOptional):
|
||||
text: str
|
||||
|
||||
|
||||
class _EmbedFieldOptional(TypedDict, total=False):
|
||||
inline: bool
|
||||
|
||||
|
||||
class EmbedField(_EmbedFieldOptional):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
class EmbedThumbnail(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
|
||||
class EmbedVideo(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
|
||||
class EmbedImage(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
|
||||
class EmbedProvider(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
|
||||
class EmbedAuthor(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
|
||||
|
||||
EmbedType = Literal["rich", "image", "video", "gifv", "article", "link"]
|
||||
|
||||
|
||||
class Embed(TypedDict, total=False):
|
||||
title: str
|
||||
|
||||
46
discord/types/emoji.py
Normal file
46
discord/types/emoji.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import Optional, TypedDict
|
||||
from .snowflake import Snowflake, SnowflakeList
|
||||
from .user import User
|
||||
|
||||
|
||||
class PartialEmoji(TypedDict):
|
||||
id: Optional[Snowflake]
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class Emoji(PartialEmoji, total=False):
|
||||
roles: SnowflakeList
|
||||
user: User
|
||||
require_colons: bool
|
||||
managed: bool
|
||||
animated: bool
|
||||
available: bool
|
||||
|
||||
|
||||
class EditEmoji(TypedDict):
|
||||
name: str
|
||||
roles: Optional[SnowflakeList]
|
||||
41
discord/types/gateway.py
Normal file
41
discord/types/gateway.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class SessionStartLimit(TypedDict):
|
||||
total: int
|
||||
remaining: int
|
||||
reset_after: int
|
||||
max_concurrency: int
|
||||
|
||||
|
||||
class Gateway(TypedDict):
|
||||
url: str
|
||||
|
||||
|
||||
class GatewayBot(Gateway):
|
||||
shards: int
|
||||
session_start_limit: SessionStartLimit
|
||||
168
discord/types/guild.py
Normal file
168
discord/types/guild.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
from .snowflake import Snowflake
|
||||
from .channel import GuildChannel
|
||||
from .voice import GuildVoiceState
|
||||
from .welcome_screen import WelcomeScreen
|
||||
from .activity import PartialPresenceUpdate
|
||||
from .role import Role
|
||||
from .member import Member
|
||||
from .emoji import Emoji
|
||||
from .user import User
|
||||
from .threads import Thread
|
||||
|
||||
|
||||
class Ban(TypedDict):
|
||||
reason: Optional[str]
|
||||
user: User
|
||||
|
||||
|
||||
class _UnavailableGuildOptional(TypedDict, total=False):
|
||||
unavailable: bool
|
||||
|
||||
|
||||
class UnavailableGuild(_UnavailableGuildOptional):
|
||||
id: Snowflake
|
||||
|
||||
|
||||
class _GuildOptional(TypedDict, total=False):
|
||||
icon_hash: Optional[str]
|
||||
owner: bool
|
||||
permissions: str
|
||||
widget_enabled: bool
|
||||
widget_channel_id: Optional[Snowflake]
|
||||
joined_at: Optional[str]
|
||||
large: bool
|
||||
member_count: int
|
||||
voice_states: List[GuildVoiceState]
|
||||
members: List[Member]
|
||||
channels: List[GuildChannel]
|
||||
presences: List[PartialPresenceUpdate]
|
||||
threads: List[Thread]
|
||||
max_presences: Optional[int]
|
||||
max_members: int
|
||||
premium_subscription_count: int
|
||||
max_video_channel_users: int
|
||||
|
||||
|
||||
DefaultMessageNotificationLevel = Literal[0, 1]
|
||||
ExplicitContentFilterLevel = Literal[0, 1, 2]
|
||||
MFALevel = Literal[0, 1]
|
||||
VerificationLevel = Literal[0, 1, 2, 3, 4]
|
||||
NSFWLevel = Literal[0, 1, 2, 3]
|
||||
PremiumTier = Literal[0, 1, 2, 3]
|
||||
GuildFeature = Literal[
|
||||
"ANIMATED_ICON",
|
||||
"BANNER",
|
||||
"COMMERCE",
|
||||
"COMMUNITY",
|
||||
"DISCOVERABLE",
|
||||
"FEATURABLE",
|
||||
"INVITE_SPLASH",
|
||||
"MEMBER_VERIFICATION_GATE_ENABLED",
|
||||
"MONETIZATION_ENABLED",
|
||||
"MORE_EMOJI",
|
||||
"MORE_STICKERS",
|
||||
"NEWS",
|
||||
"PARTNERED",
|
||||
"PREVIEW_ENABLED",
|
||||
"PRIVATE_THREADS",
|
||||
"SEVEN_DAY_THREAD_ARCHIVE",
|
||||
"THREE_DAY_THREAD_ARCHIVE",
|
||||
"TICKETED_EVENTS_ENABLED",
|
||||
"VANITY_URL",
|
||||
"VERIFIED",
|
||||
"VIP_REGIONS",
|
||||
"WELCOME_SCREEN_ENABLED",
|
||||
]
|
||||
|
||||
|
||||
class _BaseGuildPreview(UnavailableGuild):
|
||||
name: str
|
||||
icon: Optional[str]
|
||||
splash: Optional[str]
|
||||
discovery_splash: Optional[str]
|
||||
emojis: List[Emoji]
|
||||
features: List[GuildFeature]
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class _GuildPreviewUnique(TypedDict):
|
||||
approximate_member_count: int
|
||||
approximate_presence_count: int
|
||||
|
||||
|
||||
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique):
|
||||
...
|
||||
|
||||
|
||||
class Guild(_BaseGuildPreview, _GuildOptional):
|
||||
owner_id: Snowflake
|
||||
region: str
|
||||
afk_channel_id: Optional[Snowflake]
|
||||
afk_timeout: int
|
||||
verification_level: VerificationLevel
|
||||
default_message_notifications: DefaultMessageNotificationLevel
|
||||
explicit_content_filter: ExplicitContentFilterLevel
|
||||
roles: List[Role]
|
||||
mfa_level: MFALevel
|
||||
nsfw_level: NSFWLevel
|
||||
application_id: Optional[Snowflake]
|
||||
system_channel_id: Optional[Snowflake]
|
||||
system_channel_flags: int
|
||||
rules_channel_id: Optional[Snowflake]
|
||||
vanity_url_code: Optional[str]
|
||||
banner: Optional[str]
|
||||
premium_tier: PremiumTier
|
||||
preferred_locale: str
|
||||
public_updates_channel_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class InviteGuild(Guild, total=False):
|
||||
welcome_screen: WelcomeScreen
|
||||
|
||||
|
||||
class GuildWithCounts(Guild, _GuildPreviewUnique):
|
||||
...
|
||||
|
||||
|
||||
class GuildPrune(TypedDict):
|
||||
pruned: Optional[int]
|
||||
|
||||
|
||||
class ChannelPositionUpdate(TypedDict):
|
||||
id: Snowflake
|
||||
position: Optional[int]
|
||||
lock_permissions: Optional[bool]
|
||||
parent_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class _RolePositionRequired(TypedDict):
|
||||
id: Snowflake
|
||||
|
||||
|
||||
class RolePositionUpdate(_RolePositionRequired, total=False):
|
||||
position: Optional[Snowflake]
|
||||
82
discord/types/integration.py
Normal file
82
discord/types/integration.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional, TypedDict, Union
|
||||
from .snowflake import Snowflake
|
||||
from .user import User
|
||||
|
||||
|
||||
class _IntegrationApplicationOptional(TypedDict, total=False):
|
||||
bot: User
|
||||
|
||||
|
||||
class IntegrationApplication(_IntegrationApplicationOptional):
|
||||
id: Snowflake
|
||||
name: str
|
||||
icon: Optional[str]
|
||||
description: str
|
||||
summary: str
|
||||
|
||||
|
||||
class IntegrationAccount(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
IntegrationExpireBehavior = Literal[0, 1]
|
||||
|
||||
|
||||
class PartialIntegration(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
type: IntegrationType
|
||||
account: IntegrationAccount
|
||||
|
||||
|
||||
IntegrationType = Literal["twitch", "youtube", "discord"]
|
||||
|
||||
|
||||
class BaseIntegration(PartialIntegration):
|
||||
enabled: bool
|
||||
syncing: bool
|
||||
synced_at: str
|
||||
user: User
|
||||
expire_behavior: IntegrationExpireBehavior
|
||||
expire_grace_period: int
|
||||
|
||||
|
||||
class StreamIntegration(BaseIntegration):
|
||||
role_id: Optional[Snowflake]
|
||||
enable_emoticons: bool
|
||||
subscriber_count: int
|
||||
revoked: bool
|
||||
|
||||
|
||||
class BotIntegration(BaseIntegration):
|
||||
application: IntegrationApplication
|
||||
|
||||
|
||||
Integration = Union[BaseIntegration, StreamIntegration, BotIntegration]
|
||||
234
discord/types/interactions.py
Normal file
234
discord/types/interactions.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, Dict, TypedDict, Union, List, Literal
|
||||
from .snowflake import Snowflake
|
||||
from .components import Component, ComponentType
|
||||
from .embed import Embed
|
||||
from .channel import ChannelType
|
||||
from .member import Member
|
||||
from .role import Role
|
||||
from .user import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message import AllowedMentions, Message
|
||||
|
||||
|
||||
ApplicationCommandType = Literal[1, 2, 3]
|
||||
|
||||
|
||||
class _ApplicationCommandOptional(TypedDict, total=False):
|
||||
options: List[ApplicationCommandOption]
|
||||
type: ApplicationCommandType
|
||||
|
||||
|
||||
class ApplicationCommand(_ApplicationCommandOptional):
|
||||
id: Snowflake
|
||||
application_id: Snowflake
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class _ApplicationCommandOptionOptional(TypedDict, total=False):
|
||||
choices: List[ApplicationCommandOptionChoice]
|
||||
options: List[ApplicationCommandOption]
|
||||
|
||||
|
||||
ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
|
||||
class ApplicationCommandOption(_ApplicationCommandOptionOptional):
|
||||
type: ApplicationCommandOptionType
|
||||
name: str
|
||||
description: str
|
||||
required: bool
|
||||
|
||||
|
||||
class ApplicationCommandOptionChoice(TypedDict):
|
||||
name: str
|
||||
value: Union[str, int]
|
||||
|
||||
|
||||
ApplicationCommandPermissionType = Literal[1, 2]
|
||||
|
||||
|
||||
class ApplicationCommandPermissions(TypedDict):
|
||||
id: Snowflake
|
||||
type: ApplicationCommandPermissionType
|
||||
permission: bool
|
||||
|
||||
|
||||
class BaseGuildApplicationCommandPermissions(TypedDict):
|
||||
permissions: List[ApplicationCommandPermissions]
|
||||
|
||||
|
||||
class PartialGuildApplicationCommandPermissions(BaseGuildApplicationCommandPermissions):
|
||||
id: Snowflake
|
||||
|
||||
|
||||
class GuildApplicationCommandPermissions(PartialGuildApplicationCommandPermissions):
|
||||
application_id: Snowflake
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
InteractionType = Literal[1, 2, 3]
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOption(TypedDict):
|
||||
name: str
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionSubcommand(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[1, 2]
|
||||
options: List[ApplicationCommandInteractionDataOption]
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionString(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[3]
|
||||
value: str
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionInteger(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[4]
|
||||
value: int
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionBoolean(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[5]
|
||||
value: bool
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionSnowflake(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[6, 7, 8, 9]
|
||||
value: Snowflake
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptionNumber(_ApplicationCommandInteractionDataOption):
|
||||
type: Literal[10]
|
||||
value: float
|
||||
|
||||
|
||||
ApplicationCommandInteractionDataOption = Union[
|
||||
_ApplicationCommandInteractionDataOptionString,
|
||||
_ApplicationCommandInteractionDataOptionInteger,
|
||||
_ApplicationCommandInteractionDataOptionSubcommand,
|
||||
_ApplicationCommandInteractionDataOptionBoolean,
|
||||
_ApplicationCommandInteractionDataOptionSnowflake,
|
||||
_ApplicationCommandInteractionDataOptionNumber,
|
||||
]
|
||||
|
||||
|
||||
class ApplicationCommandResolvedPartialChannel(TypedDict):
|
||||
id: Snowflake
|
||||
type: ChannelType
|
||||
permissions: str
|
||||
name: str
|
||||
|
||||
|
||||
class ApplicationCommandInteractionDataResolved(TypedDict, total=False):
|
||||
users: Dict[Snowflake, User]
|
||||
members: Dict[Snowflake, Member]
|
||||
roles: Dict[Snowflake, Role]
|
||||
channels: Dict[Snowflake, ApplicationCommandResolvedPartialChannel]
|
||||
|
||||
|
||||
class _ApplicationCommandInteractionDataOptional(TypedDict, total=False):
|
||||
options: List[ApplicationCommandInteractionDataOption]
|
||||
resolved: ApplicationCommandInteractionDataResolved
|
||||
target_id: Snowflake
|
||||
type: ApplicationCommandType
|
||||
|
||||
|
||||
class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOptional):
|
||||
id: Snowflake
|
||||
name: str
|
||||
|
||||
|
||||
class _ComponentInteractionDataOptional(TypedDict, total=False):
|
||||
values: List[str]
|
||||
|
||||
|
||||
class ComponentInteractionData(_ComponentInteractionDataOptional):
|
||||
custom_id: str
|
||||
component_type: ComponentType
|
||||
|
||||
|
||||
InteractionData = Union[ApplicationCommandInteractionData, ComponentInteractionData]
|
||||
|
||||
|
||||
class _InteractionOptional(TypedDict, total=False):
|
||||
data: InteractionData
|
||||
guild_id: Snowflake
|
||||
channel_id: Snowflake
|
||||
member: Member
|
||||
user: User
|
||||
message: Message
|
||||
|
||||
|
||||
class Interaction(_InteractionOptional):
|
||||
id: Snowflake
|
||||
application_id: Snowflake
|
||||
type: InteractionType
|
||||
token: str
|
||||
version: int
|
||||
|
||||
|
||||
class InteractionApplicationCommandCallbackData(TypedDict, total=False):
|
||||
tts: bool
|
||||
content: str
|
||||
embeds: List[Embed]
|
||||
allowed_mentions: AllowedMentions
|
||||
flags: int
|
||||
components: List[Component]
|
||||
|
||||
|
||||
InteractionResponseType = Literal[1, 4, 5, 6, 7]
|
||||
|
||||
|
||||
class _InteractionResponseOptional(TypedDict, total=False):
|
||||
data: InteractionApplicationCommandCallbackData
|
||||
|
||||
|
||||
class InteractionResponse(_InteractionResponseOptional):
|
||||
type: InteractionResponseType
|
||||
|
||||
|
||||
class MessageInteraction(TypedDict):
|
||||
id: Snowflake
|
||||
type: InteractionType
|
||||
name: str
|
||||
user: User
|
||||
|
||||
|
||||
class _EditApplicationCommandOptional(TypedDict, total=False):
|
||||
description: str
|
||||
options: Optional[List[ApplicationCommandOption]]
|
||||
type: ApplicationCommandType
|
||||
default_permission: bool
|
||||
|
||||
|
||||
class EditApplicationCommand(_EditApplicationCommandOptional):
|
||||
name: str
|
||||
99
discord/types/invite.py
Normal file
99
discord/types/invite.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional, TypedDict, Union
|
||||
|
||||
from .snowflake import Snowflake
|
||||
from .guild import InviteGuild, _GuildPreviewUnique
|
||||
from .channel import PartialChannel
|
||||
from .user import PartialUser
|
||||
from .appinfo import PartialAppInfo
|
||||
|
||||
InviteTargetType = Literal[1, 2]
|
||||
|
||||
|
||||
class _InviteOptional(TypedDict, total=False):
|
||||
guild: InviteGuild
|
||||
inviter: PartialUser
|
||||
target_user: PartialUser
|
||||
target_type: InviteTargetType
|
||||
target_application: PartialAppInfo
|
||||
|
||||
|
||||
class _InviteMetadata(TypedDict, total=False):
|
||||
uses: int
|
||||
max_uses: int
|
||||
max_age: int
|
||||
temporary: bool
|
||||
created_at: str
|
||||
expires_at: Optional[str]
|
||||
|
||||
|
||||
class VanityInvite(_InviteMetadata):
|
||||
code: Optional[str]
|
||||
|
||||
|
||||
class IncompleteInvite(_InviteMetadata):
|
||||
code: str
|
||||
channel: PartialChannel
|
||||
|
||||
|
||||
class Invite(IncompleteInvite, _InviteOptional):
|
||||
...
|
||||
|
||||
|
||||
class InviteWithCounts(Invite, _GuildPreviewUnique):
|
||||
...
|
||||
|
||||
|
||||
class _GatewayInviteCreateOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
inviter: PartialUser
|
||||
target_type: InviteTargetType
|
||||
target_user: PartialUser
|
||||
target_application: PartialAppInfo
|
||||
|
||||
|
||||
class GatewayInviteCreate(_GatewayInviteCreateOptional):
|
||||
channel_id: Snowflake
|
||||
code: str
|
||||
created_at: str
|
||||
max_age: int
|
||||
max_uses: int
|
||||
temporary: bool
|
||||
uses: bool
|
||||
|
||||
|
||||
class _GatewayInviteDeleteOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class GatewayInviteDelete(_GatewayInviteDeleteOptional):
|
||||
channel_id: Snowflake
|
||||
code: str
|
||||
|
||||
|
||||
GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete]
|
||||
63
discord/types/member.py
Normal file
63
discord/types/member.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import TypedDict
|
||||
from .snowflake import SnowflakeList
|
||||
from .user import User
|
||||
|
||||
|
||||
class Nickname(TypedDict):
|
||||
nick: str
|
||||
|
||||
|
||||
class PartialMember(TypedDict):
|
||||
roles: SnowflakeList
|
||||
joined_at: str
|
||||
deaf: str
|
||||
mute: str
|
||||
|
||||
|
||||
class Member(PartialMember, total=False):
|
||||
avatar: str
|
||||
user: User
|
||||
nick: str
|
||||
premium_since: str
|
||||
pending: bool
|
||||
permissions: str
|
||||
|
||||
|
||||
class _OptionalMemberWithUser(PartialMember, total=False):
|
||||
avatar: str
|
||||
nick: str
|
||||
premium_since: str
|
||||
pending: bool
|
||||
permissions: str
|
||||
|
||||
|
||||
class MemberWithUser(_OptionalMemberWithUser):
|
||||
user: User
|
||||
|
||||
|
||||
class UserWithMember(User, total=False):
|
||||
member: _OptionalMemberWithUser
|
||||
139
discord/types/message.py
Normal file
139
discord/types/message.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
from .snowflake import Snowflake, SnowflakeList
|
||||
from .member import Member, UserWithMember
|
||||
from .user import User
|
||||
from .emoji import PartialEmoji
|
||||
from .embed import Embed
|
||||
from .channel import ChannelType
|
||||
from .components import Component
|
||||
from .interactions import MessageInteraction
|
||||
from .sticker import StickerItem
|
||||
|
||||
|
||||
class ChannelMention(TypedDict):
|
||||
id: Snowflake
|
||||
guild_id: Snowflake
|
||||
type: ChannelType
|
||||
name: str
|
||||
|
||||
|
||||
class Reaction(TypedDict):
|
||||
count: int
|
||||
me: bool
|
||||
emoji: PartialEmoji
|
||||
|
||||
|
||||
class _AttachmentOptional(TypedDict, total=False):
|
||||
height: Optional[int]
|
||||
width: Optional[int]
|
||||
content_type: str
|
||||
ephemeral: bool
|
||||
spoiler: bool
|
||||
|
||||
|
||||
class Attachment(_AttachmentOptional):
|
||||
id: Snowflake
|
||||
filename: str
|
||||
size: int
|
||||
url: str
|
||||
proxy_url: str
|
||||
|
||||
|
||||
MessageActivityType = Literal[1, 2, 3, 5]
|
||||
|
||||
|
||||
class MessageActivity(TypedDict):
|
||||
type: MessageActivityType
|
||||
party_id: str
|
||||
|
||||
|
||||
class _MessageApplicationOptional(TypedDict, total=False):
|
||||
cover_image: str
|
||||
|
||||
|
||||
class MessageApplication(_MessageApplicationOptional):
|
||||
id: Snowflake
|
||||
description: str
|
||||
icon: Optional[str]
|
||||
name: str
|
||||
|
||||
|
||||
class MessageReference(TypedDict, total=False):
|
||||
message_id: Snowflake
|
||||
channel_id: Snowflake
|
||||
guild_id: Snowflake
|
||||
fail_if_not_exists: bool
|
||||
|
||||
|
||||
class _MessageOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
member: Member
|
||||
mention_channels: List[ChannelMention]
|
||||
reactions: List[Reaction]
|
||||
nonce: Union[int, str]
|
||||
webhook_id: Snowflake
|
||||
activity: MessageActivity
|
||||
application: MessageApplication
|
||||
application_id: Snowflake
|
||||
message_reference: MessageReference
|
||||
flags: int
|
||||
sticker_items: List[StickerItem]
|
||||
referenced_message: Optional[Message]
|
||||
interaction: MessageInteraction
|
||||
components: List[Component]
|
||||
|
||||
|
||||
MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21]
|
||||
|
||||
|
||||
class Message(_MessageOptional):
|
||||
id: Snowflake
|
||||
channel_id: Snowflake
|
||||
author: User
|
||||
content: str
|
||||
timestamp: str
|
||||
edited_timestamp: Optional[str]
|
||||
tts: bool
|
||||
mention_everyone: bool
|
||||
mentions: List[UserWithMember]
|
||||
mention_roles: SnowflakeList
|
||||
attachments: List[Attachment]
|
||||
embeds: List[Embed]
|
||||
pinned: bool
|
||||
type: MessageType
|
||||
|
||||
|
||||
AllowedMentionType = Literal["roles", "users", "everyone"]
|
||||
|
||||
|
||||
class AllowedMentions(TypedDict):
|
||||
parse: List[AllowedMentionType]
|
||||
roles: SnowflakeList
|
||||
users: SnowflakeList
|
||||
replied_user: bool
|
||||
98
discord/types/raw_models.py
Normal file
98
discord/types/raw_models.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import TypedDict, List
|
||||
from .snowflake import Snowflake
|
||||
from .member import Member
|
||||
from .emoji import PartialEmoji
|
||||
|
||||
|
||||
class _MessageEventOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class MessageDeleteEvent(_MessageEventOptional):
|
||||
id: Snowflake
|
||||
channel_id: Snowflake
|
||||
|
||||
|
||||
class BulkMessageDeleteEvent(_MessageEventOptional):
|
||||
ids: List[Snowflake]
|
||||
channel_id: Snowflake
|
||||
|
||||
|
||||
class _ReactionActionEventOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
member: Member
|
||||
|
||||
|
||||
class MessageUpdateEvent(_MessageEventOptional):
|
||||
id: Snowflake
|
||||
channel_id: Snowflake
|
||||
|
||||
|
||||
class ReactionActionEvent(_ReactionActionEventOptional):
|
||||
user_id: Snowflake
|
||||
channel_id: Snowflake
|
||||
message_id: Snowflake
|
||||
emoji: PartialEmoji
|
||||
|
||||
|
||||
class _ReactionClearEventOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class ReactionClearEvent(_ReactionClearEventOptional):
|
||||
channel_id: Snowflake
|
||||
message_id: Snowflake
|
||||
|
||||
|
||||
class _ReactionClearEmojiEventOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class ReactionClearEmojiEvent(_ReactionClearEmojiEventOptional):
|
||||
channel_id: int
|
||||
message_id: int
|
||||
emoji: PartialEmoji
|
||||
|
||||
|
||||
class _IntegrationDeleteEventOptional(TypedDict, total=False):
|
||||
application_id: Snowflake
|
||||
|
||||
|
||||
class IntegrationDeleteEvent(_IntegrationDeleteEventOptional):
|
||||
id: Snowflake
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class _TypingEventOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
member: Member
|
||||
|
||||
|
||||
class TypingEvent(_TypingEventOptional):
|
||||
channel_id: Snowflake
|
||||
user_id: Snowflake
|
||||
timestamp: int
|
||||
49
discord/types/role.py
Normal file
49
discord/types/role.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
from .snowflake import Snowflake
|
||||
|
||||
|
||||
class _RoleOptional(TypedDict, total=False):
|
||||
tags: RoleTags
|
||||
|
||||
|
||||
class Role(_RoleOptional):
|
||||
id: Snowflake
|
||||
name: str
|
||||
color: int
|
||||
hoist: bool
|
||||
position: int
|
||||
permissions: str
|
||||
managed: bool
|
||||
mentionable: bool
|
||||
|
||||
|
||||
class RoleTags(TypedDict, total=False):
|
||||
bot_id: Snowflake
|
||||
integration_id: Snowflake
|
||||
premium_subscriber: None
|
||||
@@ -22,7 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
Snowflake = str
|
||||
Snowflake = Union[str, int]
|
||||
SnowflakeList = List[Snowflake]
|
||||
|
||||
93
discord/types/sticker.py
Normal file
93
discord/types/sticker.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, TypedDict, Union
|
||||
from .snowflake import Snowflake
|
||||
from .user import User
|
||||
|
||||
StickerFormatType = Literal[1, 2, 3]
|
||||
|
||||
|
||||
class StickerItem(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
format_type: StickerFormatType
|
||||
|
||||
|
||||
class BaseSticker(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
description: str
|
||||
tags: str
|
||||
format_type: StickerFormatType
|
||||
|
||||
|
||||
class StandardSticker(BaseSticker):
|
||||
type: Literal[1]
|
||||
sort_value: int
|
||||
pack_id: Snowflake
|
||||
|
||||
|
||||
class _GuildStickerOptional(TypedDict, total=False):
|
||||
user: User
|
||||
|
||||
|
||||
class GuildSticker(BaseSticker, _GuildStickerOptional):
|
||||
type: Literal[2]
|
||||
available: bool
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
Sticker = Union[BaseSticker, StandardSticker, GuildSticker]
|
||||
|
||||
|
||||
class StickerPack(TypedDict):
|
||||
id: Snowflake
|
||||
stickers: List[StandardSticker]
|
||||
name: str
|
||||
sku_id: Snowflake
|
||||
cover_sticker_id: Snowflake
|
||||
description: str
|
||||
banner_asset_id: Snowflake
|
||||
|
||||
|
||||
class _CreateGuildStickerOptional(TypedDict, total=False):
|
||||
description: str
|
||||
|
||||
|
||||
class CreateGuildSticker(_CreateGuildStickerOptional):
|
||||
name: str
|
||||
tags: str
|
||||
|
||||
|
||||
class EditGuildSticker(TypedDict, total=False):
|
||||
name: str
|
||||
tags: str
|
||||
description: str
|
||||
|
||||
|
||||
class ListPremiumStickerPacks(TypedDict):
|
||||
sticker_packs: List[StickerPack]
|
||||
45
discord/types/team.py
Normal file
45
discord/types/team.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict, List, Optional
|
||||
|
||||
from .user import PartialUser
|
||||
from .snowflake import Snowflake
|
||||
|
||||
|
||||
class TeamMember(TypedDict):
|
||||
user: PartialUser
|
||||
membership_state: int
|
||||
permissions: List[str]
|
||||
team_id: Snowflake
|
||||
|
||||
|
||||
class Team(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
owner_id: Snowflake
|
||||
members: List[TeamMember]
|
||||
icon: Optional[str]
|
||||
49
discord/types/template.py
Normal file
49
discord/types/template.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TypedDict
|
||||
from .snowflake import Snowflake
|
||||
from .user import User
|
||||
from .guild import Guild
|
||||
|
||||
|
||||
class CreateTemplate(TypedDict):
|
||||
name: str
|
||||
icon: Optional[bytes]
|
||||
|
||||
|
||||
class Template(TypedDict):
|
||||
code: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
usage_count: int
|
||||
creator_id: Snowflake
|
||||
creator: User
|
||||
created_at: str
|
||||
updated_at: str
|
||||
source_guild_id: Snowflake
|
||||
serialized_source_guild: Guild
|
||||
is_dirty: Optional[bool]
|
||||
75
discord/types/threads.py
Normal file
75
discord/types/threads.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
from .snowflake import Snowflake
|
||||
|
||||
ThreadType = Literal[10, 11, 12]
|
||||
ThreadArchiveDuration = Literal[60, 1440, 4320, 10080]
|
||||
|
||||
|
||||
class ThreadMember(TypedDict):
|
||||
id: Snowflake
|
||||
user_id: Snowflake
|
||||
join_timestamp: str
|
||||
flags: int
|
||||
|
||||
|
||||
class _ThreadMetadataOptional(TypedDict, total=False):
|
||||
archiver_id: Snowflake
|
||||
locked: bool
|
||||
invitable: bool
|
||||
|
||||
|
||||
class ThreadMetadata(_ThreadMetadataOptional):
|
||||
archived: bool
|
||||
auto_archive_duration: ThreadArchiveDuration
|
||||
archive_timestamp: str
|
||||
|
||||
|
||||
class _ThreadOptional(TypedDict, total=False):
|
||||
member: ThreadMember
|
||||
last_message_id: Optional[Snowflake]
|
||||
last_pin_timestamp: Optional[Snowflake]
|
||||
|
||||
|
||||
class Thread(_ThreadOptional):
|
||||
id: Snowflake
|
||||
guild_id: Snowflake
|
||||
parent_id: Snowflake
|
||||
owner_id: Snowflake
|
||||
name: str
|
||||
type: ThreadType
|
||||
member_count: int
|
||||
message_count: int
|
||||
rate_limit_per_user: int
|
||||
thread_metadata: ThreadMetadata
|
||||
|
||||
|
||||
class ThreadPaginationPayload(TypedDict):
|
||||
threads: List[Thread]
|
||||
members: List[ThreadMember]
|
||||
has_more: bool
|
||||
@@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .snowflake import Snowflake
|
||||
from typing import Optional, TypedDict
|
||||
from typing import Literal, Optional, TypedDict
|
||||
|
||||
|
||||
class PartialUser(TypedDict):
|
||||
@@ -31,3 +31,18 @@ class PartialUser(TypedDict):
|
||||
username: str
|
||||
discriminator: str
|
||||
avatar: Optional[str]
|
||||
|
||||
|
||||
PremiumType = Literal[0, 1, 2]
|
||||
|
||||
|
||||
class User(PartialUser, total=False):
|
||||
bot: bool
|
||||
system: bool
|
||||
mfa_enabled: bool
|
||||
local: str
|
||||
verified: bool
|
||||
email: Optional[str]
|
||||
flags: int
|
||||
premium_type: PremiumType
|
||||
public_flags: int
|
||||
|
||||
85
discord/types/voice.py
Normal file
85
discord/types/voice.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import Optional, TypedDict, List, Literal
|
||||
from .snowflake import Snowflake
|
||||
from .member import MemberWithUser
|
||||
|
||||
|
||||
SupportedModes = Literal["xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305"]
|
||||
|
||||
|
||||
class _PartialVoiceStateOptional(TypedDict, total=False):
|
||||
member: MemberWithUser
|
||||
self_stream: bool
|
||||
|
||||
|
||||
class _VoiceState(_PartialVoiceStateOptional):
|
||||
user_id: Snowflake
|
||||
session_id: str
|
||||
deaf: bool
|
||||
mute: bool
|
||||
self_deaf: bool
|
||||
self_mute: bool
|
||||
self_video: bool
|
||||
suppress: bool
|
||||
|
||||
|
||||
class GuildVoiceState(_VoiceState):
|
||||
channel_id: Snowflake
|
||||
|
||||
|
||||
class VoiceState(_VoiceState, total=False):
|
||||
channel_id: Optional[Snowflake]
|
||||
guild_id: Snowflake
|
||||
|
||||
|
||||
class VoiceRegion(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
vip: bool
|
||||
optimal: bool
|
||||
deprecated: bool
|
||||
custom: bool
|
||||
|
||||
|
||||
class VoiceServerUpdate(TypedDict):
|
||||
token: str
|
||||
guild_id: Snowflake
|
||||
endpoint: Optional[str]
|
||||
|
||||
|
||||
class VoiceIdentify(TypedDict):
|
||||
server_id: Snowflake
|
||||
user_id: Snowflake
|
||||
session_id: str
|
||||
token: str
|
||||
|
||||
|
||||
class VoiceReady(TypedDict):
|
||||
ssrc: int
|
||||
ip: str
|
||||
port: int
|
||||
modes: List[SupportedModes]
|
||||
heartbeat_interval: int
|
||||
70
discord/types/webhook.py
Normal file
70
discord/types/webhook.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Literal, Optional, TypedDict
|
||||
from .snowflake import Snowflake
|
||||
from .user import User
|
||||
from .channel import PartialChannel
|
||||
|
||||
|
||||
class SourceGuild(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
icon: str
|
||||
|
||||
|
||||
class _WebhookOptional(TypedDict, total=False):
|
||||
guild_id: Snowflake
|
||||
user: User
|
||||
token: str
|
||||
|
||||
|
||||
WebhookType = Literal[1, 2, 3]
|
||||
|
||||
|
||||
class _FollowerWebhookOptional(TypedDict, total=False):
|
||||
source_channel: PartialChannel
|
||||
source_guild: SourceGuild
|
||||
|
||||
|
||||
class FollowerWebhook(_FollowerWebhookOptional):
|
||||
channel_id: Snowflake
|
||||
webhook_id: Snowflake
|
||||
|
||||
|
||||
class PartialWebhook(_WebhookOptional):
|
||||
id: Snowflake
|
||||
type: WebhookType
|
||||
|
||||
|
||||
class _FullWebhook(TypedDict, total=False):
|
||||
name: Optional[str]
|
||||
avatar: Optional[str]
|
||||
channel_id: Snowflake
|
||||
application_id: Optional[Snowflake]
|
||||
|
||||
|
||||
class Webhook(PartialWebhook, _FullWebhook):
|
||||
...
|
||||
40
discord/types/welcome_screen.py
Normal file
40
discord/types/welcome_screen.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, TypedDict
|
||||
from .snowflake import Snowflake
|
||||
|
||||
|
||||
class WelcomeScreen(TypedDict):
|
||||
description: str
|
||||
welcome_channels: List[WelcomeScreenChannel]
|
||||
|
||||
|
||||
class WelcomeScreenChannel(TypedDict):
|
||||
channel_id: Snowflake
|
||||
description: str
|
||||
emoji_id: Optional[Snowflake]
|
||||
emoji_name: Optional[str]
|
||||
63
discord/types/widget.py
Normal file
63
discord/types/widget.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, TypedDict
|
||||
from .activity import Activity
|
||||
from .snowflake import Snowflake
|
||||
from .user import User
|
||||
|
||||
|
||||
class WidgetChannel(TypedDict):
|
||||
id: Snowflake
|
||||
name: str
|
||||
position: int
|
||||
|
||||
|
||||
class WidgetMember(User, total=False):
|
||||
nick: str
|
||||
game: Activity
|
||||
status: str
|
||||
avatar_url: str
|
||||
deaf: bool
|
||||
self_deaf: bool
|
||||
mute: bool
|
||||
self_mute: bool
|
||||
suppress: bool
|
||||
|
||||
|
||||
class _WidgetOptional(TypedDict, total=False):
|
||||
channels: List[WidgetChannel]
|
||||
members: List[WidgetMember]
|
||||
presence_count: int
|
||||
|
||||
|
||||
class Widget(_WidgetOptional):
|
||||
id: Snowflake
|
||||
name: str
|
||||
instant_invite: str
|
||||
|
||||
|
||||
class WidgetSettings(TypedDict):
|
||||
enabled: bool
|
||||
channel_id: Optional[Snowflake]
|
||||
15
discord/ui/__init__.py
Normal file
15
discord/ui/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
discord.ui
|
||||
~~~~~~~~~~~
|
||||
|
||||
Bot UI Kit helper for the Discord API
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
from .view import *
|
||||
from .item import *
|
||||
from .button import *
|
||||
from .select import *
|
||||
290
discord/ui/button.py
Normal file
290
discord/ui/button.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||
import inspect
|
||||
import os
|
||||
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from ..enums import ButtonStyle, ComponentType
|
||||
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||
from ..components import Button as ButtonComponent
|
||||
|
||||
__all__ = (
|
||||
"Button",
|
||||
"button",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .view import View
|
||||
from ..emoji import Emoji
|
||||
|
||||
B = TypeVar("B", bound="Button")
|
||||
V = TypeVar("V", bound="View", covariant=True)
|
||||
|
||||
|
||||
class Button(Item[V]):
|
||||
"""Represents a UI button.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
------------
|
||||
style: :class:`discord.ButtonStyle`
|
||||
The style of the button.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the button that gets received during an interaction.
|
||||
If this button is for a URL, it does not have a custom ID.
|
||||
url: Optional[:class:`str`]
|
||||
The URL this button sends you to.
|
||||
disabled: :class:`bool`
|
||||
Whether the button is disabled or not.
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any.
|
||||
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
|
||||
The emoji of the button, if available.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this button belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
"style",
|
||||
"url",
|
||||
"disabled",
|
||||
"label",
|
||||
"emoji",
|
||||
"row",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
label: Optional[str] = None,
|
||||
disabled: bool = False,
|
||||
custom_id: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
row: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
if custom_id is not None and url is not None:
|
||||
raise TypeError("cannot mix both url and custom_id with Button")
|
||||
|
||||
self._provided_custom_id = custom_id is not None
|
||||
if url is None and custom_id is None:
|
||||
custom_id = os.urandom(16).hex()
|
||||
|
||||
if url is not None:
|
||||
style = ButtonStyle.link
|
||||
|
||||
if emoji is not None:
|
||||
if isinstance(emoji, str):
|
||||
emoji = PartialEmoji.from_str(emoji)
|
||||
elif isinstance(emoji, _EmojiTag):
|
||||
emoji = emoji._to_partial()
|
||||
else:
|
||||
raise TypeError(f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}")
|
||||
|
||||
self._underlying = ButtonComponent._raw_construct(
|
||||
type=ComponentType.button,
|
||||
custom_id=custom_id,
|
||||
url=url,
|
||||
disabled=disabled,
|
||||
label=label,
|
||||
style=style,
|
||||
emoji=emoji,
|
||||
)
|
||||
self.row = row
|
||||
|
||||
@property
|
||||
def style(self) -> ButtonStyle:
|
||||
""":class:`discord.ButtonStyle`: The style of the button."""
|
||||
return self._underlying.style
|
||||
|
||||
@style.setter
|
||||
def style(self, value: ButtonStyle):
|
||||
self._underlying.style = value
|
||||
|
||||
@property
|
||||
def custom_id(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
|
||||
|
||||
If this button is for a URL, it does not have a custom ID.
|
||||
"""
|
||||
return self._underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: Optional[str]):
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise TypeError("custom_id must be None or str")
|
||||
|
||||
self._underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def url(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: The URL this button sends you to."""
|
||||
return self._underlying.url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: Optional[str]):
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise TypeError("url must be None or str")
|
||||
self._underlying.url = value
|
||||
|
||||
@property
|
||||
def disabled(self) -> bool:
|
||||
""":class:`bool`: Whether the button is disabled or not."""
|
||||
return self._underlying.disabled
|
||||
|
||||
@disabled.setter
|
||||
def disabled(self, value: bool):
|
||||
self._underlying.disabled = bool(value)
|
||||
|
||||
@property
|
||||
def label(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: The label of the button, if available."""
|
||||
return self._underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: Optional[str]):
|
||||
self._underlying.label = str(value) if value is not None else value
|
||||
|
||||
@property
|
||||
def emoji(self) -> Optional[PartialEmoji]:
|
||||
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available."""
|
||||
return self._underlying.emoji
|
||||
|
||||
@emoji.setter
|
||||
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
|
||||
if value is not None:
|
||||
if isinstance(value, str):
|
||||
self._underlying.emoji = PartialEmoji.from_str(value)
|
||||
elif isinstance(value, _EmojiTag):
|
||||
self._underlying.emoji = value._to_partial()
|
||||
else:
|
||||
raise TypeError(f"expected str, Emoji, or PartialEmoji, received {value.__class__} instead")
|
||||
else:
|
||||
self._underlying.emoji = None
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: Type[B], button: ButtonComponent) -> B:
|
||||
return cls(
|
||||
style=button.style,
|
||||
label=button.label,
|
||||
disabled=button.disabled,
|
||||
custom_id=button.custom_id,
|
||||
url=button.url,
|
||||
emoji=button.emoji,
|
||||
row=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self) -> ComponentType:
|
||||
return self._underlying.type
|
||||
|
||||
def to_component_dict(self):
|
||||
return self._underlying.to_dict()
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return self.custom_id is not None
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
if self.style is ButtonStyle.link:
|
||||
return self.url is not None
|
||||
return super().is_persistent()
|
||||
|
||||
def refresh_component(self, button: ButtonComponent) -> None:
|
||||
self._underlying = button
|
||||
|
||||
|
||||
def button(
|
||||
*,
|
||||
label: Optional[str] = None,
|
||||
custom_id: Optional[str] = None,
|
||||
disabled: bool = False,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
row: Optional[int] = None,
|
||||
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||
"""A decorator that attaches a button to a component.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
the :class:`discord.ui.View`, the :class:`discord.ui.Button` being pressed and
|
||||
the :class:`discord.Interaction` you receive.
|
||||
|
||||
.. note::
|
||||
|
||||
Buttons with a URL cannot be created with this function.
|
||||
Consider creating a :class:`Button` manually instead.
|
||||
This is because buttons with a URL do not have a callback
|
||||
associated with them since Discord does not do any processing
|
||||
with it.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the button that gets received during an interaction.
|
||||
It is recommended not to set this parameter to prevent conflicts.
|
||||
style: :class:`.ButtonStyle`
|
||||
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
|
||||
disabled: :class:`bool`
|
||||
Whether the button is disabled or not. Defaults to ``False``.
|
||||
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
|
||||
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
|
||||
or a full :class:`.Emoji`.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this button belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||
if not inspect.iscoroutinefunction(func):
|
||||
raise TypeError("button function must be a coroutine function")
|
||||
|
||||
func.__discord_ui_model_type__ = Button
|
||||
func.__discord_ui_model_kwargs__ = {
|
||||
"style": style,
|
||||
"custom_id": custom_id,
|
||||
"url": None,
|
||||
"disabled": disabled,
|
||||
"label": label,
|
||||
"emoji": emoji,
|
||||
"row": row,
|
||||
}
|
||||
return func
|
||||
|
||||
return decorator
|
||||
129
discord/ui/item.py
Normal file
129
discord/ui/item.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar
|
||||
|
||||
from ..interactions import Interaction
|
||||
|
||||
__all__ = ("Item",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..enums import ComponentType
|
||||
from .view import View
|
||||
from ..components import Component
|
||||
|
||||
I = TypeVar("I", bound="Item")
|
||||
V = TypeVar("V", bound="View", covariant=True)
|
||||
ItemCallbackType = Callable[[Any, I, Interaction], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
class Item(Generic[V]):
|
||||
"""Represents the base UI item that all UI components inherit from.
|
||||
|
||||
The current UI items supported are:
|
||||
|
||||
- :class:`discord.ui.Button`
|
||||
- :class:`discord.ui.Select`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = ("row",)
|
||||
|
||||
def __init__(self):
|
||||
self._view: Optional[V] = None
|
||||
self._row: Optional[int] = None
|
||||
self._rendered_row: Optional[int] = None
|
||||
# This works mostly well but there is a gotcha with
|
||||
# the interaction with from_component, since that technically provides
|
||||
# a custom_id most dispatchable items would get this set to True even though
|
||||
# it might not be provided by the library user. However, this edge case doesn't
|
||||
# actually affect the intended purpose of this check because from_component is
|
||||
# only called upon edit and we're mainly interested during initial creation time.
|
||||
self._provided_custom_id: bool = False
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh_component(self, component: Component) -> None:
|
||||
return None
|
||||
|
||||
def refresh_state(self, interaction: Interaction) -> None:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: Type[I], component: Component) -> I:
|
||||
return cls()
|
||||
|
||||
@property
|
||||
def type(self) -> ComponentType:
|
||||
raise NotImplementedError
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return self._provided_custom_id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__)
|
||||
return f"<{self.__class__.__name__} {attrs}>"
|
||||
|
||||
@property
|
||||
def row(self) -> Optional[int]:
|
||||
return self._row
|
||||
|
||||
@row.setter
|
||||
def row(self, value: Optional[int]):
|
||||
if value is None:
|
||||
self._row = None
|
||||
elif 5 > value >= 0:
|
||||
self._row = value
|
||||
else:
|
||||
raise ValueError("row cannot be negative or greater than or equal to 5")
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def view(self) -> Optional[V]:
|
||||
"""Optional[:class:`View`]: The underlying view for this item."""
|
||||
return self._view
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
"""|coro|
|
||||
|
||||
The callback associated with this UI item.
|
||||
|
||||
This can be overriden by subclasses.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`.Interaction`
|
||||
The interaction that triggered this UI item.
|
||||
"""
|
||||
pass
|
||||
356
discord/ui/select.py
Normal file
356
discord/ui/select.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type, Callable, Union
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from ..enums import ComponentType
|
||||
from ..partial_emoji import PartialEmoji
|
||||
from ..emoji import Emoji
|
||||
from ..interactions import Interaction
|
||||
from ..utils import MISSING
|
||||
from ..components import (
|
||||
SelectOption,
|
||||
SelectMenu,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"Select",
|
||||
"select",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .view import View
|
||||
from ..types.components import SelectMenu as SelectMenuPayload
|
||||
from ..types.interactions import (
|
||||
ComponentInteractionData,
|
||||
)
|
||||
|
||||
S = TypeVar("S", bound="Select")
|
||||
V = TypeVar("V", bound="View", covariant=True)
|
||||
|
||||
|
||||
class Select(Item[V]):
|
||||
"""Represents a UI select menu.
|
||||
|
||||
This is usually represented as a drop down menu.
|
||||
|
||||
In order to get the selected items that the user has chosen, use :attr:`Select.values`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
------------
|
||||
custom_id: :class:`str`
|
||||
The ID of the select menu that gets received during an interaction.
|
||||
If not given then one is generated for you.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`discord.SelectOption`]
|
||||
A list of options that can be selected in this menu.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this select menu belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
"placeholder",
|
||||
"min_values",
|
||||
"max_values",
|
||||
"options",
|
||||
"disabled",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
custom_id: str = MISSING,
|
||||
placeholder: Optional[str] = None,
|
||||
min_values: int = 1,
|
||||
max_values: int = 1,
|
||||
options: List[SelectOption] = MISSING,
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._selected_values: List[str] = []
|
||||
self._provided_custom_id = custom_id is not MISSING
|
||||
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||
options = [] if options is MISSING else options
|
||||
self._underlying = SelectMenu._raw_construct(
|
||||
custom_id=custom_id,
|
||||
type=ComponentType.select,
|
||||
placeholder=placeholder,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
options=options,
|
||||
disabled=disabled,
|
||||
)
|
||||
self.row = row
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
""":class:`str`: The ID of the select menu that gets received during an interaction."""
|
||||
return self._underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError("custom_id must be None or str")
|
||||
|
||||
self._underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def placeholder(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any."""
|
||||
return self._underlying.placeholder
|
||||
|
||||
@placeholder.setter
|
||||
def placeholder(self, value: Optional[str]):
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise TypeError("placeholder must be None or str")
|
||||
|
||||
self._underlying.placeholder = value
|
||||
|
||||
@property
|
||||
def min_values(self) -> int:
|
||||
""":class:`int`: The minimum number of items that must be chosen for this select menu."""
|
||||
return self._underlying.min_values
|
||||
|
||||
@min_values.setter
|
||||
def min_values(self, value: int):
|
||||
self._underlying.min_values = int(value)
|
||||
|
||||
@property
|
||||
def max_values(self) -> int:
|
||||
""":class:`int`: The maximum number of items that must be chosen for this select menu."""
|
||||
return self._underlying.max_values
|
||||
|
||||
@max_values.setter
|
||||
def max_values(self, value: int):
|
||||
self._underlying.max_values = int(value)
|
||||
|
||||
@property
|
||||
def options(self) -> List[SelectOption]:
|
||||
"""List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu."""
|
||||
return self._underlying.options
|
||||
|
||||
@options.setter
|
||||
def options(self, value: List[SelectOption]):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("options must be a list of SelectOption")
|
||||
if not all(isinstance(obj, SelectOption) for obj in value):
|
||||
raise TypeError("all list items must subclass SelectOption")
|
||||
|
||||
self._underlying.options = value
|
||||
|
||||
def add_option(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
value: str = MISSING,
|
||||
description: Optional[str] = None,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
default: bool = False,
|
||||
):
|
||||
"""Adds an option to the select menu.
|
||||
|
||||
To append a pre-existing :class:`discord.SelectOption` use the
|
||||
:meth:`append_option` method instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
label: :class:`str`
|
||||
The label of the option. This is displayed to users.
|
||||
Can only be up to 100 characters.
|
||||
value: :class:`str`
|
||||
The value of the option. This is not displayed to users.
|
||||
If not given, defaults to the label. Can only be up to 100 characters.
|
||||
description: Optional[:class:`str`]
|
||||
An additional description of the option, if any.
|
||||
Can only be up to 100 characters.
|
||||
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
|
||||
The emoji of the option, if available. This can either be a string representing
|
||||
the custom or unicode emoji or an instance of :class:`.PartialEmoji` or :class:`.Emoji`.
|
||||
default: :class:`bool`
|
||||
Whether this option is selected by default.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The number of options exceeds 25.
|
||||
"""
|
||||
|
||||
option = SelectOption(
|
||||
label=label,
|
||||
value=value,
|
||||
description=description,
|
||||
emoji=emoji,
|
||||
default=default,
|
||||
)
|
||||
|
||||
self.append_option(option)
|
||||
|
||||
def append_option(self, option: SelectOption):
|
||||
"""Appends an option to the select menu.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
option: :class:`discord.SelectOption`
|
||||
The option to append to the select menu.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ValueError
|
||||
The number of options exceeds 25.
|
||||
"""
|
||||
|
||||
if len(self._underlying.options) > 25:
|
||||
raise ValueError("maximum number of options already provided")
|
||||
|
||||
self._underlying.options.append(option)
|
||||
|
||||
@property
|
||||
def disabled(self) -> bool:
|
||||
""":class:`bool`: Whether the select is disabled or not."""
|
||||
return self._underlying.disabled
|
||||
|
||||
@disabled.setter
|
||||
def disabled(self, value: bool):
|
||||
self._underlying.disabled = bool(value)
|
||||
|
||||
@property
|
||||
def values(self) -> List[str]:
|
||||
"""List[:class:`str`]: A list of values that have been selected by the user."""
|
||||
return self._selected_values
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 5
|
||||
|
||||
def to_component_dict(self) -> SelectMenuPayload:
|
||||
return self._underlying.to_dict()
|
||||
|
||||
def refresh_component(self, component: SelectMenu) -> None:
|
||||
self._underlying = component
|
||||
|
||||
def refresh_state(self, interaction: Interaction) -> None:
|
||||
data: ComponentInteractionData = interaction.data # type: ignore
|
||||
self._selected_values = data.get("values", [])
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: Type[S], component: SelectMenu) -> S:
|
||||
return cls(
|
||||
custom_id=component.custom_id,
|
||||
placeholder=component.placeholder,
|
||||
min_values=component.min_values,
|
||||
max_values=component.max_values,
|
||||
options=component.options,
|
||||
disabled=component.disabled,
|
||||
row=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self) -> ComponentType:
|
||||
return self._underlying.type
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def select(
|
||||
*,
|
||||
placeholder: Optional[str] = None,
|
||||
custom_id: str = MISSING,
|
||||
min_values: int = 1,
|
||||
max_values: int = 1,
|
||||
options: List[SelectOption] = MISSING,
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||
"""A decorator that attaches a select menu to a component.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
the :class:`discord.ui.View`, the :class:`discord.ui.Select` being pressed and
|
||||
the :class:`discord.Interaction` you receive.
|
||||
|
||||
In order to get the selected items that the user has chosen within the callback
|
||||
use :attr:`Select.values`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
custom_id: :class:`str`
|
||||
The ID of the select menu that gets received during an interaction.
|
||||
It is recommended not to set this parameter to prevent conflicts.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this select menu belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`discord.SelectOption`]
|
||||
A list of options that can be selected in this menu.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not. Defaults to ``False``.
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||
if not inspect.iscoroutinefunction(func):
|
||||
raise TypeError("select function must be a coroutine function")
|
||||
|
||||
func.__discord_ui_model_type__ = Select
|
||||
func.__discord_ui_model_kwargs__ = {
|
||||
"placeholder": placeholder,
|
||||
"custom_id": custom_id,
|
||||
"row": row,
|
||||
"min_values": min_values,
|
||||
"max_values": max_values,
|
||||
"options": options,
|
||||
"disabled": disabled,
|
||||
}
|
||||
return func
|
||||
|
||||
return decorator
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user