mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 08:52:31 -05:00
Compare commits
641 Commits
3.0.0
...
V3/feature
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f28df2dd0 | ||
|
|
677d700363 | ||
|
|
8d73838d80 | ||
|
|
1fc4ece14c | ||
|
|
0adc960c60 | ||
|
|
c426aefd1a | ||
|
|
00cf395483 | ||
|
|
61ed864e02 | ||
|
|
90b099395b | ||
|
|
12e6f44135 | ||
|
|
e44fc69d14 | ||
|
|
8454239a98 | ||
|
|
64106c771a | ||
|
|
17234ac8fa | ||
|
|
b64802b92f | ||
|
|
6fa02b1a8d | ||
|
|
7420df9598 | ||
|
|
00bcd480e7 | ||
|
|
0d3c72f356 | ||
|
|
97a9fde5fd | ||
|
|
a664615a2d | ||
|
|
3d4f9500e9 | ||
|
|
a8450580e8 | ||
|
|
8654924869 | ||
|
|
068585379a | ||
|
|
fc5fc08962 | ||
|
|
41fdcb2ae8 | ||
|
|
de4804863a | ||
|
|
2ac4dde729 | ||
|
|
498d0d22fb | ||
|
|
2a38777379 | ||
|
|
01c1fdfd16 | ||
|
|
0a8e7f5663 | ||
|
|
1755334124 | ||
|
|
40c0d8d83b | ||
|
|
ee53d50c3a | ||
|
|
8570971f68 | ||
|
|
e1a110b1bf | ||
|
|
77235f7750 | ||
|
|
c7fd64e0c8 | ||
|
|
8f04fd436f | ||
|
|
b085c1501f | ||
|
|
7f390df879 | ||
|
|
54e65082bc | ||
|
|
4c77cde249 | ||
|
|
826dae129e | ||
|
|
12da3bd89e | ||
|
|
b089be7b49 | ||
|
|
33ea3a1419 | ||
|
|
66cae71d90 | ||
|
|
6219f0da67 | ||
|
|
7f2e5a0b70 | ||
|
|
d52f8974fd | ||
|
|
2c12e4f6bf | ||
|
|
b88bd5d44d | ||
|
|
41b283ce5d | ||
|
|
cd7f4681a4 | ||
|
|
d1b7f836db | ||
|
|
3d1e6eab00 | ||
|
|
48ccd9070c | ||
|
|
67fbcb1b4a | ||
|
|
a203fe34cf | ||
|
|
85438e7454 | ||
|
|
d6d14617d2 | ||
|
|
a1b95e5072 | ||
|
|
29feab638a | ||
|
|
60dc54b081 | ||
|
|
1cb43b11a1 | ||
|
|
27e6f677e8 | ||
|
|
d8199201a5 | ||
|
|
a7f0e2b7c6 | ||
|
|
79dcd22ff6 | ||
|
|
2be4080bc6 | ||
|
|
b646c2fd98 | ||
|
|
90c0f76ae4 | ||
|
|
3c53b89040 | ||
|
|
a7987a83fd | ||
|
|
ef8b57a1d2 | ||
|
|
ab2e87a8fb | ||
|
|
088360ec51 | ||
|
|
7bdd177713 | ||
|
|
81b6d5bb93 | ||
|
|
5eb4bda600 | ||
|
|
cb49c5d420 | ||
|
|
a984971774 | ||
|
|
9f027cc3e0 | ||
|
|
fe7770c833 | ||
|
|
8514dbe96a | ||
|
|
ed76454ddb | ||
|
|
54711b2054 | ||
|
|
fdfbfe7b60 | ||
|
|
d6936c87f3 | ||
|
|
a105217e83 | ||
|
|
e52c20b9e7 | ||
|
|
4754160055 | ||
|
|
d2b9504c3b | ||
|
|
b0b76c5a00 | ||
|
|
deab24e916 | ||
|
|
2bb9b87db9 | ||
|
|
5bd044d646 | ||
|
|
371292e03a | ||
|
|
acc5baec7d | ||
|
|
ed692bcaa5 | ||
|
|
7352f76b87 | ||
|
|
ad505b2b2f | ||
|
|
6ce421363d | ||
|
|
248259b312 | ||
|
|
f010df7082 | ||
|
|
69e2ebf2e7 | ||
|
|
c852505a62 | ||
|
|
d3e8d99bdf | ||
|
|
a9d3e271b0 | ||
|
|
139119e954 | ||
|
|
09a3a87cef | ||
|
|
83e93916e8 | ||
|
|
3546dd14d0 | ||
|
|
e75b5b3be5 | ||
|
|
ed3b4e5b29 | ||
|
|
9698baf6e7 | ||
|
|
26677004f1 | ||
|
|
25f0c37a20 | ||
|
|
ab3b567cd8 | ||
|
|
a0f548fc0b | ||
|
|
e3720bb4a6 | ||
|
|
b35b8d98c3 | ||
|
|
2612453597 | ||
|
|
42d83e80a3 | ||
|
|
44e680ee41 | ||
|
|
965416de73 | ||
|
|
1c75c47a9c | ||
|
|
35c27c5741 | ||
|
|
af859aa755 | ||
|
|
f5949f2664 | ||
|
|
96e9e55642 | ||
|
|
778c701b87 | ||
|
|
a73b174d9f | ||
|
|
17123c1d88 | ||
|
|
45860ca2a6 | ||
|
|
d3c97eedfe | ||
|
|
a2b68ea7fc | ||
|
|
62aad10008 | ||
|
|
474bb0904e | ||
|
|
b0f840c273 | ||
|
|
9ec78d1455 | ||
|
|
f2d2b9a682 | ||
|
|
23fe991c36 | ||
|
|
d9d2e0017e | ||
|
|
b6ca8f7d2c | ||
|
|
cacfa163ce | ||
|
|
d00609bb8a | ||
|
|
b59f136ece | ||
|
|
25999cea10 | ||
|
|
95e8d60729 | ||
|
|
1d2dd19244 | ||
|
|
743ce71c5c | ||
|
|
42e3f73088 | ||
|
|
ec6877dbc6 | ||
|
|
c8f753db0d | ||
|
|
e776b5ca1a | ||
|
|
36e2cde04d | ||
|
|
f3e7c2028c | ||
|
|
f3c57b6730 | ||
|
|
bc90f5186a | ||
|
|
62b679b1b9 | ||
|
|
ab747d2432 | ||
|
|
debed501b2 | ||
|
|
a80e20067c | ||
|
|
22268eed9d | ||
|
|
f7e2617911 | ||
|
|
f9211ff50f | ||
|
|
aabdabf3bc | ||
|
|
de229f63fe | ||
|
|
75c4bee8a3 | ||
|
|
60a1b3294d | ||
|
|
ef99174585 | ||
|
|
8b18526f46 | ||
|
|
12af6232e2 | ||
|
|
153f4d20f1 | ||
|
|
df5cfabfe5 | ||
|
|
bf6297aaf2 | ||
|
|
b72c05d3d4 | ||
|
|
a36e95c286 | ||
|
|
b457f8d1c1 | ||
|
|
9d027747d1 | ||
|
|
6bf9ff5637 | ||
|
|
0b042532fd | ||
|
|
61f467a323 | ||
|
|
02bb1fc390 | ||
|
|
e32eecd6e7 | ||
|
|
988536f96b | ||
|
|
335ded674e | ||
|
|
b6ae7a6d21 | ||
|
|
c67b6cd443 | ||
|
|
8cba47f382 | ||
|
|
203cde9805 | ||
|
|
064d97f87b | ||
|
|
9a051ef2c6 | ||
|
|
0f62614055 | ||
|
|
672050727f | ||
|
|
d136d594f4 | ||
|
|
b4186d6724 | ||
|
|
02d6b7d658 | ||
|
|
d07e718ab8 | ||
|
|
f0836d7182 | ||
|
|
bc5c2513f6 | ||
|
|
ec834a0666 | ||
|
|
77742179c0 | ||
|
|
a3140b6659 | ||
|
|
4b62598a3d | ||
|
|
ddfabb0c0e | ||
|
|
51298f156b | ||
|
|
141b48d3cf | ||
|
|
5a7c36c581 | ||
|
|
8a90996b36 | ||
|
|
548a50b984 | ||
|
|
19e8e60a4d | ||
|
|
6aeca83c63 | ||
|
|
33178ef034 | ||
|
|
710b520da9 | ||
|
|
b3363acf77 | ||
|
|
6852b7a1d1 | ||
|
|
418f957332 | ||
|
|
a05508a9f2 | ||
|
|
dd899c804a | ||
|
|
1651de1305 | ||
|
|
078210b54c | ||
|
|
e2c8b11008 | ||
|
|
d85fb260e7 | ||
|
|
e79a08e392 | ||
|
|
7e9b1b87e6 | ||
|
|
b8cbaa2fa0 | ||
|
|
ddd9c4c6b0 | ||
|
|
a729a474b1 | ||
|
|
911aed5fe2 | ||
|
|
53606a4bbc | ||
|
|
3b653f93fc | ||
|
|
17c8cbb057 | ||
|
|
ee293876d9 | ||
|
|
3b0fa0c05d | ||
|
|
8267ad9aab | ||
|
|
a3b6aafaca | ||
|
|
2dbed96be1 | ||
|
|
2ba6fb17ca | ||
|
|
3723b4b1ea | ||
|
|
172dd58903 | ||
|
|
a9a547e56d | ||
|
|
d42a2d5140 | ||
|
|
428bf55480 | ||
|
|
36f494ba63 | ||
|
|
9b60816ebd | ||
|
|
ea77de5d56 | ||
|
|
05eba603a5 | ||
|
|
37f27d8ae4 | ||
|
|
759ca3ba7e | ||
|
|
59e7d063a0 | ||
|
|
6e3ccc1a21 | ||
|
|
80628a28a7 | ||
|
|
6bb1004bcd | ||
|
|
6170a56648 | ||
|
|
f3b6c4cf32 | ||
|
|
83483abfa5 | ||
|
|
e38c08ab12 | ||
|
|
c288185a16 | ||
|
|
b9ed8b84f5 | ||
|
|
ee162f6f9e | ||
|
|
101e977939 | ||
|
|
25614620db | ||
|
|
62dcebff94 | ||
|
|
af97175839 | ||
|
|
1ee5238ad7 | ||
|
|
575e55cb0f | ||
|
|
ee3be8b633 | ||
|
|
7f22d27d51 | ||
|
|
4546ca9ba6 | ||
|
|
1e97597bc2 | ||
|
|
77f1da30ea | ||
|
|
682b86c193 | ||
|
|
cdb7a02cb8 | ||
|
|
0be3b1acd7 | ||
|
|
4f1f49d96f | ||
|
|
6075c5bde0 | ||
|
|
d86cc7a854 | ||
|
|
25fb389a7d | ||
|
|
d9e774f079 | ||
|
|
7959e0c916 | ||
|
|
ef3ac77bd8 | ||
|
|
efcf91e934 | ||
|
|
b8a7a66566 | ||
|
|
e04eed4a89 | ||
|
|
b490942bcd | ||
|
|
26cc85806e | ||
|
|
2c8152606c | ||
|
|
3c1b6ae4cf | ||
|
|
6c3a3fea66 | ||
|
|
fb9fec282b | ||
|
|
bbd30411fd | ||
|
|
2056f9f8d0 | ||
|
|
68018c924e | ||
|
|
0c773134f2 | ||
|
|
0d16e27070 | ||
|
|
326c53d6c4 | ||
|
|
d1a46acc9a | ||
|
|
57fa29dd64 | ||
|
|
bfa55922cf | ||
|
|
43da727a9f | ||
|
|
580c4429e8 | ||
|
|
3498f8ccb6 | ||
|
|
5e9b3d9190 | ||
|
|
dfb4212d43 | ||
|
|
b1ccfab6d2 | ||
|
|
984a97d958 | ||
|
|
463546f102 | ||
|
|
477a17d0b9 | ||
|
|
556af32bb5 | ||
|
|
ef8b9b81c3 | ||
|
|
9362dd9465 | ||
|
|
6c9c57c14d | ||
|
|
404c5f6dc0 | ||
|
|
20091cc10a | ||
|
|
6280fd9c28 | ||
|
|
af096bc1cc | ||
|
|
a8091332b8 | ||
|
|
7ba50c91a2 | ||
|
|
0ba9eaeccc | ||
|
|
d4b6fdea92 | ||
|
|
f0f274e1e1 | ||
|
|
e9f014df07 | ||
|
|
778eadd418 | ||
|
|
3de9d15410 | ||
|
|
3b0567d261 | ||
|
|
49a75b5f19 | ||
|
|
8676dd3ce3 | ||
|
|
d5c412e3df | ||
|
|
1d2980f8fa | ||
|
|
3e80edcdfd | ||
|
|
be184b57dd | ||
|
|
f83f378528 | ||
|
|
21a6384ebf | ||
|
|
03e0683dd7 | ||
|
|
ac2813012a | ||
|
|
e34eca557b | ||
|
|
687b88ca6f | ||
|
|
776c75ba86 | ||
|
|
55ff9bedb7 | ||
|
|
2bdc3ac10c | ||
|
|
f2039300c2 | ||
|
|
03e59ea9d3 | ||
|
|
a89a156f8e | ||
|
|
0eb22c84ff | ||
|
|
d1593b8069 | ||
|
|
93391d028c | ||
|
|
142fb0ad08 | ||
|
|
942dca43d3 | ||
|
|
081bf663a4 | ||
|
|
55e309125e | ||
|
|
7323e8eb67 | ||
|
|
098540b9e5 | ||
|
|
10412c4f51 | ||
|
|
2f8b1a21c7 | ||
|
|
03fe3ee720 | ||
|
|
8a72840de0 | ||
|
|
8bf86f33a3 | ||
|
|
8637f8a852 | ||
|
|
bff7e214ab | ||
|
|
f3bbfdc64d | ||
|
|
461f56bca1 | ||
|
|
d1d4ec3e38 | ||
|
|
a0f34bbbd9 | ||
|
|
49819a2eeb | ||
|
|
f91d8610ae | ||
|
|
52f5d5cd6a | ||
|
|
870b615364 | ||
|
|
25ccc11dc4 | ||
|
|
f2b7ce9546 | ||
|
|
6bdc9606f6 | ||
|
|
71d0bd0d07 | ||
|
|
6ae3040aac | ||
|
|
065396abab | ||
|
|
1804314f45 | ||
|
|
ff894ecbe7 | ||
|
|
0bf54fae52 | ||
|
|
3c66c602f6 | ||
|
|
e854716236 | ||
|
|
beb16b81a9 | ||
|
|
57d5c0870a | ||
|
|
9d008d587a | ||
|
|
804d6eecea | ||
|
|
cc927248f0 | ||
|
|
d133598d80 | ||
|
|
682ee1a459 | ||
|
|
61b5730c48 | ||
|
|
463d8d6306 | ||
|
|
da40511306 | ||
|
|
c2195ec576 | ||
|
|
16443c8cc0 | ||
|
|
9116cd02e6 | ||
|
|
652d9fe950 | ||
|
|
e956e6e320 | ||
|
|
2e58922d01 | ||
|
|
33b7652b62 | ||
|
|
0e9086ca1f | ||
|
|
3ca2a9af28 | ||
|
|
e7b615d921 | ||
|
|
2cb6e98092 | ||
|
|
1ccc441aab | ||
|
|
8ddc5aa63e | ||
|
|
f894b62bfe | ||
|
|
aac9369f3f | ||
|
|
1581604f71 | ||
|
|
56161c0a88 | ||
|
|
242df83785 | ||
|
|
2338ad8223 | ||
|
|
b4f4e080af | ||
|
|
132545e057 | ||
|
|
68590dfdb8 | ||
|
|
2e271d695b | ||
|
|
cd745d35c2 | ||
|
|
6928e2aca2 | ||
|
|
49e86614c5 | ||
|
|
51dcf65fd7 | ||
|
|
c6c0165214 | ||
|
|
342935de49 | ||
|
|
ced5bb4631 | ||
|
|
0a832cee9c | ||
|
|
1cfce8b72c | ||
|
|
cdcde26dfc | ||
|
|
1ffb79f852 | ||
|
|
644aaf0c0e | ||
|
|
cdea03792d | ||
|
|
b190e7417e | ||
|
|
7dd3ff7c8d | ||
|
|
21a253103e | ||
|
|
db3fb29b30 | ||
|
|
c5d2ae5831 | ||
|
|
9d0ca00f89 | ||
|
|
79e5d2c9d7 | ||
|
|
3a62d392b4 | ||
|
|
f2858ea48c | ||
|
|
3c78fb420b | ||
|
|
2d22ee7ccc | ||
|
|
9a243a1454 | ||
|
|
165e40c0db | ||
|
|
7f1c2b475b | ||
|
|
a5f38fa6e6 | ||
|
|
598968bf74 | ||
|
|
ee661b0a9f | ||
|
|
7d7b3413bc | ||
|
|
7400008384 | ||
|
|
4e564e8ce4 | ||
|
|
8691fdc533 | ||
|
|
7d103f1d32 | ||
|
|
3de1a265ea | ||
|
|
3b6d4d9df6 | ||
|
|
e96e5374b4 | ||
|
|
5c91709ac8 | ||
|
|
96a91b9d0e | ||
|
|
65b88c09fb | ||
|
|
80ff07f53d | ||
|
|
4f6485d1f9 | ||
|
|
d1c903f36f | ||
|
|
516ebcfa2b | ||
|
|
9414de24d4 | ||
|
|
16990071cb | ||
|
|
52433d253f | ||
|
|
bb6327d969 | ||
|
|
476f441c9b | ||
|
|
c79b5e6179 | ||
|
|
c1bee3fee5 | ||
|
|
d786103d8d | ||
|
|
b3850f6bb7 | ||
|
|
e3db3c0341 | ||
|
|
1ce3bc2870 | ||
|
|
24ac111782 | ||
|
|
07f127ffe4 | ||
|
|
c58f566047 | ||
|
|
f28d6dff32 | ||
|
|
7e49ce9a7b | ||
|
|
59115cd1c7 | ||
|
|
1d93fe4cf9 | ||
|
|
3f1f7640cb | ||
|
|
83ee7c5e92 | ||
|
|
b95ddf18ba | ||
|
|
61d255726c | ||
|
|
05e2851c67 | ||
|
|
f20a174038 | ||
|
|
ec108e7c02 | ||
|
|
ad114295e7 | ||
|
|
0ff7259bc3 | ||
|
|
49af94334e | ||
|
|
22c318fda3 | ||
|
|
da5fd7699e | ||
|
|
3d498a74ba | ||
|
|
460b4bb3f2 | ||
|
|
47723cee33 | ||
|
|
a1b03be27e | ||
|
|
012d99c05c | ||
|
|
2c8a425f87 | ||
|
|
8555f8c28c | ||
|
|
46413c2c52 | ||
|
|
eaeaf9dd69 | ||
|
|
ee11d7da63 | ||
|
|
0ac93aacd5 | ||
|
|
691d8af26d | ||
|
|
87c66b2423 | ||
|
|
005123a371 | ||
|
|
bb8ce43cc0 | ||
|
|
13611e34d2 | ||
|
|
b8190c44a8 | ||
|
|
95d5ec5f0e | ||
|
|
874204bf18 | ||
|
|
6c296a9a17 | ||
|
|
ad06b0e723 | ||
|
|
0652dd344b | ||
|
|
8b3c3e89e9 | ||
|
|
c82ac5ae68 | ||
|
|
2776db0cf9 | ||
|
|
ba19179e4f | ||
|
|
e347ffa336 | ||
|
|
c85af62401 | ||
|
|
39b64b7570 | ||
|
|
56b220b92e | ||
|
|
972fbecc94 | ||
|
|
136e781c7f | ||
|
|
0852d1be9f | ||
|
|
fb722c79be | ||
|
|
c63d069f69 | ||
|
|
1cd7e41f33 | ||
|
|
d6d6d14977 | ||
|
|
82cda4b57a | ||
|
|
301c800319 | ||
|
|
30af83aa6a | ||
|
|
0f9501f93a | ||
|
|
466b2b82d0 | ||
|
|
8ab39512d9 | ||
|
|
e08e95c04e | ||
|
|
e7b1fa5ab5 | ||
|
|
14a2f98418 | ||
|
|
de7d08ee75 | ||
|
|
2a486cad66 | ||
|
|
80fc639480 | ||
|
|
c7608aeb17 | ||
|
|
050300040c | ||
|
|
94c3a2fedd | ||
|
|
421043d923 | ||
|
|
1c22b212c2 | ||
|
|
d52b3eaf21 | ||
|
|
30ca226e39 | ||
|
|
15037013e7 | ||
|
|
30fa9303e8 | ||
|
|
b4753a02de | ||
|
|
628073cbe1 | ||
|
|
2755592175 | ||
|
|
6051ccb23c | ||
|
|
46f9cae0ef | ||
|
|
16614168a7 | ||
|
|
5a15939f08 | ||
|
|
bb5aab16c9 | ||
|
|
b38ac1d025 | ||
|
|
22cf8e940c | ||
|
|
b0ab6bd7e2 | ||
|
|
7b9d85c1b5 | ||
|
|
3637804929 | ||
|
|
c70a44e0fe | ||
|
|
cf18b601e2 | ||
|
|
139cc07bda | ||
|
|
619c3f28f7 | ||
|
|
9966668307 | ||
|
|
77a0a67029 | ||
|
|
b65466cebd | ||
|
|
f1873e32d6 | ||
|
|
b7b4e65d2d | ||
|
|
16bb334fba | ||
|
|
3f1d416526 | ||
|
|
722aaa225b | ||
|
|
7e2e37ab3f | ||
|
|
83411d0fa4 | ||
|
|
d608dd953b | ||
|
|
e5e0a024f9 | ||
|
|
8e6db0829c | ||
|
|
5359fec195 | ||
|
|
f2daf0be9a | ||
|
|
b633a33137 | ||
|
|
d13bf37845 | ||
|
|
4b831a634a | ||
|
|
f91e0a6546 | ||
|
|
2e2d669fdf | ||
|
|
7ecdf7a7be | ||
|
|
9b940de854 | ||
|
|
82807ffe69 | ||
|
|
b1066ad58f | ||
|
|
ac8b1fc108 | ||
|
|
820be2a0ae | ||
|
|
9869f95bd6 | ||
|
|
c87286d3c6 | ||
|
|
7028ca9df3 | ||
|
|
435fc141ae | ||
|
|
889fa63aff | ||
|
|
dae75521d3 | ||
|
|
b9d440f2f7 | ||
|
|
ec4c325efd | ||
|
|
b350ac38dc | ||
|
|
e88c82e7e0 | ||
|
|
99ad01ae0d | ||
|
|
8f8c52d8c4 | ||
|
|
c56fa5a320 | ||
|
|
7d5bae5a50 | ||
|
|
7c404082f8 | ||
|
|
dc8e61cbe5 | ||
|
|
f2ebf52f6e | ||
|
|
fa223e22ed | ||
|
|
6d22c8faa5 | ||
|
|
01ebf2835b | ||
|
|
3ef693a259 | ||
|
|
3a4d932d2b | ||
|
|
571332ae18 | ||
|
|
0607f5552a | ||
|
|
016a6d3aa6 | ||
|
|
c7d98f88e8 | ||
|
|
b82756087a | ||
|
|
3b62572c89 | ||
|
|
abcf179042 | ||
|
|
3dba09d19d | ||
|
|
e07408161a | ||
|
|
937d2fe0f6 | ||
|
|
bcc50557a9 | ||
|
|
bdcb69ad37 | ||
|
|
3b50ed8192 | ||
|
|
30c3a4c7c1 | ||
|
|
6470bc1cda | ||
|
|
221b636f3f | ||
|
|
f7e41063bf | ||
|
|
d6cd959a2b | ||
|
|
57f078925e | ||
|
|
8b4e12da81 | ||
|
|
5ed8be9998 | ||
|
|
4357fe1ba9 | ||
|
|
a64db76b4d | ||
|
|
c464f5e7dc | ||
|
|
00bc3c86b1 | ||
|
|
ba605495ac | ||
|
|
748847d5bf |
396
.bandit.yml
Normal file
396
.bandit.yml
Normal file
@@ -0,0 +1,396 @@
|
||||
|
||||
### Bandit config file generated
|
||||
|
||||
### This config may optionally select a subset of tests to run or skip by
|
||||
### filling out the 'tests' and 'skips' lists given below. If no tests are
|
||||
### specified for inclusion then it is assumed all tests are desired. The skips
|
||||
### set will remove specific tests from the include set. This can be controlled
|
||||
### using the -t/-s CLI options. Note that the same test ID should not appear
|
||||
### in both 'tests' and 'skips', this would be nonsensical and is detected by
|
||||
### Bandit at runtime.
|
||||
|
||||
# Available tests:
|
||||
# B101 : assert_used
|
||||
# B102 : exec_used
|
||||
# B103 : set_bad_file_permissions
|
||||
# B104 : hardcoded_bind_all_interfaces
|
||||
# B105 : hardcoded_password_string
|
||||
# B106 : hardcoded_password_funcarg
|
||||
# B107 : hardcoded_password_default
|
||||
# B108 : hardcoded_tmp_directory
|
||||
# B110 : try_except_pass
|
||||
# B112 : try_except_continue
|
||||
# B201 : flask_debug_true
|
||||
# B301 : pickle
|
||||
# B302 : marshal
|
||||
# B303 : md5
|
||||
# B304 : ciphers
|
||||
# B305 : cipher_modes
|
||||
# B306 : mktemp_q
|
||||
# B307 : eval
|
||||
# B308 : mark_safe
|
||||
# B309 : httpsconnection
|
||||
# B310 : urllib_urlopen
|
||||
# B311 : random
|
||||
# B312 : telnetlib
|
||||
# B313 : xml_bad_cElementTree
|
||||
# B314 : xml_bad_ElementTree
|
||||
# B315 : xml_bad_expatreader
|
||||
# B316 : xml_bad_expatbuilder
|
||||
# B317 : xml_bad_sax
|
||||
# B318 : xml_bad_minidom
|
||||
# B319 : xml_bad_pulldom
|
||||
# B320 : xml_bad_etree
|
||||
# B321 : ftplib
|
||||
# B322 : input
|
||||
# B323 : unverified_context
|
||||
# B324 : hashlib_new_insecure_functions
|
||||
# B325 : tempnam
|
||||
# B401 : import_telnetlib
|
||||
# B402 : import_ftplib
|
||||
# B403 : import_pickle
|
||||
# B404 : import_subprocess
|
||||
# B405 : import_xml_etree
|
||||
# B406 : import_xml_sax
|
||||
# B407 : import_xml_expat
|
||||
# B408 : import_xml_minidom
|
||||
# B409 : import_xml_pulldom
|
||||
# B410 : import_lxml
|
||||
# B411 : import_xmlrpclib
|
||||
# B412 : import_httpoxy
|
||||
# B413 : import_pycrypto
|
||||
# B501 : request_with_no_cert_validation
|
||||
# B502 : ssl_with_bad_version
|
||||
# B503 : ssl_with_bad_defaults
|
||||
# B504 : ssl_with_no_version
|
||||
# B505 : weak_cryptographic_key
|
||||
# B506 : yaml_load
|
||||
# B507 : ssh_no_host_key_verification
|
||||
# B601 : paramiko_calls
|
||||
# B602 : subprocess_popen_with_shell_equals_true
|
||||
# B603 : subprocess_without_shell_equals_true
|
||||
# B604 : any_other_function_with_shell_equals_true
|
||||
# B605 : start_process_with_a_shell
|
||||
# B606 : start_process_with_no_shell
|
||||
# B607 : start_process_with_partial_path
|
||||
# B608 : hardcoded_sql_expressions
|
||||
# B609 : linux_commands_wildcard_injection
|
||||
# B610 : django_extra_used
|
||||
# B611 : django_rawsql_used
|
||||
# B701 : jinja2_autoescape_false
|
||||
# B702 : use_of_mako_templates
|
||||
# B703 : django_mark_safe
|
||||
|
||||
# (optional) list included test IDs here, eg '[B101, B406]':
|
||||
tests:
|
||||
|
||||
# (optional) list skipped test IDs here, eg '[B101, B406]':
|
||||
skips: ['B322']
|
||||
|
||||
### (optional) plugin settings - some test plugins require configuration data
|
||||
### that may be given here, per-plugin. All bandit test plugins have a built in
|
||||
### set of sensible defaults and these will be used if no configuration is
|
||||
### provided. It is not necessary to provide settings for every (or any) plugin
|
||||
### if the defaults are acceptable.
|
||||
|
||||
any_other_function_with_shell_equals_true:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
hardcoded_tmp_directory:
|
||||
tmp_dirs:
|
||||
- /tmp
|
||||
- /var/tmp
|
||||
- /dev/shm
|
||||
linux_commands_wildcard_injection:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
ssl_with_bad_defaults:
|
||||
bad_protocol_versions:
|
||||
- PROTOCOL_SSLv2
|
||||
- SSLv2_METHOD
|
||||
- SSLv23_METHOD
|
||||
- PROTOCOL_SSLv3
|
||||
- PROTOCOL_TLSv1
|
||||
- SSLv3_METHOD
|
||||
- TLSv1_METHOD
|
||||
ssl_with_bad_version:
|
||||
bad_protocol_versions:
|
||||
- PROTOCOL_SSLv2
|
||||
- SSLv2_METHOD
|
||||
- SSLv23_METHOD
|
||||
- PROTOCOL_SSLv3
|
||||
- PROTOCOL_TLSv1
|
||||
- SSLv3_METHOD
|
||||
- TLSv1_METHOD
|
||||
start_process_with_a_shell:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
start_process_with_no_shell:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
start_process_with_partial_path:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
subprocess_popen_with_shell_equals_true:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
subprocess_without_shell_equals_true:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
- subprocess.check_call
|
||||
- subprocess.check_output
|
||||
- subprocess.run
|
||||
try_except_continue:
|
||||
check_typed_exception: false
|
||||
try_except_pass:
|
||||
check_typed_exception: false
|
||||
weak_cryptographic_key:
|
||||
weak_key_size_dsa_high: 1024
|
||||
weak_key_size_dsa_medium: 2048
|
||||
weak_key_size_ec_high: 160
|
||||
weak_key_size_ec_medium: 224
|
||||
weak_key_size_rsa_high: 1024
|
||||
weak_key_size_rsa_medium: 2048
|
||||
|
||||
52
.codeclimate.yml
Normal file
52
.codeclimate.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
version: "2" # required to adjust maintainability checks
|
||||
checks:
|
||||
argument-count:
|
||||
config:
|
||||
threshold: 8 # work on this later
|
||||
complex-logic:
|
||||
enabled: false # Disabled in favor of using Radon for this
|
||||
config:
|
||||
threshold: 4
|
||||
file-lines:
|
||||
enabled: false # enable after audio stuff...
|
||||
config:
|
||||
threshold: 2000 # I would set this lower if not for cogs as command containers.
|
||||
method-complexity:
|
||||
enabled: false # Disabled in favor of using Radon for this
|
||||
config:
|
||||
threshold: 5
|
||||
method-count:
|
||||
enabled: false # I would set this lower if not for cogs as command containers.
|
||||
config:
|
||||
threshold: 20
|
||||
method-lines:
|
||||
enabled: false
|
||||
config:
|
||||
threshold: 25 # I'm fine with long methods, cautious about the complexity of a single method.
|
||||
nested-control-flow:
|
||||
config:
|
||||
threshold: 6
|
||||
return-statements:
|
||||
config:
|
||||
threshold: 6
|
||||
similar-code:
|
||||
enabled: false
|
||||
config:
|
||||
threshold: # language-specific defaults. an override will affect all languages.
|
||||
identical-code:
|
||||
enabled: false
|
||||
config:
|
||||
threshold: # language-specific defaults. an override will affect all languages.
|
||||
plugins:
|
||||
bandit:
|
||||
enabled: false
|
||||
radon:
|
||||
enabled: false
|
||||
config:
|
||||
threshold: "D"
|
||||
duplication:
|
||||
enabled: false
|
||||
config:
|
||||
languages:
|
||||
python:
|
||||
python_version: 3
|
||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -9,16 +9,15 @@ redbot/core/config.py @tekulvw
|
||||
redbot/core/cog_manager.py @tekulvw
|
||||
redbot/core/core_commands.py @tekulvw
|
||||
redbot/core/context.py @Tobotimus
|
||||
redbot/core/commands/* @mikeshardmind
|
||||
redbot/core/data_manager.py @tekulvw
|
||||
redbot/core/dev_commands.py @tekulvw
|
||||
redbot/core/drivers/* @tekulvw
|
||||
redbot/core/events.py @tekulvw
|
||||
redbot/core/global_checks.py @tekulvw
|
||||
redbot/core/i18n.py @tekulvw
|
||||
redbot/core/json_io.py @tekulvw
|
||||
redbot/core/modlog.py @palmtree5
|
||||
redbot/core/rpc.py @tekulvw
|
||||
redbot/core/sentry_setup.py @Kowlin @tekulvw
|
||||
redbot/core/utils/chat_formatting.py @tekulvw
|
||||
redbot/core/utils/mod.py @palmtree5
|
||||
redbot/core/utils/data_converter.py @mikeshardmind
|
||||
@@ -26,15 +25,16 @@ redbot/core/utils/antispam.py @mikeshardmind
|
||||
redbot/core/utils/tunnel.py @mikeshardmind
|
||||
redbot/core/utils/caching.py @mikeshardmind
|
||||
redbot/core/utils/common_filters.py @mikeshardmind
|
||||
redbot/core/utils/dbtools.py @mikeshardmind
|
||||
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
redbot/cogs/alias/* @tekulvw
|
||||
redbot/cogs/audio/* @aikaterna @atiwiex
|
||||
redbot/cogs/audio/* @aikaterna @Drapersniper
|
||||
redbot/cogs/bank/* @tekulvw
|
||||
redbot/cogs/cleanup/* @palmtree5
|
||||
redbot/cogs/customcom/* @palmtree5
|
||||
redbot/cogs/downloader/* @tekulvw
|
||||
redbot/cogs/downloader/* @tekulvw @jack1142
|
||||
redbot/cogs/economy/* @palmtree5
|
||||
redbot/cogs/filter/* @palmtree5
|
||||
redbot/cogs/general/* @palmtree5
|
||||
@@ -43,7 +43,6 @@ redbot/cogs/mod/* @palmtree5
|
||||
redbot/cogs/modlog/* @palmtree5
|
||||
redbot/cogs/streams/* @Twentysix26 @palmtree5
|
||||
redbot/cogs/trivia/* @Tobotimus
|
||||
redbot/cogs/dataconverter/* @mikeshardmind
|
||||
redbot/cogs/reports/* @mikeshardmind
|
||||
redbot/cogs/permissions/* @mikeshardmind
|
||||
redbot/cogs/warnings/* @palmtree5
|
||||
@@ -51,12 +50,16 @@ redbot/cogs/warnings/* @palmtree5
|
||||
# Docs
|
||||
docs/* @tekulvw @palmtree5
|
||||
|
||||
# Tests
|
||||
tests/cogs/downloader/* @jack1142
|
||||
|
||||
# Setup, instance setup, and running the bot
|
||||
setup.py @tekulvw
|
||||
redbot/__init__.py @tekulvw
|
||||
redbot/__main__.py @tekulvw
|
||||
redbot/__main__.py @tekulvw @mikeshardmind
|
||||
redbot/setup.py @tekulvw
|
||||
|
||||
# Others
|
||||
.travis.yml @Kowlin
|
||||
crowdin.yml @Kowlin
|
||||
.github/workflows/* @Kowlin
|
||||
|
||||
81
.github/CONTRIBUTING.md
vendored
81
.github/CONTRIBUTING.md
vendored
@@ -11,8 +11,9 @@
|
||||
* [4.4 Make](#44-make)
|
||||
* [4.5 Keeping your dependencies up to date](#45-keeping-your-dependencies-up-to-date)
|
||||
* [4.6 To contribute changes](#46-to-contribute-changes)
|
||||
* [4.7 How To Report A Bug](#47-how-to-report-a-bug)
|
||||
* [4.8 How To Suggest A Feature Or Enhancement](#48-how-to-suggest-a-feature-or-enhancement)
|
||||
* [4.7 Using towncrier](#47-using-towncrier)
|
||||
* [4.8 How To Report A Bug](#48-how-to-report-a-bug)
|
||||
* [4.9 How To Suggest A Feature Or Enhancement](#49-how-to-suggest-a-feature-or-enhancement)
|
||||
* [5. Code Review Process](#5-code-review-process)
|
||||
* [5.1 Issues](#51-issues)
|
||||
* [5.2 Pull Requests](#52-pull-requests)
|
||||
@@ -29,9 +30,8 @@ Red is an open source project. This means that each and every one of the develop
|
||||
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
|
||||
|
||||
# 2. Ground Rules
|
||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.8.1 and above.
|
||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||
@@ -53,33 +53,36 @@ Red's repository is configured to follow a particular development workflow, usin
|
||||
|
||||
### 4.1 Setting up your development environment
|
||||
The following requirements must be installed prior to setting up:
|
||||
- Python 3.6.2 or greater (3.6.6 or greater on Windows)
|
||||
- Python 3.8.1 or greater
|
||||
- git
|
||||
- pip
|
||||
- pipenv
|
||||
|
||||
If you're not on Windows, you can optionally install [pyenv](https://github.com/pyenv/pyenv), which will help you run tests for different python versions.
|
||||
If you're not on Windows, you should also have GNU make installed, and you can optionally install [pyenv](https://github.com/pyenv/pyenv), which can help you run tests for different python versions.
|
||||
|
||||
1. Fork and clone the repository to a directory on your local machine.
|
||||
2. Open a command line in that directory and execute the following commands:
|
||||
2. Open a command line in that directory and execute the following command:
|
||||
```bash
|
||||
pip install pipenv
|
||||
pipenv install --dev
|
||||
make newenv
|
||||
```
|
||||
Red, its dependencies, and all required development tools, are now installed to a virtual environment. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
||||
3. Activate the new virtual environment with the command:
|
||||
```bash
|
||||
pipenv shell
|
||||
```
|
||||
From here onwards, we will assume you are executing commands from within this shell. Each time you open a new command line, you should execute this command first.
|
||||
Red, its dependencies, and all required development tools, are now installed to a virtual environment located in the `.venv` subdirectory. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
||||
3. Activate the new virtual environment with one of the following commands:
|
||||
- Posix:
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
- Windows:
|
||||
```powershell
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
Each time you open a new command line, you should execute this command first. From here onwards, we will assume you are executing commands from within this activated virtual environment.
|
||||
|
||||
Note: If you haven't used `pipenv` before but are comfortable with virtualenvs, just run `pip install pipenv` in the virtualenv you're already using and invoke the command above from the cloned Red repo. It will do the correct thing.
|
||||
**Note:** If you're comfortable with setting up virtual environments yourself and would rather do it manually, just run `pip install -Ur tools/dev-requirements.txt` after setting it up.
|
||||
|
||||
### 4.2 Testing
|
||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||
|
||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 and 3.7 (test environments `py36` and `py37`)
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.8 (test environment `py38`)
|
||||
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||
|
||||
@@ -95,25 +98,57 @@ Our style checker of choice, [black](https://github.com/ambv/black), actually ha
|
||||
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 -N <src>`.
|
||||
|
||||
### 4.4 Make
|
||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do a few things with them:
|
||||
1. `make reformat`: Reformat all python files in the project with Black
|
||||
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
|
||||
3. `make newenv`: Set up a new virtual environment in the `.venv` subdirectory, and install Red and its dependencies. If one already exists, it is cleared out and replaced.
|
||||
4. `make syncenv`: Sync your environment with Red's latest dependencies.
|
||||
|
||||
The other make recipes are most likely for project maintainers rather than contributors.
|
||||
|
||||
You can specify the Python executable used in the make recipes with the `PYTHON` environment variable, e.g. `make PYTHON=/usr/bin/python3.8 newenv`.
|
||||
|
||||
### 4.5 Keeping your dependencies up to date
|
||||
Whenever you pull from upstream (V3/develop on the main repository) and you notice the file `Pipfile.lock` has been changed, it usually means one of the package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `pipenv install --dev` again.
|
||||
Whenever you pull from upstream (V3/develop on the main repository) and you notice either of the files `setup.cfg` or `tools/dev-requirements.txt` have been changed, it can often mean some package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `make syncenv`. You could also simply do `make newenv` to install them to a clean new virtual environment.
|
||||
|
||||
### 4.6 To contribute changes
|
||||
|
||||
1. Create a new branch on your fork
|
||||
2. Make the changes
|
||||
3. If you like the changes and think the main Red project could use it:
|
||||
* Create a towncrier entry for the changes. (See next section for details)
|
||||
* Run tests with `tox` to ensure your code is up to scratch
|
||||
* Create a Pull Request on GitHub with your changes
|
||||
|
||||
### 4.7 How To Report A Bug
|
||||
### 4.7 Using towncrier
|
||||
|
||||
Red uses towncrier to create changelogs.
|
||||
|
||||
To create a towncrier entry for your PR, create a file in `changelog.d` for it. If the changes are for a specific cog, place the file in the related subdirectory.
|
||||
|
||||
The filename should be of the format `issuenumber.changetype(.count).rst`, where `(.count)` is an optional
|
||||
part of the filename should multiple entries for the same issue number and type be necessary.
|
||||
If there is not an issue associated with your PR,
|
||||
you may use the PR number in place of the issue number.
|
||||
|
||||
Valid changetypes are:
|
||||
|
||||
* breaking : Breaking changes
|
||||
* dep : Changes to dependencies
|
||||
* enhance : Enhancements
|
||||
* feature : New features
|
||||
* bugfix : Bugfixes
|
||||
* docs : documentation improvements and additions
|
||||
* removal : removal of something
|
||||
* misc : any changes which don't have a user facing change, and don't belong in the changelog for users
|
||||
|
||||
The contents of the file should be a short, human readable description of the impact of the changes made,
|
||||
not the technical details of the change.
|
||||
|
||||
### 4.8 How To Report A Bug
|
||||
Please see our **ISSUES.MD** for more information.
|
||||
|
||||
### 4.8 How To Suggest A Feature Or Enhancement
|
||||
### 4.9 How To Suggest A Feature Or Enhancement
|
||||
The goal of Red is to be as useful to as many people as possible, this means that all features must be useful to anyone and any server that uses Red.
|
||||
|
||||
If you find yourself wanting a feature that Red does not already have, you're probably not alone. There's bound to be a great number of users out there needing the same thing and a lot of the features that Red has today have been added because of the needs of our users. Open an issue on our issues list and describe the feature you would like to see, how you would use it, how it should work, and why it would be useful to the Red community as a whole.
|
||||
@@ -132,7 +167,7 @@ Pull requests are evaluated by their quality and how effectively they solve thei
|
||||
|
||||
1. A pull request is submitted
|
||||
2. Core team members will review and test the pull request (usually within a week)
|
||||
3. After a majority of the core team approves your pull request:
|
||||
3. After a member of the core team approves your pull request:
|
||||
* If your pull request is considered an improvement or enhancement the project owner will have 1 day to veto or approve your pull request.
|
||||
* If your pull request is considered a new feature the project owner will have 1 week to veto or approve your pull request.
|
||||
4. If any feedback is given we expect a response within 1 week or we may decide to close the PR.
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: Red_Devs
|
||||
5
.github/ISSUE_TEMPLATE.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<!--
|
||||
Please be sure to use the correct template,
|
||||
if your report doesn't have the correct template please open an issue describing your issue in detail
|
||||
For support regarding the bot itself please visit the discord server over at https://discord.gg/red
|
||||
-->
|
||||
9
.github/ISSUE_TEMPLATE/command_bug.md
vendored
9
.github/ISSUE_TEMPLATE/command_bug.md
vendored
@@ -1,3 +1,12 @@
|
||||
---
|
||||
name: Bug reports for commands
|
||||
about: For bugs that involve commands found within Red
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Command bugs
|
||||
|
||||
<!--
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/feature_req.md
vendored
9
.github/ISSUE_TEMPLATE/feature_req.md
vendored
@@ -1,3 +1,12 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: For feature requests regarding Red itself.
|
||||
title: ''
|
||||
labels: 'Type: Feature'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Feature request
|
||||
|
||||
<!-- This template is for feature requests. Please fill out the following: -->
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/other_bug.md
vendored
11
.github/ISSUE_TEMPLATE/other_bug.md
vendored
@@ -1,3 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: For bugs that don't involve a command.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Other bugs
|
||||
|
||||
<!--
|
||||
@@ -18,4 +27,4 @@ Did you find a bug with something other than a command? Fill out the following:
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
### Type
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Enhancement
|
||||
- [ ] New feature
|
||||
|
||||
### Description of the changes
|
||||
1
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
1
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
@@ -1,6 +1,7 @@
|
||||
# Bugfix request
|
||||
|
||||
<!--
|
||||
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||
To be used for pull requests that fix a bug
|
||||
-->
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
3
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
@@ -1,6 +1,7 @@
|
||||
# Enhancement request
|
||||
|
||||
<!--
|
||||
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||
To be used for PRs which enhance existing features
|
||||
-->
|
||||
|
||||
@@ -17,4 +18,4 @@ If adding commands, describe any restrictions on their usage.
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
- [ ] No
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
3
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
@@ -1,6 +1,7 @@
|
||||
# New feature addition
|
||||
|
||||
<!--
|
||||
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||
To be used for PRs which add a new feature
|
||||
Examples of this include new APIs, new core cogs, etc.
|
||||
-->
|
||||
@@ -18,4 +19,4 @@ Examples of this include new APIs, new core cogs, etc.
|
||||
<!--
|
||||
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||
-->
|
||||
-->
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
@@ -1,6 +1,7 @@
|
||||
# New release
|
||||
|
||||
<!--
|
||||
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||
To be used by collaborators for doing releases.
|
||||
Most contributors will not need to use this.
|
||||
-->
|
||||
@@ -13,4 +14,3 @@ Most contributors will not need to use this.
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Translations update
|
||||
|
||||
<!--
|
||||
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||
Used for PRs updating translations from Crowdin
|
||||
-->
|
||||
-->
|
||||
|
||||
26
.github/workflows/auto_labeler.yml
vendored
Normal file
26
.github/workflows/auto_labeler.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Auto Labeler
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply Triage Label
|
||||
uses: actions/github-script@0.4.0
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const is_status_label = (label) => label.name.startsWith('Status: ');
|
||||
if (context.payload.issue.labels.some(is_status_label)) {
|
||||
console.log('Issue already has Status label, skipping...');
|
||||
return;
|
||||
}
|
||||
github.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Status: Needs Triage']
|
||||
});
|
||||
16
.github/workflows/lint_python.yaml
vendored
Normal file
16
.github/workflows/lint_python.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Lint Python
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint_python:
|
||||
name: Lint Python
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python_version: "3.8"
|
||||
- run: "python -m pip install flake8"
|
||||
name: Install Flake8
|
||||
- run: "python -m flake8 . --count --select=E9,F7,F82 --show-source"
|
||||
name: Flake8 Linting
|
||||
28
.github/workflows/publish_crowdin.yml
vendored
Normal file
28
.github/workflows/publish_crowdin.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Publish to Crowdin
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||
echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y crowdin
|
||||
pip install redgettext==3.1
|
||||
- name: Publish
|
||||
env:
|
||||
CROWDIN_API_KEY: ${{ secrets.crowdin_token}}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.crowdin_identifier }}
|
||||
run: |
|
||||
make upload_translations
|
||||
26
.github/workflows/publish_pypi.yml
vendored
Normal file
26
.github/workflows/publish_pypi.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Publish to PyPI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.pypi_token }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
73
.github/workflows/tests.yml
vendored
Normal file
73
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
tox:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python_version:
|
||||
- "3.8"
|
||||
tox_env:
|
||||
- py
|
||||
- style
|
||||
- docs
|
||||
include:
|
||||
- tox_env: py
|
||||
friendly_name: Tests
|
||||
- tox_env: style
|
||||
friendly_name: Style
|
||||
- tox_env: docs
|
||||
friendly_name: Docs
|
||||
fail-fast: false
|
||||
name: Tox - ${{ matrix.friendly_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- name: Tox test
|
||||
env:
|
||||
TOXENV: ${{ matrix.tox_env }}
|
||||
run: tox
|
||||
|
||||
tox-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python_version:
|
||||
- "3.8"
|
||||
fail-fast: false
|
||||
name: Tox - Postgres
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:10
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_DB: red_db
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- name: Tox test
|
||||
env:
|
||||
TOXENV: postgres
|
||||
PGDATABASE: red_db
|
||||
PGUSER: postgres
|
||||
PGPASSWORD: postgres
|
||||
PGPORT: 5432
|
||||
run: tox
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
*.pot
|
||||
.data
|
||||
!/tests/cogs/dataconverter/data/**/*.json
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
@@ -55,6 +57,7 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
@@ -134,3 +137,6 @@ ENV/
|
||||
|
||||
# pytest
|
||||
.pytest_cache/
|
||||
|
||||
# Pre-commit hooks
|
||||
/.pre-commit-config.yaml
|
||||
|
||||
148
.pylintrc
Normal file
148
.pylintrc
Normal file
@@ -0,0 +1,148 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=pytest
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=no
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# DO NOT CHANGE THIS VALUE # Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||
# optimizer, which will apply various small optimizations. For instance, it can
|
||||
# be used to obtain the result of joining multiple strings with the addition
|
||||
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
||||
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
||||
# AST will be different than the one from reality.
|
||||
optimize-ast=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time. See also the "--disable" option for examples.
|
||||
|
||||
|
||||
enable=all
|
||||
|
||||
disable=C, # black is enforcing this for us already, incompatibly
|
||||
W, # unbroaden this to the below specifics later on.
|
||||
W0107, # uneccessary pass is stylisitc in most places
|
||||
W0212, # Should likely refactor around protected access warnings later
|
||||
W1203, # fstrings are too fast to care about enforcing this.
|
||||
W0612, # unused vars can sometimes indicate an issue, but ...
|
||||
W1401, # Should probably fix the reason this is disabled (start up screen)
|
||||
W0511, # Nope, todos are fine for future people to see things to do.
|
||||
W0613, # Too many places where we need to take unused args do to d.py ... also menus
|
||||
W0221, # Overriden converters.
|
||||
W0223, # abstractmethod not defined in mixins is expected
|
||||
I, # ...
|
||||
R # While some of these have merit, It's too large a burden to enable this right now.
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
output-format=parseable
|
||||
files-output=no
|
||||
reports=no
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# TODO: Write a plyint plugin to allow this with these mixin classes
|
||||
# To use the abstractmethod we know will be defined in the final class.
|
||||
ignored-classes=redbot.cogs.mod.movetocore.MoveToCore,
|
||||
redbot.cogs.mod.kickban.KickBanMixin,
|
||||
redbot.cogs.mod.mutes.MuteMixin,
|
||||
redbot.cogs.mod.names.ModInfo,
|
||||
redbot.cogs.mod.settings.ModSettings,
|
||||
redbot.cogs.mod.events.Events
|
||||
|
||||
ignored-modules=distutils # https://github.com/PyCQA/pylint/issues/73
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_$|dummy
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,__call__
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception,discord.DiscordException
|
||||
@@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
@@ -5,8 +7,10 @@ build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
pip_install: true
|
||||
extra_requirements:
|
||||
- docs
|
||||
- mongo
|
||||
version: 3.8
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
|
||||
40
.travis.yml
40
.travis.yml
@@ -3,16 +3,12 @@ language: python
|
||||
cache: pip
|
||||
notifications:
|
||||
email: false
|
||||
sudo: true
|
||||
|
||||
python:
|
||||
- 3.6.6
|
||||
- 3.7
|
||||
- 3.8.1
|
||||
env:
|
||||
global:
|
||||
PIPENV_IGNORE_VIRTUALENVS=1
|
||||
matrix:
|
||||
TOXENV=py
|
||||
- PIPENV_IGNORE_VIRTUALENVS=1
|
||||
|
||||
install:
|
||||
- pip install --upgrade pip tox
|
||||
@@ -22,46 +18,48 @@ script:
|
||||
|
||||
jobs:
|
||||
include:
|
||||
|
||||
- python: 3.6.6
|
||||
env: TOXENV=docs
|
||||
- python: 3.6.6
|
||||
env: TOXENV=style
|
||||
|
||||
- env: TOXENV=py
|
||||
- env: TOXENV=docs
|
||||
- env: TOXENV=style
|
||||
- env: TOXENV=postgres
|
||||
services: postgresql
|
||||
addons:
|
||||
postgresql: "10"
|
||||
before_script:
|
||||
- psql -c 'create database red_db;' -U postgres
|
||||
# These jobs only occur on tag creation if the prior ones succeed
|
||||
- stage: PyPi Deployment
|
||||
if: tag IS present
|
||||
python: 3.6.6
|
||||
python: 3.8.1
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
- TOXENV=py36
|
||||
- TOXENV=py38
|
||||
deploy:
|
||||
- provider: pypi
|
||||
distributions: sdist bdist_wheel
|
||||
user: Red-DiscordBot
|
||||
password:
|
||||
secure: Ty9vYnd/wCuQkVC/OsS4E2jT9LVDVfzsFrQc4U2hMYcTJnYbl/3omyObdCWCOBC40vUDkVHAQU8ULHzoCA+2KX9Ds/7/P5zCumAA0uJRR9Smw7OlRzSMxJI+/lGq4CwXKzxDZKuo5rsxXEbW5qmYjtO8Mk6KuLkvieb1vyr2DcqWEFzg/7TZNDfD1oP8et8ITQ26lLP1dtQx/jlAiIBzgK9wziuwj1Divb9A///VsGz43N8maZ+jfsDjYqrfUVWTy3ar7JPUplletenYCR1PmQ5C46XfV0kitKd1aITJ48YPAKyYgKy8AIT+Uz1JArTnqdzLSFRNELS57qS00lzgllbteCyWQ8Uzy0Zpxb/5DDH8/mL1n0MyJrF8qjZd2hLNAXg3z/k9bGXeiMLGwoxRlGXkL2XpiVgI93UKKyVyooGNMgPTc/QdSc7krjAWcOtX/HgLR34jxeLPFEdzJNAFIimfDD8N+XTFcNBw6EvOYm/n5MXkckNoX/G+ThNobHZ7VKSASltZ9zBRAJ2dDh35G3CYmVEk33U77RKbL9le/Za9QVBcAO8i6rqVGYkdO7thHHKHc/1CB1jNnjsFSDt0bURtNfAqfwKCurQC8487zbEzT+2fog3Wygv7g3cklaRg4guY8UjZuFWStYGqbroTsOCd9ATNqeO5B13pNhllSzU=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
python: 3.6.6
|
||||
tags: true
|
||||
- stage: Crowdin Deployment
|
||||
if: tag IS present
|
||||
python: 3.6.6
|
||||
if: tag IS present OR ENV(BUILD_CROWDIN)
|
||||
python: 3.8.1
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
- TOXENV=py36
|
||||
- TOXENV=py38
|
||||
before_deploy:
|
||||
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y crowdin
|
||||
- pip install redgettext==2.2
|
||||
- pip install redgettext==3.1
|
||||
deploy:
|
||||
- provider: script
|
||||
script: make gettext
|
||||
script: make upload_translations
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
python: 3.6.6
|
||||
tags: true
|
||||
|
||||
32
LICENSE
32
LICENSE
@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Red - A fully customizable Discord bot
|
||||
Copyright (C) 2015-2018 Twentysix
|
||||
Copyright (C) 2015-2020 Twentysix
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Red-DiscordBot Copyright (C) 2015-2018 Twentysix
|
||||
Red-DiscordBot Copyright (C) 2015-2020 Twentysix
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
@@ -672,3 +672,31 @@ may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
The Red-DiscordBot project contains subcomponents in audio.py that have a
|
||||
separate copyright notice and license terms. Your use of the source code for
|
||||
these subcomponents is subject to the terms and conditions of the following
|
||||
licenses.
|
||||
|
||||
This product bundles methods from https://github.com/Just-Some-Bots/MusicBot/
|
||||
blob/master/musicbot/spotify.py which are available under an MIT license.
|
||||
|
||||
Copyright (c) 2015-2018 Just-Some-Bots (https://github.com/Just-Some-Bots)
|
||||
|
||||
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.
|
||||
|
||||
44
Makefile
44
Makefile
@@ -1,13 +1,33 @@
|
||||
reformat:
|
||||
black -l 99 -N `git ls-files "*.py"`
|
||||
stylecheck:
|
||||
black --check -l 99 -N `git ls-files "*.py"`
|
||||
gettext:
|
||||
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||
crowdin upload
|
||||
PYTHON ?= python3.8
|
||||
|
||||
REF?=rewrite
|
||||
update_vendor:
|
||||
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/$(REF).tar.gz#egg=discord.py
|
||||
rm -r discord.py*.egg-info
|
||||
$(MAKE) reformat
|
||||
# Python Code Style
|
||||
reformat:
|
||||
$(PYTHON) -m black -l 99 --target-version py38 `git ls-files "*.py"`
|
||||
stylecheck:
|
||||
$(PYTHON) -m black --check -l 99 --target-version py38 `git ls-files "*.py"`
|
||||
|
||||
# Translations
|
||||
gettext:
|
||||
$(PYTHON) -m redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||
upload_translations:
|
||||
$(MAKE) gettext
|
||||
crowdin upload sources
|
||||
download_translations:
|
||||
crowdin download
|
||||
|
||||
# Dependencies
|
||||
bumpdeps:
|
||||
$(PYTHON) tools/bumpdeps.py
|
||||
|
||||
# Development environment
|
||||
newenv:
|
||||
$(PYTHON) -m venv --clear .venv
|
||||
.venv/bin/pip install -U pip setuptools
|
||||
$(MAKE) syncenv
|
||||
syncenv:
|
||||
.venv/bin/pip install -Ur ./tools/dev-requirements.txt
|
||||
|
||||
# Changelog check
|
||||
checkchangelog:
|
||||
bash tools/check_changelog_entries.sh
|
||||
$(PYTHON) -m towncrier --draft
|
||||
|
||||
11
Pipfile
11
Pipfile
@@ -1,11 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
"e1839a8" = { path = ".", editable = true, extras = ['mongo', 'voice'] }
|
||||
|
||||
[dev-packages]
|
||||
tox = "*"
|
||||
"e1839a9" = { path = ".", editable = true, extras = ['docs', 'test', 'style'] }
|
||||
771
Pipfile.lock
generated
771
Pipfile.lock
generated
@@ -1,771 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "57184ef83392116db24a1966022ad358f54048bb43d428d47a6e31f1a88fc434"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||
],
|
||||
"version": "==3.4.4"
|
||||
},
|
||||
"aiohttp-json-rpc": {
|
||||
"hashes": [
|
||||
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||
],
|
||||
"version": "==0.11.2"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
||||
],
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
"extras": [
|
||||
"mongo",
|
||||
"voice"
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||
],
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"idna-ssl": {
|
||||
"hashes": [
|
||||
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"motor": {
|
||||
"hashes": [
|
||||
"sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
|
||||
"sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
],
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"pymongo": {
|
||||
"hashes": [
|
||||
"sha256:025f94fc1e1364f00e50badc88c47f98af20012f23317234e51a11333ef986e6",
|
||||
"sha256:02aa7fb282606331aefbc0586e2cf540e9dbe5e343493295e7f390936ad2738e",
|
||||
"sha256:057210e831573e932702cf332012ed39da78edf0f02d24a3f0b213264a87a397",
|
||||
"sha256:0d946b79c56187fe139276d4c8ed612a27a616966c8b9779d6b79e2053587c8b",
|
||||
"sha256:104790893b928d310aae8a955e0bdbaa442fb0ac0a33d1bbb0741c791a407778",
|
||||
"sha256:15527ef218d95a8717486106553b0d54ff2641e795b65668754e17ab9ca6e381",
|
||||
"sha256:1826527a0b032f6e20e7ac7f72d7c26dd476a5e5aa82c04aa1c7088a59fded7d",
|
||||
"sha256:22e3aa4ce1c3eebc7f70f9ca7fd4ce1ea33e8bdb7b61996806cd312f08f84a3a",
|
||||
"sha256:244e1101e9a48615b9a16cbd194f73c115fdfefc96894803158608115f703b26",
|
||||
"sha256:24b8c04fdb633a84829d03909752c385faef249c06114cc8d8e1700b95aae5c8",
|
||||
"sha256:2c276696350785d3104412cbe3ac70ab1e3a10c408e7b20599ee41403a3ed630",
|
||||
"sha256:2d8474dc833b1182b651b184ace997a7bd83de0f51244de988d3c30e49f07de3",
|
||||
"sha256:3119b57fe1d964781e91a53e81532c85ed1701baaddec592e22f6b77a9fdf3df",
|
||||
"sha256:3bee8e7e0709b0fcdaa498a3e513bde9ffc7cd09dbceb11e425bd91c89dbd5b6",
|
||||
"sha256:436c071e01a464753d30dbfc8768dd93aecf2a8e378e5314d130b95e77b4d612",
|
||||
"sha256:46635e3f19ad04d5a7d7cf23d232388ddbfccf46d9a3b7436b6abadda4e84813",
|
||||
"sha256:4772e0b679717e7ac4608d996f57b6f380748a919b457cb05bb941467b888b22",
|
||||
"sha256:4e2cd80e16f481a62c3175b607373200e714ed29025f21559ebf7524f295689f",
|
||||
"sha256:52732960efa0e003ca1c092dc0a3c65276e897681287a788a01ca78dda3b41f0",
|
||||
"sha256:55a7de51ec7d1731b2431886d0349146645f2816e5b8eb982d7c49f89472c9f3",
|
||||
"sha256:5f8ed5934197a2d4b2087646e98de3e099a237099dcf498b9e38dd3465f74ef4",
|
||||
"sha256:64b064124fcbc8eb04a155117dc4d9a336e3cda3f069958fbc44fe70c3c3d1e9",
|
||||
"sha256:65958b8e4319f992e85dad59d8081888b97fcdbde5f0d14bc28f2848b92d3ef1",
|
||||
"sha256:7683428862e20c6a790c19e64f8ccf487f613fbc83d47e3d532df9c81668d451",
|
||||
"sha256:78566d5570c75a127c2491e343dc006798a384f06be588fe9b0cbe5595711559",
|
||||
"sha256:7d1cb00c093dbf1d0b16ccf123e79dee3b82608e4a2a88947695f0460eef13ff",
|
||||
"sha256:8c74e2a9b594f7962c62cef7680a4cb92a96b4e6e3c2f970790da67cc0213a7e",
|
||||
"sha256:8e60aa7699170f55f4b0f56ee6f8415229777ac7e4b4b1aa41fc61eec08c1f1d",
|
||||
"sha256:9447b561529576d89d3bf973e5241a88cf76e45bd101963f5236888713dea774",
|
||||
"sha256:970055bfeb0be373f2f5299a3db8432444bad3bc2f198753ee6c2a3a781e0959",
|
||||
"sha256:a6344b8542e584e140dc3c651d68bde51270e79490aa9320f9e708f9b2c39bd5",
|
||||
"sha256:ce309ca470d747b02ba6069d286a17b7df8e9c94d10d727d9cf3a64e51d85184",
|
||||
"sha256:cfbd86ed4c2b2ac71bbdbcea6669bf295def7152e3722ddd9dda94ac7981f33d",
|
||||
"sha256:d7929c513732dff093481f4a0954ed5ff16816365842136b17caa0b4992e49d3"
|
||||
],
|
||||
"version": "==3.7.2"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||
],
|
||||
"version": "==3.13"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
||||
],
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"raven-aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
||||
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
||||
],
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"red-lavalink": {
|
||||
"hashes": [
|
||||
"sha256:6a1a34471ccf4630eee537049568dd87e8e93614f1d1ce355dd74e5b10079782"
|
||||
],
|
||||
"version": "==0.1.2"
|
||||
},
|
||||
"schema": {
|
||||
"hashes": [
|
||||
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||
],
|
||||
"version": "==0.6.8"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"version": "==6.0"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||
],
|
||||
"version": "==3.4.4"
|
||||
},
|
||||
"aiohttp-json-rpc": {
|
||||
"hashes": [
|
||||
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||
],
|
||||
"version": "==0.11.2"
|
||||
},
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||
],
|
||||
"version": "==0.7.12"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||
],
|
||||
"version": "==18.9b0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
||||
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
||||
],
|
||||
"version": "==2018.11.29"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"e1839a9": {
|
||||
"editable": true,
|
||||
"extras": [
|
||||
"docs",
|
||||
"test",
|
||||
"style"
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
|
||||
"sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"
|
||||
],
|
||||
"version": "==3.0.10"
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
||||
],
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"idna-ssl": {
|
||||
"hashes": [
|
||||
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
],
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
],
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||
],
|
||||
"version": "==18.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
||||
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
|
||||
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
|
||||
],
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02",
|
||||
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"
|
||||
],
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
|
||||
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
||||
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
||||
],
|
||||
"version": "==2018.9"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||
],
|
||||
"version": "==3.13"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
||||
],
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"raven-aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
||||
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
||||
],
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||
],
|
||||
"version": "==2.21.0"
|
||||
},
|
||||
"schema": {
|
||||
"hashes": [
|
||||
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||
],
|
||||
"version": "==0.6.8"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5",
|
||||
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"
|
||||
],
|
||||
"version": "==1.8.3"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09",
|
||||
"sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"sphinxcontrib-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
"sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e",
|
||||
"sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c",
|
||||
"sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd"
|
||||
],
|
||||
"version": "==16.2.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"version": "==6.0"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
42
README.md
42
README.md
@@ -16,21 +16,21 @@
|
||||
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
||||
</a>
|
||||
<a href="https://www.python.org/downloads/">
|
||||
<img src="https://img.shields.io/badge/Made%20With-Python%203-blue.svg?style=for-the-badge" alt="Made with Python 3">
|
||||
<img src="https://img.shields.io/badge/Made%20With-Python%203.8-blue.svg?style=for-the-badge" alt="Made with Python 3.8">
|
||||
</a>
|
||||
<a href="https://crowdin.com/project/red-discordbot">
|
||||
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
||||
</a>
|
||||
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
|
||||
<a href="https://github.com/Rapptz/discord.py/">
|
||||
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/Cog-Creators/Red-DiscordBot">
|
||||
<img src="https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
|
||||
<a href="https://github.com/Cog-Creators/Red-DiscordBot/actions">
|
||||
<img src="https://github.com/Cog-Creators/Red-DiscordBot/workflows/Tests/badge.svg" alt="GitHub Actions">
|
||||
</a>
|
||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop">
|
||||
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop" alt="Red on readthedocs.org">
|
||||
<a href="http://red-discordbot.readthedocs.io/en/stable/?badge=stable">
|
||||
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=stable" alt="Red on readthedocs.org">
|
||||
</a>
|
||||
<a href="https://github.com/ambv/black">
|
||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
|
||||
@@ -45,7 +45,7 @@
|
||||
•
|
||||
<a href="#installation">Installation</a>
|
||||
•
|
||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
|
||||
<a href="http://red-discordbot.readthedocs.io/en/stable/index.html">Documentation</a>
|
||||
•
|
||||
<a href="#plugins">Plugins</a>
|
||||
•
|
||||
@@ -83,19 +83,12 @@ community of cog repositories.**
|
||||
|
||||
**The following platforms are officially supported:**
|
||||
|
||||
- [Windows](https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html)
|
||||
- [MacOS](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
- [Ubuntu](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
- [Debian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
- [CentOS 7](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
- [Arch Linux](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
- [Raspbian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
||||
|
||||
Already using **Red** V2? Take a look at the [Data Converter](https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html)
|
||||
to import your data to V3.
|
||||
- [Windows](https://red-discordbot.readthedocs.io/en/stable/install_windows.html)
|
||||
- [MacOS](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||
- [Most major linux distributions](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||
|
||||
If after reading the guide you are still experiencing issues, feel free to join the
|
||||
[Official Discord Server](https://discord.gg/red) and ask in the **#v3-support** channel for help.
|
||||
[Official Discord Server](https://discord.gg/red) and ask in the **#support** channel for help.
|
||||
|
||||
# Plugins
|
||||
|
||||
@@ -108,18 +101,18 @@ plugins directly from Discord! A few examples are:
|
||||
- Casino
|
||||
- Reaction roles
|
||||
- Slow Mode
|
||||
- Anilist
|
||||
- AniList
|
||||
- And much, much more!
|
||||
|
||||
Feel free to take a [peek](https://github.com/Cog-Creators/Red-DiscordBot/issues/1398) at a list of
|
||||
Feel free to take a [peek](https://cogboard.red/t/approved-repositories/210) at a list of
|
||||
available 3rd party cogs!
|
||||
|
||||
# Join the community!
|
||||
|
||||
**Red** is in continuous development, and it’s supported by an active community which produces new
|
||||
content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t
|
||||
[find](https://github.com/Cog-Creators/Red-DiscordBot/issues/1398) the cog you’re looking for,
|
||||
consult our [guide](https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html) on
|
||||
[find](https://cogboard.red/t/approved-repositories/210) the cog you’re looking for,
|
||||
consult our [guide](https://red-discordbot.readthedocs.io/en/stable/guide_cog_creation.html) on
|
||||
building your own cogs!
|
||||
|
||||
Join us on our [Official Discord Server](https://discord.gg/red)!
|
||||
@@ -128,11 +121,6 @@ Join us on our [Official Discord Server](https://discord.gg/red)!
|
||||
|
||||
Released under the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
||||
|
||||
This project vendors the
|
||||
[discord.py library by Rapptz](https://github.com/Rapptz/discord.py/tree/rewrite), which is
|
||||
licensed under the [MIT License](https://opensource.org/licenses/MIT). This amounts to everything
|
||||
within the *discord* folder of this repository.
|
||||
|
||||
Red is named after the main character of "Transistor", a video game by
|
||||
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
||||
|
||||
|
||||
1
changelog.d/.gitignore
vendored
Normal file
1
changelog.d/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/admin/.gitignore
vendored
Normal file
1
changelog.d/admin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/alias/.gitignore
vendored
Normal file
1
changelog.d/alias/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/audio/.gitignore
vendored
Normal file
1
changelog.d/audio/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/bank/.gitignore
vendored
Normal file
1
changelog.d/bank/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/cleanup/.gitignore
vendored
Normal file
1
changelog.d/cleanup/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/customcom/.gitignore
vendored
Normal file
1
changelog.d/customcom/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/downloader/.gitignore
vendored
Normal file
1
changelog.d/downloader/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/economy/.gitignore
vendored
Normal file
1
changelog.d/economy/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/filter/.gitignore
vendored
Normal file
1
changelog.d/filter/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/general/.gitignore
vendored
Normal file
1
changelog.d/general/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/image/.gitignore
vendored
Normal file
1
changelog.d/image/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/mod/.gitignore
vendored
Normal file
1
changelog.d/mod/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/modlog/.gitignore
vendored
Normal file
1
changelog.d/modlog/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/mutes/.gitignore
vendored
Normal file
1
changelog.d/mutes/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/permissions/.gitignore
vendored
Normal file
1
changelog.d/permissions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/reports/.gitignore
vendored
Normal file
1
changelog.d/reports/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/streams/.gitignore
vendored
Normal file
1
changelog.d/streams/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/trivia/.gitignore
vendored
Normal file
1
changelog.d/trivia/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
changelog.d/warnings/.gitignore
vendored
Normal file
1
changelog.d/warnings/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
@@ -1,5 +1,8 @@
|
||||
api_key_env: CROWDIN_API_KEY
|
||||
project_identifier_env: CROWDIN_PROJECT_ID
|
||||
base_path: ./redbot/
|
||||
files:
|
||||
- source: /redbot/**/*.pot
|
||||
- source: cogs/**/messages.pot
|
||||
translation: /%original_path%/%locale%.po
|
||||
- source: core/**/messages.pot
|
||||
translation: /%original_path%/%locale%.po
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Discord API Wrapper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A basic wrapper for the Discord API.
|
||||
|
||||
:copyright: (c) 2015-2017 Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
||||
|
||||
__title__ = "discord"
|
||||
__author__ = "Rapptz"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2015-2017 Rapptz"
|
||||
__version__ = "1.0.0a"
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from .client import Client, AppInfo
|
||||
from .user import User, ClientUser, Profile
|
||||
from .emoji import Emoji, PartialEmoji
|
||||
from .activity import *
|
||||
from .channel import *
|
||||
from .guild import Guild
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .message import Message, Attachment
|
||||
from .errors import *
|
||||
from .calls import CallMessage, GroupCall
|
||||
from .permissions import Permissions, PermissionOverwrite
|
||||
from .role import Role
|
||||
from .file import File
|
||||
from .colour import Color, Colour
|
||||
from .invite import Invite
|
||||
from .object import Object
|
||||
from .reaction import Reaction
|
||||
from . import utils, opus, abc
|
||||
from .enums import *
|
||||
from .embeds import Embed
|
||||
from .shard import AutoShardedClient
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import VoiceClient
|
||||
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
|
||||
from .raw_models import *
|
||||
|
||||
VersionInfo = namedtuple("VersionInfo", "major minor micro releaselevel serial")
|
||||
|
||||
version_info = VersionInfo(major=1, minor=0, micro=0, releaselevel="alpha", serial=0)
|
||||
|
||||
try:
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
@@ -1,337 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
def core(parser, args):
|
||||
pass
|
||||
|
||||
|
||||
bot_template = """#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import config
|
||||
|
||||
class Bot(commands.{base}):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs)
|
||||
for cog in config.cogs:
|
||||
try:
|
||||
self.load_extension(cog)
|
||||
except Exception as exc:
|
||||
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
|
||||
|
||||
async def on_ready(self):
|
||||
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
|
||||
|
||||
|
||||
bot = Bot()
|
||||
|
||||
# write general commands here
|
||||
|
||||
bot.run(config.token)
|
||||
"""
|
||||
|
||||
gitignore_template = """# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Our configuration files
|
||||
config.py
|
||||
"""
|
||||
|
||||
cog_template = '''# -*- coding: utf-8 -*-
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
|
||||
class {name}:
|
||||
"""The description for {name} goes here."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
{extra}
|
||||
def setup(bot):
|
||||
bot.add_cog({name}(bot))
|
||||
'''
|
||||
|
||||
cog_extras = """
|
||||
def __unload(self):
|
||||
# clean up logic goes here
|
||||
pass
|
||||
|
||||
async def __local_check(self, ctx):
|
||||
# checks that apply to every command in here
|
||||
return True
|
||||
|
||||
async def __global_check(self, ctx):
|
||||
# checks that apply to every command to the bot
|
||||
return True
|
||||
|
||||
async def __global_check_once(self, ctx):
|
||||
# check that apply to every command but is guaranteed to be called only once
|
||||
return True
|
||||
|
||||
async def __error(self, ctx, error):
|
||||
# error handling to every command in here
|
||||
pass
|
||||
|
||||
async def __before_invoke(self, ctx):
|
||||
# called before a command is called here
|
||||
pass
|
||||
|
||||
async def __after_invoke(self, ctx):
|
||||
# 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
|
||||
# '\\': '-',
|
||||
"|": "-",
|
||||
"?": "-",
|
||||
"*": "-",
|
||||
}
|
||||
|
||||
#
|
||||
_base_table.update((chr(i), None) for i in range(32))
|
||||
|
||||
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 len(name) <= 4 and name.upper() in forbidden:
|
||||
parser.error("invalid directory name given, use a different one")
|
||||
|
||||
name = name.translate(translation_table)
|
||||
if replace_spaces:
|
||||
name = name.replace(" ", "-")
|
||||
return Path(name)
|
||||
|
||||
|
||||
def newbot(parser, args):
|
||||
if sys.version_info < (3, 5):
|
||||
parser.error("python version is older than 3.5, consider upgrading.")
|
||||
|
||||
new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
|
||||
|
||||
# as a note exist_ok for Path is a 3.5+ only feature
|
||||
# since we already checked above that we're >3.5
|
||||
try:
|
||||
new_directory.mkdir(exist_ok=True, parents=True)
|
||||
except OSError as exc:
|
||||
parser.error("could not create our bot directory ({})".format(exc))
|
||||
|
||||
cogs = new_directory / "cogs"
|
||||
|
||||
try:
|
||||
cogs.mkdir(exist_ok=True)
|
||||
init = cogs / "__init__.py"
|
||||
init.touch()
|
||||
except OSError as exc:
|
||||
print("warning: could not create cogs directory ({})".format(exc))
|
||||
|
||||
try:
|
||||
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("could not create config file ({})".format(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))
|
||||
except OSError as exc:
|
||||
parser.error("could not create bot file ({})".format(exc))
|
||||
|
||||
if not args.no_git:
|
||||
try:
|
||||
with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp:
|
||||
fp.write(gitignore_template)
|
||||
except OSError as exc:
|
||||
print("warning: could not create .gitignore file ({})".format(exc))
|
||||
|
||||
print("successfully made bot at", new_directory)
|
||||
|
||||
|
||||
def newcog(parser, args):
|
||||
if sys.version_info < (3, 5):
|
||||
parser.error("python version is older than 3.5, consider upgrading.")
|
||||
|
||||
cog_dir = to_path(parser, args.directory)
|
||||
try:
|
||||
cog_dir.mkdir(exist_ok=True)
|
||||
except OSError as exc:
|
||||
print("warning: could not create cogs directory ({})".format(exc))
|
||||
|
||||
directory = cog_dir / to_path(parser, args.name)
|
||||
directory = directory.with_suffix(".py")
|
||||
try:
|
||||
with open(str(directory), "w", encoding="utf-8") as fp:
|
||||
extra = cog_extras if args.full else ""
|
||||
if args.class_name:
|
||||
name = args.class_name
|
||||
else:
|
||||
name = str(directory.stem)
|
||||
if "-" in name:
|
||||
name = name.replace("-", " ").title().replace(" ", "")
|
||||
else:
|
||||
name = name.title()
|
||||
fp.write(cog_template.format(name=name, extra=extra))
|
||||
except OSError as exc:
|
||||
parser.error("could not create cog file ({})".format(exc))
|
||||
else:
|
||||
print("successfully made cog at", directory)
|
||||
|
||||
|
||||
def add_newbot_args(subparser):
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
def add_newcog_args(subparser):
|
||||
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("--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"
|
||||
)
|
||||
|
||||
version = "discord.py v{0.__version__} for Python {1[0]}.{1[1]}.{1[2]}".format(
|
||||
discord, sys.version_info
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version", action="version", version=version, help="shows the library version"
|
||||
)
|
||||
parser.set_defaults(func=core)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
main()
|
||||
1030
discord/abc.py
1030
discord/abc.py
File diff suppressed because it is too large
Load Diff
@@ -1,613 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from .enums import ActivityType, try_enum
|
||||
from .colour import Colour
|
||||
|
||||
__all__ = ["Activity", "Streaming", "Game", "Spotify"]
|
||||
|
||||
"""If curious, this is the current schema for an activity.
|
||||
|
||||
It's fairly long so I will document it here:
|
||||
|
||||
All keys are optional.
|
||||
|
||||
state: str (max: 128),
|
||||
details: str (max: 128)
|
||||
timestamps: dict
|
||||
start: int (min: 1)
|
||||
end: int (min: 1)
|
||||
assets: dict
|
||||
large_image: str (max: 32)
|
||||
large_text: str (max: 128)
|
||||
small_image: str (max: 32)
|
||||
small_text: str (max: 128)
|
||||
party: dict
|
||||
id: str (max: 128),
|
||||
size: List[int] (max-length: 2)
|
||||
elem: int (min: 1)
|
||||
secrets: dict
|
||||
match: str (max: 128)
|
||||
join: str (max: 128)
|
||||
spectate: str (max: 128)
|
||||
instance: bool
|
||||
application_id: str
|
||||
name: str (max: 128)
|
||||
url: str
|
||||
type: int
|
||||
sync_id: str
|
||||
session_id: str
|
||||
flags: int
|
||||
|
||||
There are also activity flags which are mostly uninteresting for the library atm.
|
||||
|
||||
t.ActivityFlags = {
|
||||
INSTANCE: 1,
|
||||
JOIN: 2,
|
||||
SPECTATE: 4,
|
||||
JOIN_REQUEST: 8,
|
||||
SYNC: 16,
|
||||
PLAY: 32
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class _ActivityTag:
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class Activity(_ActivityTag):
|
||||
"""Represents an activity in Discord.
|
||||
|
||||
This could be an activity such as streaming, playing, listening
|
||||
or watching.
|
||||
|
||||
For memory optimisation purposes, some activities are offered in slimmed
|
||||
down versions:
|
||||
|
||||
- :class:`Game`
|
||||
- :class:`Streaming`
|
||||
|
||||
Attributes
|
||||
------------
|
||||
application_id: :class:`str`
|
||||
The application ID of the game.
|
||||
name: :class:`str`
|
||||
The name of the activity.
|
||||
url: :class:`str`
|
||||
A stream URL that the activity could be doing.
|
||||
type: :class:`ActivityType`
|
||||
The type of activity currently being done.
|
||||
state: :class:`str`
|
||||
The user's current state. For example, "In Game".
|
||||
details: :class:`str`
|
||||
The detail of the user's current activity.
|
||||
timestamps: :class:`dict`
|
||||
A dictionary of timestamps. It contains the following optional keys:
|
||||
|
||||
- ``start``: Corresponds to when the user started doing the
|
||||
activity in milliseconds since Unix epoch.
|
||||
- ``end``: Corresponds to when the user will finish doing the
|
||||
activity in milliseconds since Unix epoch.
|
||||
|
||||
assets: :class:`dict`
|
||||
A dictionary representing the images and their hover text of an activity.
|
||||
It contains the following optional keys:
|
||||
|
||||
- ``large_image``: A string representing the ID for the large image asset.
|
||||
- ``large_text``: A string representing the text when hovering over the large image asset.
|
||||
- ``small_image``: A string representing the ID for the small image asset.
|
||||
- ``small_text``: A string representing the text when hovering over the small image asset.
|
||||
|
||||
party: :class:`dict`
|
||||
A dictionary representing the activity party. It contains the following optional keys:
|
||||
|
||||
- ``id``: A string representing the party ID.
|
||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"state",
|
||||
"details",
|
||||
"timestamps",
|
||||
"assets",
|
||||
"party",
|
||||
"flags",
|
||||
"sync_id",
|
||||
"session_id",
|
||||
"type",
|
||||
"name",
|
||||
"url",
|
||||
"application_id",
|
||||
)
|
||||
|
||||
def __init__(self, **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 = kwargs.pop("application_id", None)
|
||||
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.type = try_enum(ActivityType, kwargs.pop("type", -1))
|
||||
|
||||
def to_dict(self):
|
||||
ret = {}
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr, None)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, dict) and len(value) == 0:
|
||||
continue
|
||||
|
||||
ret[attr] = value
|
||||
ret["type"] = int(self.type)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps["start"] / 1000)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
||||
try:
|
||||
return datetime.datetime.utcfromtimestamp(self.timestamps["end"] / 1000)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def large_image_url(self):
|
||||
"""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"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
|
||||
self.application_id, large_image
|
||||
)
|
||||
|
||||
@property
|
||||
def small_image_url(self):
|
||||
"""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"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
|
||||
self.application_id, small_image
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
def small_image_text(self):
|
||||
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
|
||||
return self.assets.get("small_text", None)
|
||||
|
||||
|
||||
class Game(_ActivityTag):
|
||||
"""A slimmed down version of :class:`Activity` that represents a Discord game.
|
||||
|
||||
This is typically displayed via **Playing** on the official Discord client.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two games are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two games are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the game's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the game's name.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The game's name.
|
||||
start: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
|
||||
end: Optional[:class:`datetime.datetime`]
|
||||
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The game's name.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "_end", "_start")
|
||||
|
||||
def __init__(self, name, **extra):
|
||||
self.name = name
|
||||
|
||||
try:
|
||||
timestamps = extra["timestamps"]
|
||||
except KeyError:
|
||||
self._extract_timestamp(extra, "start")
|
||||
self._extract_timestamp(extra, "end")
|
||||
else:
|
||||
self._start = timestamps.get("start", 0)
|
||||
self._end = timestamps.get("end", 0)
|
||||
|
||||
def _extract_timestamp(self, data, key):
|
||||
try:
|
||||
dt = data[key]
|
||||
except KeyError:
|
||||
setattr(self, "_" + key, 0)
|
||||
else:
|
||||
setattr(self, "_" + key, dt.timestamp() * 1000.0)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.playing`.
|
||||
"""
|
||||
return ActivityType.playing
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""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)
|
||||
return None
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
"""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)
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Game name={0.name!r}>".format(self)
|
||||
|
||||
def to_dict(self):
|
||||
timestamps = {}
|
||||
if self._start:
|
||||
timestamps["start"] = self._start
|
||||
|
||||
if self._end:
|
||||
timestamps["end"] = self._end
|
||||
|
||||
return {
|
||||
"type": ActivityType.playing.value,
|
||||
"name": str(self.name),
|
||||
"timestamps": timestamps,
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Game) and other.name == self.name
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Streaming(_ActivityTag):
|
||||
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
||||
|
||||
This is typically displayed via **Streaming** on the official Discord client.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two streams are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two streams are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the stream's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the stream's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The stream's name.
|
||||
url: :class:`str`
|
||||
The stream's URL. Currently only twitch.tv URLs are supported. Anything else is silently
|
||||
discarded.
|
||||
details: Optional[:class:`str`]
|
||||
If provided, typically the game the streamer is playing.
|
||||
assets: :class:`dict`
|
||||
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "url", "details", "assets")
|
||||
|
||||
def __init__(self, *, name, url, **extra):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.details = extra.pop("details", None)
|
||||
self.assets = extra.pop("assets", {})
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Returns the game's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.streaming`.
|
||||
"""
|
||||
return ActivityType.streaming
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Streaming name={0.name!r}>".format(self)
|
||||
|
||||
@property
|
||||
def twitch_name(self):
|
||||
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
|
||||
|
||||
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
|
||||
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
|
||||
"""
|
||||
|
||||
try:
|
||||
name = self.assets["large_image"]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return name[7:] if name[:7] == "twitch:" else None
|
||||
|
||||
def to_dict(self):
|
||||
ret = {
|
||||
"type": ActivityType.streaming.value,
|
||||
"name": str(self.name),
|
||||
"url": str(self.url),
|
||||
"assets": self.assets,
|
||||
}
|
||||
if self.details:
|
||||
ret["details"] = self.details
|
||||
return ret
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
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.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two activities are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two activities are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the activity's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the string 'Spotify'.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_state",
|
||||
"_details",
|
||||
"_timestamps",
|
||||
"_assets",
|
||||
"_party",
|
||||
"_sync_id",
|
||||
"_session_id",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Returns the activity's type. This is for compatibility with :class:`Activity`.
|
||||
|
||||
It always returns :attr:`ActivityType.listening`.
|
||||
"""
|
||||
return ActivityType.listening
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
"""Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :meth:`color`"""
|
||||
return Colour(0x1DB954)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
"""Returns the Spotify integration colour, as a :class:`Colour`.
|
||||
|
||||
There is an alias for this named :meth:`colour`"""
|
||||
return self.colour
|
||||
|
||||
def to_dict(self):
|
||||
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,
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""":class:`str`: The activity's name. This will always return "Spotify"."""
|
||||
return "Spotify"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Spotify) and other._session_id == self._session_id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._session_id)
|
||||
|
||||
def __str__(self):
|
||||
return "Spotify"
|
||||
|
||||
def __repr__(self):
|
||||
return "<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>".format(
|
||||
self
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""":class:`str`: The title of the song being played."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
"""List[:class:`str`]: The artists of the song being played."""
|
||||
return self._state.split("; ")
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
""":class:`str`: The artist of the song being played.
|
||||
|
||||
This does not attempt to split the artist information into
|
||||
multiple artists. Useful if there's only a single artist.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def album(self):
|
||||
""":class:`str`: The album that the song being played belongs to."""
|
||||
return self._assets.get("large_text", "")
|
||||
|
||||
@property
|
||||
def album_cover_url(self):
|
||||
""":class:`str`: The album cover image URL from Spotify's CDN."""
|
||||
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
|
||||
|
||||
@property
|
||||
def track_id(self):
|
||||
""":class:`str`: The track ID used by Spotify to identify this song."""
|
||||
return self._sync_id
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps["start"] / 1000)
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
|
||||
return datetime.datetime.utcfromtimestamp(self._timestamps["end"] / 1000)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
""":class:`datetime.timedelta`: The duration of the song being played."""
|
||||
return self.end - self.start
|
||||
|
||||
@property
|
||||
def party_id(self):
|
||||
""":class:`str`: The party ID of the listening party."""
|
||||
return self._party.get("id", "")
|
||||
|
||||
|
||||
def create_activity(data):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
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:
|
||||
return Activity(**data)
|
||||
return Game(**data)
|
||||
elif game_type is ActivityType.streaming:
|
||||
if "url" in data:
|
||||
return Streaming(**data)
|
||||
return Activity(**data)
|
||||
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
|
||||
return Spotify(**data)
|
||||
return Activity(**data)
|
||||
@@ -1,366 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 . import utils, enums
|
||||
from .object import Object
|
||||
from .permissions import PermissionOverwrite, Permissions
|
||||
from .colour import Colour
|
||||
from .invite import Invite
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _transform_explicit_content_filter(entry, data):
|
||||
return enums.try_enum(enums.ContentFilter, data)
|
||||
|
||||
|
||||
def _transform_permissions(entry, data):
|
||||
return Permissions(data)
|
||||
|
||||
|
||||
def _transform_color(entry, data):
|
||||
return Colour(data)
|
||||
|
||||
|
||||
def _transform_snowflake(entry, data):
|
||||
return int(data)
|
||||
|
||||
|
||||
def _transform_channel(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
channel = entry.guild.get_channel(int(data)) or Object(id=data)
|
||||
return channel
|
||||
|
||||
|
||||
def _transform_owner_id(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
|
||||
def _transform_inviter_id(entry, data):
|
||||
if data is None:
|
||||
return None
|
||||
return entry._get_member(int(data))
|
||||
|
||||
|
||||
def _transform_overwrites(entry, data):
|
||||
overwrites = []
|
||||
for elem in data:
|
||||
allow = Permissions(elem["allow"])
|
||||
deny = Permissions(elem["deny"])
|
||||
ow = PermissionOverwrite.from_pair(allow, deny)
|
||||
|
||||
ow_type = elem["type"]
|
||||
ow_id = int(elem["id"])
|
||||
if ow_type == "role":
|
||||
target = entry.guild.get_role(ow_id)
|
||||
else:
|
||||
target = entry._get_member(ow_id)
|
||||
|
||||
if target is None:
|
||||
target = Object(id=ow_id)
|
||||
|
||||
overwrites.append((target, ow))
|
||||
|
||||
return overwrites
|
||||
|
||||
|
||||
class AuditLogDiff:
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__dict__.items())
|
||||
|
||||
def __repr__(self):
|
||||
return "<AuditLogDiff attrs={0!r}>".format(tuple(self.__dict__))
|
||||
|
||||
|
||||
class AuditLogChanges:
|
||||
TRANSFORMERS = {
|
||||
"verification_level": (None, _transform_verification_level),
|
||||
"explicit_content_filter": (None, _transform_explicit_content_filter),
|
||||
"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),
|
||||
"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),
|
||||
"permission_overwrites": ("overwrites", _transform_overwrites),
|
||||
"splash_hash": ("splash", None),
|
||||
"icon_hash": ("icon", None),
|
||||
"avatar_hash": ("avatar", None),
|
||||
"rate_limit_per_user": ("slowmode_delay", None),
|
||||
"default_message_notifications": (
|
||||
"default_notifications",
|
||||
_transform_default_notifications,
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, entry, data):
|
||||
self.before = AuditLogDiff()
|
||||
self.after = AuditLogDiff()
|
||||
|
||||
for elem in data:
|
||||
attr = elem["key"]
|
||||
|
||||
# special cases for role add/remove
|
||||
if attr == "$add":
|
||||
self._handle_role(self.before, self.after, entry, elem["new_value"])
|
||||
continue
|
||||
elif attr == "$remove":
|
||||
self._handle_role(self.after, self.before, entry, elem["new_value"])
|
||||
continue
|
||||
|
||||
transformer = self.TRANSFORMERS.get(attr)
|
||||
if transformer:
|
||||
key, transformer = transformer
|
||||
if key:
|
||||
attr = key
|
||||
|
||||
try:
|
||||
before = elem["old_value"]
|
||||
except KeyError:
|
||||
before = None
|
||||
else:
|
||||
if transformer:
|
||||
before = transformer(entry, before)
|
||||
|
||||
setattr(self.before, attr, before)
|
||||
|
||||
try:
|
||||
after = elem["new_value"]
|
||||
except KeyError:
|
||||
after = None
|
||||
else:
|
||||
if transformer:
|
||||
after = transformer(entry, after)
|
||||
|
||||
setattr(self.after, attr, after)
|
||||
|
||||
# add an alias
|
||||
if hasattr(self.after, "colour"):
|
||||
self.after.color = self.after.colour
|
||||
self.before.color = self.before.colour
|
||||
|
||||
def _handle_role(self, first, second, entry, elem):
|
||||
if not hasattr(first, "roles"):
|
||||
setattr(first, "roles", [])
|
||||
|
||||
data = []
|
||||
g = entry.guild
|
||||
|
||||
for e in elem:
|
||||
role_id = int(e["id"])
|
||||
role = g.get_role(role_id)
|
||||
|
||||
if role is None:
|
||||
role = Object(id=role_id)
|
||||
role.name = e["name"]
|
||||
|
||||
data.append(role)
|
||||
|
||||
setattr(second, "roles", data)
|
||||
|
||||
|
||||
class AuditLogEntry:
|
||||
r"""Represents an Audit Log entry.
|
||||
|
||||
You retrieve these via :meth:`Guild.audit_logs`.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
action: :class:`AuditLogAction`
|
||||
The action that was done.
|
||||
user: :class:`abc.User`
|
||||
The user who initiated this action. Usually a :class:`Member`\, unless gone
|
||||
then it's a :class:`User`.
|
||||
id: :class:`int`
|
||||
The entry ID.
|
||||
target: Any
|
||||
The target that got changed. The exact type of this depends on
|
||||
the action being done.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason this action was done.
|
||||
extra: Any
|
||||
Extra information that this entry has that might be useful.
|
||||
For most actions, this is ``None``. However in some cases it
|
||||
contains extra information. See :class:`AuditLogAction` for
|
||||
which actions have this field filled out.
|
||||
"""
|
||||
|
||||
def __init__(self, *, users, data, guild):
|
||||
self._state = guild._state
|
||||
self.guild = guild
|
||||
self._users = users
|
||||
self._from_data(data)
|
||||
|
||||
def _from_data(self, data):
|
||||
self.action = 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")
|
||||
|
||||
if 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()}
|
||||
)()
|
||||
elif self.action is enums.AuditLogAction.message_delete:
|
||||
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),
|
||||
}
|
||||
self.extra = 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":
|
||||
self.extra = self._get_member(instance_id)
|
||||
else:
|
||||
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
|
||||
|
||||
# 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.user = self._get_member(utils._get_as_snowflake(data, "user_id"))
|
||||
self._target_id = utils._get_as_snowflake(data, "target_id")
|
||||
|
||||
def _get_member(self, user_id):
|
||||
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)
|
||||
|
||||
@utils.cached_property
|
||||
def created_at(self):
|
||||
"""Returns the entry's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@utils.cached_property
|
||||
def target(self):
|
||||
try:
|
||||
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):
|
||||
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
|
||||
return self.action.category
|
||||
|
||||
@utils.cached_property
|
||||
def changes(self):
|
||||
""":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):
|
||||
""":class:`AuditLogDiff`: The target's prior state."""
|
||||
return self.changes.before
|
||||
|
||||
@utils.cached_property
|
||||
def after(self):
|
||||
""":class:`AuditLogDiff`: The target's subsequent state."""
|
||||
return self.changes.after
|
||||
|
||||
def _convert_target_guild(self, target_id):
|
||||
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_user(self, target_id):
|
||||
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_invite(self, target_id):
|
||||
# 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,
|
||||
}
|
||||
|
||||
obj = Invite(state=self._state, data=fake_payload)
|
||||
try:
|
||||
obj.inviter = changeset.inviter
|
||||
except AttributeError:
|
||||
pass
|
||||
return obj
|
||||
|
||||
def _convert_target_emoji(self, target_id):
|
||||
return self._state.get_emoji(target_id) or Object(id=target_id)
|
||||
|
||||
def _convert_target_message(self, target_id):
|
||||
return self._get_member(target_id)
|
||||
@@ -1,86 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
class ExponentialBackoff:
|
||||
"""An implementation of the exponential backoff algorithm
|
||||
|
||||
Provides a convenient interface to implement an exponential backoff
|
||||
for reconnecting or retrying transmissions in a distributed network.
|
||||
|
||||
Once instantiated, the delay method will return the next interval to
|
||||
wait for when retrying a connection or transmission. The maximum
|
||||
delay increases exponentially with each retry up to a maximum of
|
||||
2^10 * base, and is reset if no more attempts are needed in a period
|
||||
of 2^11 * base seconds.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
base: int
|
||||
The base delay in seconds. The first retry-delay will be up to
|
||||
this many seconds.
|
||||
integral: bool
|
||||
Set to True if whole periods of base is desirable, otherwise any
|
||||
number in between may be returned.
|
||||
"""
|
||||
|
||||
def __init__(self, base=1, *, integral=False):
|
||||
self._base = base
|
||||
|
||||
self._exp = 0
|
||||
self._max = 10
|
||||
self._reset_time = base * 2 ** 11
|
||||
self._last_invocation = 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
|
||||
|
||||
def delay(self):
|
||||
"""Compute the next delay
|
||||
|
||||
Returns the next delay to wait according to the exponential
|
||||
backoff algorithm. This is a value between 0 and base * 2^exp
|
||||
where exponent starts off at 1 and is incremented at every
|
||||
invocation of this method up to a maximum of 10.
|
||||
|
||||
If a period of more than base * 2^11 has passed since the last
|
||||
retry, the exponent is reset to 1.
|
||||
"""
|
||||
invocation = time.monotonic()
|
||||
interval = invocation - self._last_invocation
|
||||
self._last_invocation = invocation
|
||||
|
||||
if interval > self._reset_time:
|
||||
self._exp = 0
|
||||
|
||||
self._exp = min(self._exp + 1, self._max)
|
||||
return self._randfunc(0, self._base * 2 ** self._exp)
|
||||
157
discord/calls.py
157
discord/calls.py
@@ -1,157 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from . import utils
|
||||
from .enums import VoiceRegion, try_enum
|
||||
from .member import VoiceState
|
||||
|
||||
|
||||
class CallMessage:
|
||||
"""Represents a group call message from Discord.
|
||||
|
||||
This is only received in cases where the message type is equivalent to
|
||||
:attr:`MessageType.call`.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
ended_timestamp: Optional[datetime.datetime]
|
||||
A naive UTC datetime object that represents the time that the call has ended.
|
||||
participants: List[:class:`User`]
|
||||
The list of users that are participating in this call.
|
||||
message: :class:`Message`
|
||||
The message associated with this call message.
|
||||
"""
|
||||
|
||||
def __init__(self, message, **kwargs):
|
||||
self.message = message
|
||||
self.ended_timestamp = utils.parse_time(kwargs.get("ended_timestamp"))
|
||||
self.participants = kwargs.get("participants")
|
||||
|
||||
@property
|
||||
def call_ended(self):
|
||||
""":obj:`bool`: Indicates if the call has ended."""
|
||||
return self.ended_timestamp is not None
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: The private channel associated with this message."""
|
||||
return self.message.channel
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Queries the duration of the call.
|
||||
|
||||
If the call has not ended then the current duration will
|
||||
be returned.
|
||||
|
||||
Returns
|
||||
---------
|
||||
datetime.timedelta
|
||||
The timedelta object representing the duration.
|
||||
"""
|
||||
if self.ended_timestamp is None:
|
||||
return datetime.datetime.utcnow() - self.message.created_at
|
||||
else:
|
||||
return self.ended_timestamp - self.message.created_at
|
||||
|
||||
|
||||
class GroupCall:
|
||||
"""Represents the actual group call from Discord.
|
||||
|
||||
This is accompanied with a :class:`CallMessage` denoting the information.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
call: :class:`CallMessage`
|
||||
The call message associated with this group call.
|
||||
unavailable: :obj:`bool`
|
||||
Denotes if this group call is unavailable.
|
||||
ringing: List[:class:`User`]
|
||||
A list of users that are currently being rung to join the call.
|
||||
region: :class:`VoiceRegion`
|
||||
The guild region the group call is being hosted on.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.call = kwargs.get("call")
|
||||
self.unavailable = kwargs.get("unavailable")
|
||||
self._voice_states = {}
|
||||
|
||||
for state in kwargs.get("voice_states", []):
|
||||
self._update_voice_state(state)
|
||||
|
||||
self._update(**kwargs)
|
||||
|
||||
def _update(self, **kwargs):
|
||||
self.region = try_enum(VoiceRegion, kwargs.get("region"))
|
||||
lookup = {u.id: u for u in self.call.channel.recipients}
|
||||
me = self.call.channel.me
|
||||
lookup[me.id] = me
|
||||
self.ringing = list(filter(None, map(lookup.get, kwargs.get("ringing", []))))
|
||||
|
||||
def _update_voice_state(self, data):
|
||||
user_id = int(data["user_id"])
|
||||
# left the voice channel?
|
||||
if data["channel_id"] is None:
|
||||
self._voice_states.pop(user_id, None)
|
||||
else:
|
||||
self._voice_states[user_id] = VoiceState(data=data, channel=self.channel)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""A property that returns the :obj:`list` of :class:`User` that are currently in this call."""
|
||||
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
|
||||
me = self.channel.me
|
||||
if self.voice_state_for(me) is not None:
|
||||
ret.append(me)
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
r""":class:`GroupChannel`\: Returns the channel the group call is in."""
|
||||
return self.call.channel
|
||||
|
||||
def voice_state_for(self, user):
|
||||
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
|
||||
|
||||
If the :class:`User` has no voice state then this function returns
|
||||
``None``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
user: :class:`User`
|
||||
The user to retrieve the voice state for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`VoiceState`]
|
||||
The voice state associated with this user.
|
||||
"""
|
||||
|
||||
return self._voice_states.get(user.id)
|
||||
@@ -1,986 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
import discord.abc
|
||||
from .permissions import Permissions
|
||||
from .enums import ChannelType, try_enum
|
||||
from .mixins import Hashable
|
||||
from . import utils
|
||||
from .errors import ClientException, NoMoreItems
|
||||
from .webhook import Webhook
|
||||
|
||||
__all__ = [
|
||||
"TextChannel",
|
||||
"VoiceChannel",
|
||||
"DMChannel",
|
||||
"CategoryChannel",
|
||||
"GroupChannel",
|
||||
"_channel_factory",
|
||||
]
|
||||
|
||||
|
||||
async def _single_delete_strategy(messages):
|
||||
for m in messages:
|
||||
await m.delete()
|
||||
|
||||
|
||||
class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord guild text channel.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the channel's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The channel name.
|
||||
guild: :class:`Guild`
|
||||
The guild the channel belongs to.
|
||||
id: :class:`int`
|
||||
The channel ID.
|
||||
category_id: :class:`int`
|
||||
The category channel ID this channel belongs to.
|
||||
topic: Optional[:class:`str`]
|
||||
The channel's topic. None if it doesn't exist.
|
||||
position: :class:`int`
|
||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||
top channel is position 0.
|
||||
slowmode_delay: :class:`int`
|
||||
The number of seconds a member must wait between sending messages
|
||||
in this channel. A value of `0` denotes that it is disabled.
|
||||
Bots and users with :attr:`~Permissions.manage_channels` or
|
||||
:attr:`~Permissions.manage_messages` bypass slowmode.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"name",
|
||||
"id",
|
||||
"guild",
|
||||
"topic",
|
||||
"_state",
|
||||
"nsfw",
|
||||
"category_id",
|
||||
"position",
|
||||
"slowmode_delay",
|
||||
"_overwrites",
|
||||
)
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self._update(guild, data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TextChannel id={0.id} name={0.name!r} position={0.position}>".format(self)
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data["name"]
|
||||
self.category_id = utils._get_as_snowflake(data, "parent_id")
|
||||
self.topic = data.get("topic")
|
||||
self.position = data["position"]
|
||||
self.nsfw = data.get("nsfw", False)
|
||||
# Does this need coercion into `int`? No idea yet.
|
||||
self.slowmode_delay = data.get("rate_limit_per_user", 0)
|
||||
self._fill_overwrites(data)
|
||||
|
||||
async def _get_channel(self):
|
||||
return self
|
||||
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# text channels do not have voice related permissions
|
||||
denied = Permissions.voice()
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""Returns a :class:`list` of :class:`Member` that can see this channel."""
|
||||
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
||||
|
||||
def is_nsfw(self):
|
||||
"""Checks if the channel is NSFW."""
|
||||
n = self.name
|
||||
return self.nsfw or n == "nsfw" or n[:5] == "nsfw-"
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
Edits the channel.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: :class:`str`
|
||||
The new channel name.
|
||||
topic: :class:`str`
|
||||
The new channel's topic.
|
||||
position: :class:`int`
|
||||
The new channel's position.
|
||||
nsfw: :class:`bool`
|
||||
To mark the channel as NSFW or not.
|
||||
sync_permissions: :class:`bool`
|
||||
Whether to sync permissions with the channel's new or pre-existing
|
||||
category. Defaults to ``False``.
|
||||
category: Optional[:class:`CategoryChannel`]
|
||||
The new category for this channel. Can be ``None`` to remove the
|
||||
category.
|
||||
slowmode_delay: :class:`int`
|
||||
Specifies the slowmode rate limit for user in this channel. A value of
|
||||
`0` disables slowmode. The maximum value possible is `120`.
|
||||
reason: Optional[:class:`str`]
|
||||
The reason for editing this channel. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If position is less than 0 or greater than the number of channels.
|
||||
Forbidden
|
||||
You do not have permissions to edit the channel.
|
||||
HTTPException
|
||||
Editing the channel failed.
|
||||
"""
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
async def delete_messages(self, messages):
|
||||
"""|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.
|
||||
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 = [m.id for m in messages]
|
||||
await self._state.http.delete_messages(self.id, message_ids)
|
||||
|
||||
async def purge(
|
||||
self,
|
||||
*,
|
||||
limit=100,
|
||||
check=None,
|
||||
before=None,
|
||||
after=None,
|
||||
around=None,
|
||||
reverse=False,
|
||||
bulk=True
|
||||
):
|
||||
"""|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.
|
||||
|
||||
Internally, this employs a different number of strategies depending
|
||||
on the conditions met such as if a bulk delete is possible or if
|
||||
the account is a user bot or not.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
limit: int
|
||||
The number of messages to search through. This is not the number
|
||||
of messages that will be deleted, though it can be.
|
||||
check: predicate
|
||||
The function used to check if a message should be deleted.
|
||||
It must take a :class:`Message` as its sole parameter.
|
||||
before
|
||||
Same as ``before`` in :meth:`history`.
|
||||
after
|
||||
Same as ``after`` in :meth:`history`.
|
||||
around
|
||||
Same as ``around`` in :meth:`history`.
|
||||
reverse
|
||||
Same as ``reverse`` in :meth:`history`.
|
||||
bulk: bool
|
||||
If True, use bulk delete. bulk=False is useful for mass-deleting
|
||||
a bot's own messages without manage_messages. When True, will fall
|
||||
back to single delete if current account is a user bot, or 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.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Deleting bot's messages ::
|
||||
|
||||
def is_me(m):
|
||||
return m.author == client.user
|
||||
|
||||
deleted = await channel.purge(limit=100, check=is_me)
|
||||
await channel.send('Deleted {} message(s)'.format(len(deleted)))
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
The list of messages that were deleted.
|
||||
"""
|
||||
|
||||
if check is None:
|
||||
check = lambda m: True
|
||||
|
||||
iterator = self.history(
|
||||
limit=limit, before=before, after=after, reverse=reverse, around=around
|
||||
)
|
||||
ret = []
|
||||
count = 0
|
||||
|
||||
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
|
||||
strategy = self.delete_messages if self._state.is_bot and bulk else _single_delete_strategy
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = await iterator.next()
|
||||
except NoMoreItems:
|
||||
# no more messages 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
|
||||
else:
|
||||
if count == 100:
|
||||
# we've reached a full 'queue'
|
||||
to_delete = ret[-100:]
|
||||
await strategy(to_delete)
|
||||
count = 0
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if check(msg):
|
||||
if msg.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(msg)
|
||||
|
||||
async def webhooks(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the list of webhooks from this channel.
|
||||
|
||||
Requires :attr:`~.Permissions.manage_webhooks` permissions.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You don't have permissions to get the webhooks.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`Webhook`]
|
||||
The webhooks for this channel.
|
||||
"""
|
||||
|
||||
data = await self._state.http.channel_webhooks(self.id)
|
||||
return [Webhook.from_state(d, state=self._state) for d in data]
|
||||
|
||||
async def create_webhook(self, *, name, avatar=None):
|
||||
"""|coro|
|
||||
|
||||
Creates a webhook for this channel.
|
||||
|
||||
Requires :attr:`~.Permissions.manage_webhooks` permissions.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
name: str
|
||||
The webhook's name.
|
||||
avatar: Optional[bytes]
|
||||
A :term:`py:bytes-like object` representing the webhook's default avatar.
|
||||
This operates similarly to :meth:`~ClientUser.edit`.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Creating the webhook failed.
|
||||
Forbidden
|
||||
You do not have permissions to create a webhook.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Webhook`
|
||||
The created webhook.
|
||||
"""
|
||||
|
||||
if avatar is not None:
|
||||
avatar = utils._bytes_to_base64_data(avatar)
|
||||
|
||||
data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar)
|
||||
return Webhook.from_state(data, state=self._state)
|
||||
|
||||
|
||||
class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord guild voice channel.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the channel's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The channel name.
|
||||
guild: :class:`Guild`
|
||||
The guild the channel belongs to.
|
||||
id: :class:`int`
|
||||
The channel ID.
|
||||
category_id: :class:`int`
|
||||
The category channel ID this channel belongs to.
|
||||
position: :class:`int`
|
||||
The position in the channel list. This is a number that starts at 0. e.g. the
|
||||
top channel is position 0.
|
||||
bitrate: :class:`int`
|
||||
The channel's preferred audio bitrate in bits per second.
|
||||
user_limit: :class:`int`
|
||||
The channel's limit for number of members that can be in a voice channel.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"name",
|
||||
"id",
|
||||
"guild",
|
||||
"bitrate",
|
||||
"user_limit",
|
||||
"_state",
|
||||
"position",
|
||||
"_overwrites",
|
||||
"category_id",
|
||||
)
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self._update(guild, data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<VoiceChannel id={0.id} name={0.name!r} position={0.position}>".format(self)
|
||||
|
||||
def _get_voice_client_key(self):
|
||||
return self.guild.id, "guild_id"
|
||||
|
||||
def _get_voice_state_pair(self):
|
||||
return self.guild.id, self.id
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data["name"]
|
||||
self.category_id = utils._get_as_snowflake(data, "parent_id")
|
||||
self.position = data["position"]
|
||||
self.bitrate = data.get("bitrate")
|
||||
self.user_limit = data.get("user_limit")
|
||||
self._fill_overwrites(data)
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""Returns a list of :class:`Member` that are currently inside this voice channel."""
|
||||
ret = []
|
||||
for user_id, state in self.guild._voice_states.items():
|
||||
if state.channel.id == self.id:
|
||||
member = self.guild.get_member(user_id)
|
||||
if member is not None:
|
||||
ret.append(member)
|
||||
return ret
|
||||
|
||||
def permissions_for(self, member):
|
||||
base = super().permissions_for(member)
|
||||
|
||||
# voice channels cannot be edited by people who can't connect to them
|
||||
# It also implicitly denies all other voice perms
|
||||
if not base.connect:
|
||||
denied = Permissions.voice()
|
||||
denied.update(manage_channels=True, manage_roles=True)
|
||||
base.value &= ~denied.value
|
||||
return base
|
||||
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
Edits the channel.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: str
|
||||
The new channel's name.
|
||||
bitrate: int
|
||||
The new channel's bitrate.
|
||||
user_limit: int
|
||||
The new channel's user limit.
|
||||
position: int
|
||||
The new channel's position.
|
||||
sync_permissions: bool
|
||||
Whether to sync permissions with the channel's new or pre-existing
|
||||
category. Defaults to ``False``.
|
||||
category: Optional[:class:`CategoryChannel`]
|
||||
The new category for this channel. Can be ``None`` to remove the
|
||||
category.
|
||||
reason: Optional[str]
|
||||
The reason for editing this channel. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have permissions to edit the channel.
|
||||
HTTPException
|
||||
Editing the channel failed.
|
||||
"""
|
||||
|
||||
await self._edit(options, reason=reason)
|
||||
|
||||
|
||||
class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
"""Represents a Discord channel category.
|
||||
|
||||
These are useful to group channels to logical compartments.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the category's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the category's name.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The category name.
|
||||
guild: :class:`Guild`
|
||||
The guild the category belongs to.
|
||||
id: :class:`int`
|
||||
The category channel ID.
|
||||
position: :class:`int`
|
||||
The position in the category list. This is a number that starts at 0. e.g. the
|
||||
top category is position 0.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "id", "guild", "nsfw", "_state", "position", "_overwrites", "category_id")
|
||||
|
||||
def __init__(self, *, state, guild, data):
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self._update(guild, data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CategoryChannel id={0.id} name={0.name!r} position={0.position}>".format(self)
|
||||
|
||||
def _update(self, guild, data):
|
||||
self.guild = guild
|
||||
self.name = data["name"]
|
||||
self.category_id = utils._get_as_snowflake(data, "parent_id")
|
||||
self.nsfw = data.get("nsfw", False)
|
||||
self.position = data["position"]
|
||||
self._fill_overwrites(data)
|
||||
|
||||
def is_nsfw(self):
|
||||
"""Checks if the category is NSFW."""
|
||||
n = self.name
|
||||
return self.nsfw or n == "nsfw" or n[:5] == "nsfw-"
|
||||
|
||||
async def edit(self, *, reason=None, **options):
|
||||
"""|coro|
|
||||
|
||||
Edits the channel.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: str
|
||||
The new category's name.
|
||||
position: int
|
||||
The new category's position.
|
||||
nsfw: bool
|
||||
To mark the category as NSFW or not.
|
||||
reason: Optional[str]
|
||||
The reason for editing this category. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If position is less than 0 or greater than the number of categories.
|
||||
Forbidden
|
||||
You do not have permissions to edit the category.
|
||||
HTTPException
|
||||
Editing the category failed.
|
||||
"""
|
||||
|
||||
try:
|
||||
position = options.pop("position")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
await self._move(position, reason=reason)
|
||||
self.position = position
|
||||
|
||||
if options:
|
||||
data = await self._state.http.edit_channel(self.id, reason=reason, **options)
|
||||
self._update(self.guild, data)
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
"""List[:class:`abc.GuildChannel`]: Returns the channels that are under this category.
|
||||
|
||||
These are sorted by the official Discord UI, which places voice channels below the text channels.
|
||||
"""
|
||||
|
||||
def comparator(channel):
|
||||
return (not isinstance(channel, TextChannel), channel.position)
|
||||
|
||||
ret = [c for c in self.guild.channels if c.category_id == self.id]
|
||||
ret.sort(key=comparator)
|
||||
return ret
|
||||
|
||||
|
||||
class DMChannel(discord.abc.Messageable, Hashable):
|
||||
"""Represents a Discord direct message channel.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns a string representation of the channel
|
||||
|
||||
Attributes
|
||||
----------
|
||||
recipient: :class:`User`
|
||||
The user you are participating with in the direct message channel.
|
||||
me: :class:`ClientUser`
|
||||
The user presenting yourself.
|
||||
id: :class:`int`
|
||||
The direct message channel ID.
|
||||
"""
|
||||
|
||||
__slots__ = ("id", "recipient", "me", "_state")
|
||||
|
||||
def __init__(self, *, me, state, data):
|
||||
self._state = state
|
||||
self.recipient = state.store_user(data["recipients"][0])
|
||||
self.me = me
|
||||
self.id = int(data["id"])
|
||||
|
||||
async def _get_channel(self):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return "Direct Message with %s" % self.recipient
|
||||
|
||||
def __repr__(self):
|
||||
return "<DMChannel id={0.id} recipient={0.recipient!r}>".format(self)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the direct message channel's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
def permissions_for(self, user=None):
|
||||
"""Handles permission resolution for a :class:`User`.
|
||||
|
||||
This function is there for compatibility with other channel types.
|
||||
|
||||
Actual direct messages do not really have the concept of permissions.
|
||||
|
||||
This returns all the Text related permissions set to true except:
|
||||
|
||||
- send_tts_messages: You cannot send TTS messages in a DM.
|
||||
- manage_messages: You cannot delete others messages in a DM.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user: :class:`User`
|
||||
The user to check permissions for. This parameter is ignored
|
||||
but kept for compatibility.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Permissions`
|
||||
The resolved permissions.
|
||||
"""
|
||||
|
||||
base = Permissions.text()
|
||||
base.send_tts_messages = False
|
||||
base.manage_messages = False
|
||||
return base
|
||||
|
||||
|
||||
class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
"""Represents a Discord group channel.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two channels are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two channels are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the channel's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns a string representation of the channel
|
||||
|
||||
Attributes
|
||||
----------
|
||||
recipients: :class:`list` of :class:`User`
|
||||
The users you are participating with in the group channel.
|
||||
me: :class:`ClientUser`
|
||||
The user presenting yourself.
|
||||
id: :class:`int`
|
||||
The group channel ID.
|
||||
owner: :class:`User`
|
||||
The user that owns the group channel.
|
||||
icon: Optional[:class:`str`]
|
||||
The group channel's icon hash if provided.
|
||||
name: Optional[:class:`str`]
|
||||
The group channel's name if provided.
|
||||
"""
|
||||
|
||||
__slots__ = ("id", "recipients", "owner", "icon", "name", "me", "_state")
|
||||
|
||||
def __init__(self, *, me, state, data):
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self.me = me
|
||||
self._update_group(data)
|
||||
|
||||
def _update_group(self, data):
|
||||
owner_id = utils._get_as_snowflake(data, "owner_id")
|
||||
self.icon = data.get("icon")
|
||||
self.name = data.get("name")
|
||||
|
||||
try:
|
||||
self.recipients = [self._state.store_user(u) for u in data["recipients"]]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if owner_id == self.me.id:
|
||||
self.owner = self.me
|
||||
else:
|
||||
self.owner = utils.find(lambda u: u.id == owner_id, self.recipients)
|
||||
|
||||
async def _get_channel(self):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
|
||||
if len(self.recipients) == 0:
|
||||
return "Unnamed"
|
||||
|
||||
return ", ".join(map(lambda x: x.name, self.recipients))
|
||||
|
||||
def __repr__(self):
|
||||
return "<GroupChannel id={0.id} name={0.name!r}>".format(self)
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
"""Returns the channel's icon URL if available or an empty string otherwise."""
|
||||
if self.icon is None:
|
||||
return ""
|
||||
|
||||
return "https://cdn.discordapp.com/channel-icons/{0.id}/{0.icon}.jpg".format(self)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the channel's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
def permissions_for(self, user):
|
||||
"""Handles permission resolution for a :class:`User`.
|
||||
|
||||
This function is there for compatibility with other channel types.
|
||||
|
||||
Actual direct messages do not really have the concept of permissions.
|
||||
|
||||
This returns all the Text related permissions set to true except:
|
||||
|
||||
- send_tts_messages: You cannot send TTS messages in a DM.
|
||||
- manage_messages: You cannot delete others messages in a DM.
|
||||
|
||||
This also checks the kick_members permission if the user is the owner.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user: :class:`User`
|
||||
The user to check permissions for.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Permissions`
|
||||
The resolved permissions for the user.
|
||||
"""
|
||||
|
||||
base = Permissions.text()
|
||||
base.send_tts_messages = False
|
||||
base.manage_messages = False
|
||||
base.mention_everyone = True
|
||||
|
||||
if user.id == self.owner.id:
|
||||
base.kick_members = True
|
||||
|
||||
return base
|
||||
|
||||
async def add_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Adds recipients to this group.
|
||||
|
||||
A group can only have a maximum of 10 members.
|
||||
Attempting to add more ends up in an exception. To
|
||||
add a recipient to the group, you must have a relationship
|
||||
with the user of type :attr:`RelationshipType.friend`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to add to this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Adding a recipient to this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.add_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def remove_recipients(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Removes recipients from this group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients: :class:`User`
|
||||
An argument list of users to remove from this group.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Removing a recipient from this group failed.
|
||||
"""
|
||||
|
||||
# TODO: wait for the corresponding WS event
|
||||
|
||||
req = self._state.http.remove_group_recipient
|
||||
for recipient in recipients:
|
||||
await req(self.id, recipient.id)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: Optional[str]
|
||||
The new name to change the group to.
|
||||
Could be ``None`` to remove the name.
|
||||
icon: Optional[bytes]
|
||||
A :term:`py:bytes-like object` representing the new icon.
|
||||
Could be ``None`` to remove the icon.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the group failed.
|
||||
"""
|
||||
|
||||
try:
|
||||
icon_bytes = fields["icon"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if icon_bytes is not None:
|
||||
fields["icon"] = utils._bytes_to_base64_data(icon_bytes)
|
||||
|
||||
data = await self._state.http.edit_group(self.id, **fields)
|
||||
self._update_group(data)
|
||||
|
||||
async def leave(self):
|
||||
"""|coro|
|
||||
|
||||
Leave the group.
|
||||
|
||||
If you are the only one in the group, this deletes it as well.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Leaving the group failed.
|
||||
"""
|
||||
|
||||
await self._state.http.leave_group(self.id)
|
||||
|
||||
|
||||
def _channel_factory(channel_type):
|
||||
value = try_enum(ChannelType, channel_type)
|
||||
if value is ChannelType.text:
|
||||
return TextChannel, value
|
||||
elif value is ChannelType.voice:
|
||||
return VoiceChannel, value
|
||||
elif value is ChannelType.private:
|
||||
return DMChannel, value
|
||||
elif value is ChannelType.category:
|
||||
return CategoryChannel, value
|
||||
elif value is ChannelType.group:
|
||||
return GroupChannel, value
|
||||
else:
|
||||
return None, value
|
||||
1074
discord/client.py
1074
discord/client.py
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import colorsys
|
||||
|
||||
|
||||
class Colour:
|
||||
"""Represents a Discord role colour. This class is similar
|
||||
to an (red, green, blue) :class:`tuple`.
|
||||
|
||||
There is an alias for this called Color.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two colours are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two colours are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the colour's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the hex format for the colour.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
value: :class:`int`
|
||||
The raw integer colour value.
|
||||
"""
|
||||
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError(
|
||||
"Expected int parameter, received %s instead." % value.__class__.__name__
|
||||
)
|
||||
|
||||
self.value = value
|
||||
|
||||
def _get_byte(self, byte):
|
||||
return (self.value >> (8 * byte)) & 0xFF
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Colour) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return "#{:0>6x}".format(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Colour value=%s>" % self.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
@property
|
||||
def r(self):
|
||||
"""Returns the red component of the colour."""
|
||||
return self._get_byte(2)
|
||||
|
||||
@property
|
||||
def g(self):
|
||||
"""Returns the green component of the colour."""
|
||||
return self._get_byte(1)
|
||||
|
||||
@property
|
||||
def b(self):
|
||||
"""Returns the blue component of the colour."""
|
||||
return self._get_byte(0)
|
||||
|
||||
def to_rgb(self):
|
||||
"""Returns an (r, g, b) tuple representing the colour."""
|
||||
return (self.r, self.g, self.b)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls, r, g, b):
|
||||
"""Constructs a :class:`Colour` from an RGB tuple."""
|
||||
return cls((r << 16) + (g << 8) + b)
|
||||
|
||||
@classmethod
|
||||
def from_hsv(cls, h, s, v):
|
||||
"""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):
|
||||
"""A factory method that returns a :class:`Colour` with a value of 0."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def teal(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
|
||||
return cls(0x1ABC9C)
|
||||
|
||||
@classmethod
|
||||
def dark_teal(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
|
||||
return cls(0x11806A)
|
||||
|
||||
@classmethod
|
||||
def green(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
|
||||
return cls(0x2ECC71)
|
||||
|
||||
@classmethod
|
||||
def dark_green(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
|
||||
return cls(0x1F8B4C)
|
||||
|
||||
@classmethod
|
||||
def blue(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
|
||||
return cls(0x3498DB)
|
||||
|
||||
@classmethod
|
||||
def dark_blue(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
|
||||
return cls(0x206694)
|
||||
|
||||
@classmethod
|
||||
def purple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
|
||||
return cls(0x9B59B6)
|
||||
|
||||
@classmethod
|
||||
def dark_purple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
|
||||
return cls(0x71368A)
|
||||
|
||||
@classmethod
|
||||
def magenta(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
|
||||
return cls(0xE91E63)
|
||||
|
||||
@classmethod
|
||||
def dark_magenta(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
|
||||
return cls(0xAD1457)
|
||||
|
||||
@classmethod
|
||||
def gold(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
|
||||
return cls(0xF1C40F)
|
||||
|
||||
@classmethod
|
||||
def dark_gold(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
|
||||
return cls(0xC27C0E)
|
||||
|
||||
@classmethod
|
||||
def orange(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
|
||||
return cls(0xE67E22)
|
||||
|
||||
@classmethod
|
||||
def dark_orange(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
|
||||
return cls(0xA84300)
|
||||
|
||||
@classmethod
|
||||
def red(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
||||
return cls(0xE74C3C)
|
||||
|
||||
@classmethod
|
||||
def dark_red(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
|
||||
return cls(0x992D22)
|
||||
|
||||
@classmethod
|
||||
def lighter_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
|
||||
return cls(0x95A5A6)
|
||||
|
||||
@classmethod
|
||||
def dark_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
|
||||
return cls(0x607D8B)
|
||||
|
||||
@classmethod
|
||||
def light_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
|
||||
return cls(0x979C9F)
|
||||
|
||||
@classmethod
|
||||
def darker_grey(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
|
||||
return cls(0x546E7A)
|
||||
|
||||
@classmethod
|
||||
def blurple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
|
||||
return cls(0x7289DA)
|
||||
|
||||
@classmethod
|
||||
def greyple(cls):
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
|
||||
return cls(0x99AAB5)
|
||||
|
||||
|
||||
Color = Colour
|
||||
@@ -1,69 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
def _typing_done_callback(fut):
|
||||
# just retrieve any exception and call it a day
|
||||
try:
|
||||
fut.exception()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class Typing:
|
||||
def __init__(self, messageable):
|
||||
self.loop = messageable._state.loop
|
||||
self.messageable = messageable
|
||||
|
||||
async def do_typing(self):
|
||||
try:
|
||||
channel = self._channel
|
||||
except AttributeError:
|
||||
channel = await self.messageable._get_channel()
|
||||
|
||||
typing = channel._state.http.send_typing
|
||||
|
||||
while True:
|
||||
await typing(channel.id)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def __enter__(self):
|
||||
self.task = asyncio.ensure_future(self.do_typing(), loop=self.loop)
|
||||
self.task.add_done_callback(_typing_done_callback)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.task.cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
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):
|
||||
self.task.cancel()
|
||||
@@ -1,492 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from . import utils
|
||||
from .colour import Colour
|
||||
|
||||
|
||||
class _EmptyEmbed:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return "Embed.Empty"
|
||||
|
||||
|
||||
EmptyEmbed = _EmptyEmbed()
|
||||
|
||||
|
||||
class EmbedProxy:
|
||||
def __init__(self, layer):
|
||||
self.__dict__.update(layer)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
return "EmbedProxy(%s)" % ", ".join(
|
||||
("%s=%r" % (k, v) for k, v in self.__dict__.items() if not k.startswith("_"))
|
||||
)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return EmptyEmbed
|
||||
|
||||
|
||||
class Embed:
|
||||
"""Represents a Discord embed.
|
||||
|
||||
The following attributes can be set during creation
|
||||
of the object:
|
||||
|
||||
Certain properties return an ``EmbedProxy``. Which is a type
|
||||
that acts similar to a regular :class:`dict` except access the attributes
|
||||
via dotted access, e.g. ``embed.author.icon_url``. If the attribute
|
||||
is invalid or empty, then a special sentinel value is returned,
|
||||
:attr:`Embed.Empty`.
|
||||
|
||||
For ease of use, all parameters that expect a :class:`str` are implicitly
|
||||
casted to :class:`str` for you.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
title: :class:`str`
|
||||
The title of the embed.
|
||||
type: :class:`str`
|
||||
The type of embed. Usually "rich".
|
||||
description: :class:`str`
|
||||
The description of the embed.
|
||||
url: :class:`str`
|
||||
The URL of the embed.
|
||||
timestamp: `datetime.datetime`
|
||||
The timestamp of the embed content. This could be a naive or aware datetime.
|
||||
colour: :class:`Colour` or :class:`int`
|
||||
The colour code of the embed. Aliased to ``color`` as well.
|
||||
Empty
|
||||
A special sentinel value used by ``EmbedProxy`` and this class
|
||||
to denote that the value or attribute is empty.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"title",
|
||||
"url",
|
||||
"type",
|
||||
"_timestamp",
|
||||
"_colour",
|
||||
"_footer",
|
||||
"_image",
|
||||
"_thumbnail",
|
||||
"_video",
|
||||
"_provider",
|
||||
"_author",
|
||||
"_fields",
|
||||
"description",
|
||||
)
|
||||
|
||||
Empty = EmptyEmbed
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# swap the colour/color aliases
|
||||
try:
|
||||
colour = kwargs["colour"]
|
||||
except KeyError:
|
||||
colour = kwargs.get("color", EmptyEmbed)
|
||||
|
||||
self.colour = colour
|
||||
self.title = kwargs.get("title", EmptyEmbed)
|
||||
self.type = kwargs.get("type", "rich")
|
||||
self.url = kwargs.get("url", EmptyEmbed)
|
||||
self.description = kwargs.get("description", EmptyEmbed)
|
||||
|
||||
try:
|
||||
timestamp = kwargs["timestamp"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
# we are bypassing __init__ here since it doesn't apply here
|
||||
self = cls.__new__(cls)
|
||||
|
||||
# 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)
|
||||
|
||||
# try to fill in the more rich fields
|
||||
|
||||
try:
|
||||
self._colour = Colour(value=data["color"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._timestamp = utils.parse_time(data["timestamp"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for attr in ("thumbnail", "video", "provider", "author", "fields", "image", "footer"):
|
||||
try:
|
||||
value = data[attr]
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
setattr(self, "_" + attr, value)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
return getattr(self, "_colour", EmptyEmbed)
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value):
|
||||
if isinstance(value, (Colour, _EmptyEmbed)):
|
||||
self._colour = value
|
||||
elif isinstance(value, int):
|
||||
self._colour = Colour(value=value)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Expected discord.Colour, int, or Embed.Empty but received %s instead."
|
||||
% value.__class__.__name__
|
||||
)
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return getattr(self, "_timestamp", EmptyEmbed)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, value):
|
||||
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
|
||||
self._timestamp = value
|
||||
else:
|
||||
raise TypeError(
|
||||
"Expected datetime.datetime or Embed.Empty received %s instead"
|
||||
% value.__class__.__name__
|
||||
)
|
||||
|
||||
@property
|
||||
def footer(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the footer contents.
|
||||
|
||||
See :meth:`set_footer` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_footer", {}))
|
||||
|
||||
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
"""Sets the footer for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
text: str
|
||||
The footer text.
|
||||
icon_url: str
|
||||
The URL of the footer icon. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._footer = {}
|
||||
if text is not EmptyEmbed:
|
||||
self._footer["text"] = str(text)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._footer["icon_url"] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
- ``url``
|
||||
- ``proxy_url``
|
||||
- ``width``
|
||||
- ``height``
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_image", {}))
|
||||
|
||||
def set_image(self, *, url):
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
url: str
|
||||
The source URL for the image. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._image = {"url": str(url)}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
|
||||
|
||||
Possible attributes you can access are:
|
||||
|
||||
- ``url``
|
||||
- ``proxy_url``
|
||||
- ``width``
|
||||
- ``height``
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_thumbnail", {}))
|
||||
|
||||
def set_thumbnail(self, *, url):
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
url: str
|
||||
The source URL for the thumbnail. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._thumbnail = {"url": str(url)}
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the video contents.
|
||||
|
||||
Possible attributes include:
|
||||
|
||||
- ``url`` for the video URL.
|
||||
- ``height`` for the video height.
|
||||
- ``width`` for the video width.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_video", {}))
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the provider contents.
|
||||
|
||||
The only attributes that might be accessed are ``name`` and ``url``.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_provider", {}))
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""Returns an ``EmbedProxy`` denoting the author contents.
|
||||
|
||||
See :meth:`set_author` for possible values you can access.
|
||||
|
||||
If the attribute has no value then :attr:`Empty` is returned.
|
||||
"""
|
||||
return EmbedProxy(getattr(self, "_author", {}))
|
||||
|
||||
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
|
||||
"""Sets the author for the embed content.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: str
|
||||
The name of the author.
|
||||
url: str
|
||||
The URL for the author.
|
||||
icon_url: str
|
||||
The URL of the author icon. Only HTTP(S) is supported.
|
||||
"""
|
||||
|
||||
self._author = {"name": str(name)}
|
||||
|
||||
if url is not EmptyEmbed:
|
||||
self._author["url"] = str(url)
|
||||
|
||||
if icon_url is not EmptyEmbed:
|
||||
self._author["icon_url"] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""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", [])]
|
||||
|
||||
def add_field(self, *, name, value, inline=True):
|
||||
"""Adds a field to the embed object.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: str
|
||||
The name of the field.
|
||||
value: str
|
||||
The value of the field.
|
||||
inline: bool
|
||||
Whether the field should be displayed inline.
|
||||
"""
|
||||
|
||||
field = {"inline": inline, "name": str(name), "value": str(value)}
|
||||
|
||||
try:
|
||||
self._fields.append(field)
|
||||
except AttributeError:
|
||||
self._fields = [field]
|
||||
|
||||
return self
|
||||
|
||||
def clear_fields(self):
|
||||
"""Removes all fields from this embed."""
|
||||
try:
|
||||
self._fields.clear()
|
||||
except AttributeError:
|
||||
self._fields = []
|
||||
|
||||
def remove_field(self, index):
|
||||
"""Removes a field at a specified index.
|
||||
|
||||
If the index is invalid or out of bounds then the error is
|
||||
silently swallowed.
|
||||
|
||||
.. note::
|
||||
|
||||
When deleting a field by index, the index of the other fields
|
||||
shift to fill the gap just like a regular list.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
index: int
|
||||
The index of the field to remove.
|
||||
"""
|
||||
try:
|
||||
del self._fields[index]
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
def set_field_at(self, index, *, name, value, inline=True):
|
||||
"""Modifies a field to the embed object.
|
||||
|
||||
The index must point to a valid pre-existing field.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
index: int
|
||||
The index of the field to modify.
|
||||
name: str
|
||||
The name of the field.
|
||||
value: str
|
||||
The value of the field.
|
||||
inline: bool
|
||||
Whether the field should be displayed inline.
|
||||
|
||||
Raises
|
||||
-------
|
||||
IndexError
|
||||
An invalid index was provided.
|
||||
"""
|
||||
|
||||
try:
|
||||
field = self._fields[index]
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
raise IndexError("field index out of range")
|
||||
|
||||
field["name"] = str(name)
|
||||
field["value"] = str(value)
|
||||
field["inline"] = inline
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
"""Converts this embed object into a dict."""
|
||||
|
||||
# add in the raw data into the dict
|
||||
result = {
|
||||
key[1:]: getattr(self, key)
|
||||
for key in self.__slots__
|
||||
if key[0] == "_" and hasattr(self, key)
|
||||
}
|
||||
|
||||
# deal with basic convenience wrappers
|
||||
|
||||
try:
|
||||
colour = result.pop("colour")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if colour:
|
||||
result["color"] = colour.value
|
||||
|
||||
try:
|
||||
timestamp = result.pop("timestamp")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if timestamp:
|
||||
result["timestamp"] = timestamp.isoformat()
|
||||
|
||||
# add in the non raw attribute ones
|
||||
if self.type:
|
||||
result["type"] = self.type
|
||||
|
||||
if self.description:
|
||||
result["description"] = self.description
|
||||
|
||||
if self.url:
|
||||
result["url"] = self.url
|
||||
|
||||
if self.title:
|
||||
result["title"] = self.title
|
||||
|
||||
return result
|
||||
269
discord/emoji.py
269
discord/emoji.py
@@ -1,269 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 collections import namedtuple
|
||||
|
||||
from . import utils
|
||||
from .mixins import Hashable
|
||||
|
||||
|
||||
class PartialEmoji(namedtuple("PartialEmoji", "animated name id")):
|
||||
"""Represents a "partial" emoji.
|
||||
|
||||
This model will be given in two scenarios:
|
||||
|
||||
- "Raw" data events such as :func:`on_raw_reaction_add`
|
||||
- Custom emoji that the bot cannot see from e.g. :attr:`Message.reactions`
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two emoji are the same.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two emoji are not the same.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the emoji's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the emoji rendered for discord.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The custom emoji name, if applicable, or the unicode codepoint
|
||||
of the non-custom emoji.
|
||||
animated: :class:`bool`
|
||||
Whether the emoji is animated or not.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the custom emoji, if applicable.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
if self.animated:
|
||||
return "<a:%s:%s>" % (self.name, self.id)
|
||||
return "<:%s:%s>" % (self.name, self.id)
|
||||
|
||||
def is_custom_emoji(self):
|
||||
"""Checks if this is a custom non-Unicode emoji."""
|
||||
return self.id is not None
|
||||
|
||||
def is_unicode_emoji(self):
|
||||
"""Checks if this is a Unicode emoji."""
|
||||
return self.id is None
|
||||
|
||||
def _as_reaction(self):
|
||||
if self.id is None:
|
||||
return self.name
|
||||
return "%s:%s" % (self.name, self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Returns a URL version of the emoji, if it is custom."""
|
||||
if self.is_unicode_emoji():
|
||||
return None
|
||||
|
||||
_format = "gif" if self.animated else "png"
|
||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
|
||||
|
||||
class Emoji(Hashable):
|
||||
"""Represents a custom emoji.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two emoji are the same.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two emoji are not the same.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the emoji's hash.
|
||||
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(field, value)`` pairs. This allows this class
|
||||
to be used as an iterable in list/dict/etc constructions.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the emoji rendered for discord.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the emoji.
|
||||
id: :class:`int`
|
||||
The emoji's ID.
|
||||
require_colons: :class:`bool`
|
||||
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
|
||||
animated: :class:`bool`
|
||||
Whether an emoji is animated or not.
|
||||
managed: :class:`bool`
|
||||
If this emoji is managed by a Twitch integration.
|
||||
guild_id: :class:`int`
|
||||
The guild ID the emoji belongs to.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"require_colons",
|
||||
"animated",
|
||||
"managed",
|
||||
"id",
|
||||
"name",
|
||||
"_roles",
|
||||
"guild_id",
|
||||
"_state",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild, state, data):
|
||||
self.guild_id = guild.id
|
||||
self._state = 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._roles = utils.SnowflakeList(map(int, emoji.get("roles", [])))
|
||||
|
||||
def _iterator(self):
|
||||
for attr in self.__slots__:
|
||||
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):
|
||||
if self.animated:
|
||||
return "<a:{0.name}:{0.id}>".format(self)
|
||||
return "<:{0.name}:{0.id}>".format(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Emoji id={0.id} name={0.name!r}>".format(self)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the emoji's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Returns a URL version of the emoji."""
|
||||
_format = "gif" if self.animated else "png"
|
||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
|
||||
|
||||
If roles is empty, the emoji is unrestricted.
|
||||
"""
|
||||
guild = self.guild
|
||||
if guild is None:
|
||||
return []
|
||||
|
||||
return [role for role in guild.roles if self._roles.has(role.id)]
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
""":class:`Guild`: The guild this emoji belongs to."""
|
||||
return self._state._get_guild(self.guild_id)
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Deletes the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[str]
|
||||
The reason for deleting this emoji. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to delete emojis.
|
||||
HTTPException
|
||||
An error occurred deleting the emoji.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
||||
|
||||
async def edit(self, *, name, roles=None, reason=None):
|
||||
r"""|coro|
|
||||
|
||||
Edits the custom emoji.
|
||||
|
||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
||||
do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: 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.
|
||||
reason: Optional[str]
|
||||
The reason for editing this emoji. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You are not allowed to edit emojis.
|
||||
HTTPException
|
||||
An error occurred editing the emoji.
|
||||
"""
|
||||
|
||||
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
|
||||
)
|
||||
274
discord/enums.py
274
discord/enums.py
@@ -1,274 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 enum import Enum, IntEnum
|
||||
|
||||
__all__ = [
|
||||
"ChannelType",
|
||||
"MessageType",
|
||||
"VoiceRegion",
|
||||
"VerificationLevel",
|
||||
"ContentFilter",
|
||||
"Status",
|
||||
"DefaultAvatar",
|
||||
"RelationshipType",
|
||||
"AuditLogAction",
|
||||
"AuditLogActionCategory",
|
||||
"UserFlags",
|
||||
"ActivityType",
|
||||
"HypeSquadHouse",
|
||||
"NotificationLevel",
|
||||
]
|
||||
|
||||
|
||||
class ChannelType(Enum):
|
||||
text = 0
|
||||
private = 1
|
||||
voice = 2
|
||||
group = 3
|
||||
category = 4
|
||||
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
vip_us_east = "vip-us-east"
|
||||
vip_us_west = "vip-us-west"
|
||||
vip_amsterdam = "vip-amsterdam"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class VerificationLevel(IntEnum):
|
||||
none = 0
|
||||
low = 1
|
||||
medium = 2
|
||||
high = 3
|
||||
table_flip = 3
|
||||
extreme = 4
|
||||
double_table_flip = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ContentFilter(IntEnum):
|
||||
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"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class DefaultAvatar(Enum):
|
||||
blurple = 0
|
||||
grey = 1
|
||||
gray = 1
|
||||
green = 2
|
||||
orange = 3
|
||||
red = 4
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RelationshipType(Enum):
|
||||
friend = 1
|
||||
blocked = 2
|
||||
incoming_request = 3
|
||||
outgoing_request = 4
|
||||
|
||||
|
||||
class NotificationLevel(IntEnum):
|
||||
all_messages = 0
|
||||
only_mentions = 1
|
||||
|
||||
|
||||
class AuditLogActionCategory(Enum):
|
||||
create = 1
|
||||
delete = 2
|
||||
update = 3
|
||||
|
||||
|
||||
class AuditLogAction(Enum):
|
||||
guild_update = 1
|
||||
channel_create = 10
|
||||
channel_update = 11
|
||||
channel_delete = 12
|
||||
overwrite_create = 13
|
||||
overwrite_update = 14
|
||||
overwrite_delete = 15
|
||||
kick = 20
|
||||
member_prune = 21
|
||||
ban = 22
|
||||
unban = 23
|
||||
member_update = 24
|
||||
member_role_update = 25
|
||||
role_create = 30
|
||||
role_update = 31
|
||||
role_delete = 32
|
||||
invite_create = 40
|
||||
invite_update = 41
|
||||
invite_delete = 42
|
||||
webhook_create = 50
|
||||
webhook_update = 51
|
||||
webhook_delete = 52
|
||||
emoji_create = 60
|
||||
emoji_update = 61
|
||||
emoji_delete = 62
|
||||
message_delete = 72
|
||||
|
||||
@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.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,
|
||||
}
|
||||
return lookup[self]
|
||||
|
||||
@property
|
||||
def target_type(self):
|
||||
v = self.value
|
||||
if v == -1:
|
||||
return "all"
|
||||
elif v < 10:
|
||||
return "guild"
|
||||
elif v < 20:
|
||||
return "channel"
|
||||
elif v < 30:
|
||||
return "user"
|
||||
elif v < 40:
|
||||
return "role"
|
||||
elif v < 50:
|
||||
return "invite"
|
||||
elif v < 60:
|
||||
return "webhook"
|
||||
elif v < 70:
|
||||
return "emoji"
|
||||
elif v < 80:
|
||||
return "message"
|
||||
|
||||
|
||||
class UserFlags(Enum):
|
||||
staff = 1
|
||||
partner = 2
|
||||
hypesquad = 4
|
||||
bug_hunter = 8
|
||||
hypesquad_bravery = 64
|
||||
hypesquad_brilliance = 128
|
||||
hypesquad_balance = 256
|
||||
early_supporter = 512
|
||||
|
||||
|
||||
class ActivityType(IntEnum):
|
||||
unknown = -1
|
||||
playing = 0
|
||||
streaming = 1
|
||||
listening = 2
|
||||
watching = 3
|
||||
|
||||
|
||||
class HypeSquadHouse(Enum):
|
||||
bravery = 1
|
||||
brilliance = 2
|
||||
balance = 3
|
||||
|
||||
|
||||
def try_enum(cls, val):
|
||||
"""A function that tries to turn the value into enum ``cls``.
|
||||
|
||||
If it fails it returns the value instead.
|
||||
"""
|
||||
try:
|
||||
return cls(val)
|
||||
except ValueError:
|
||||
return val
|
||||
@@ -1,183 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
|
||||
class DiscordException(Exception):
|
||||
"""Base exception class for discord.py
|
||||
|
||||
Ideally speaking, this could be caught to handle any exceptions thrown from this library.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ClientException(DiscordException):
|
||||
"""Exception that's thrown 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."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GatewayNotFound(DiscordException):
|
||||
"""An exception that is usually thrown when the gateway hub
|
||||
for the :class:`Client` websocket is not found."""
|
||||
|
||||
def __init__(self):
|
||||
message = "The gateway to connect to discord was not found."
|
||||
super(GatewayNotFound, self).__init__(message)
|
||||
|
||||
|
||||
def flatten_error_dict(d, key=""):
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
new_key = key + "." + k if key else k
|
||||
|
||||
if isinstance(v, dict):
|
||||
try:
|
||||
_errors = v["_errors"]
|
||||
except KeyError:
|
||||
items.extend(flatten_error_dict(v, new_key).items())
|
||||
else:
|
||||
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.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
response: aiohttp.ClientResponse
|
||||
The response of the failed HTTP request. This is an
|
||||
instance of `aiohttp.ClientResponse`__. In some cases
|
||||
this could also be a ``requests.Response``.
|
||||
|
||||
__ http://aiohttp.readthedocs.org/en/stable/client_reference.html#aiohttp.ClientResponse
|
||||
|
||||
text: :class:`str`
|
||||
The text of the error. Could be an empty string.
|
||||
status: :class:`int`
|
||||
The status code of the HTTP request.
|
||||
code: :class:`int`
|
||||
The Discord specific error code for the failure.
|
||||
"""
|
||||
|
||||
def __init__(self, response, message):
|
||||
self.response = response
|
||||
self.status = response.status
|
||||
if isinstance(message, dict):
|
||||
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
|
||||
else:
|
||||
self.text = base
|
||||
else:
|
||||
self.text = message
|
||||
self.code = 0
|
||||
|
||||
fmt = "{0.reason} (status code: {0.status})"
|
||||
if len(self.text):
|
||||
fmt = fmt + ": {1}"
|
||||
|
||||
super().__init__(fmt.format(self.response, self.text))
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""Exception that's thrown for when status code 403 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""Exception that's thrown for when status code 404 occurs.
|
||||
|
||||
Subclass of :exc:`HTTPException`
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidArgument(ClientException):
|
||||
"""Exception that's thrown 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 derived from :exc:`ClientException` and thus
|
||||
:exc:`DiscordException`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LoginFailure(ClientException):
|
||||
"""Exception that's thrown 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
|
||||
closed for reasons that could not be handled internally.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
code: :class:`int`
|
||||
The close code of the websocket.
|
||||
reason: :class:`str`
|
||||
The reason provided for the closure.
|
||||
shard_id: Optional[:class:`int`]
|
||||
The shard ID that got closed if applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, original, *, shard_id):
|
||||
# This exception is just the same exception except
|
||||
# reconfigured to subclass ClientException for users
|
||||
self.code = original.code
|
||||
self.reason = original.reason
|
||||
self.shard_id = shard_id
|
||||
super().__init__(str(original))
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
discord.ext.commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2017 Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
|
||||
from .context import Context
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .formatter import HelpFormatter, Paginator
|
||||
from .converter import *
|
||||
from .cooldowns import *
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import discord.abc
|
||||
import discord.utils
|
||||
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
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
|
||||
the invocation context. This class is not created manually and is instead
|
||||
passed around to commands as the first parameter.
|
||||
|
||||
This class implements the :class:`abc.Messageable` ABC.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message: :class:`discord.Message`
|
||||
The message that triggered the command being executed.
|
||||
bot: :class:`.Bot`
|
||||
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
|
||||
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`
|
||||
The prefix that was used to invoke the command.
|
||||
command
|
||||
The command (i.e. :class:`.Command` or its superclasses) that is being
|
||||
invoked currently.
|
||||
invoked_with: :class:`str`
|
||||
The command name that triggered this invocation. Useful for finding out
|
||||
which alias called the command.
|
||||
invoked_subcommand
|
||||
The subcommand (i.e. :class:`.Command` or its superclasses) that was
|
||||
invoked. If no valid subcommand was invoked then this is equal to
|
||||
`None`.
|
||||
subcommand_passed: Optional[:class:`str`]
|
||||
The string that was attempted to call a subcommand. This does not have
|
||||
to point to a valid registered subcommand and could just point to a
|
||||
nonsense string. If nothing was passed to attempt a call to a
|
||||
subcommand then this is set to `None`.
|
||||
command_failed: :class:`bool`
|
||||
A boolean that indicates if the command failed to be parsed, checked,
|
||||
or invoked.
|
||||
"""
|
||||
|
||||
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_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
|
||||
|
||||
async def invoke(self, *args, **kwargs):
|
||||
r"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
|
||||
This is useful if you want to just call the callback that a
|
||||
:class:`.Command` holds internally.
|
||||
|
||||
Note
|
||||
------
|
||||
You do not pass in the context as it is done for you.
|
||||
|
||||
Warning
|
||||
---------
|
||||
The first parameter passed **must** be the command being invoked.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`.Command`
|
||||
A command or superclass of a command that is going to be called.
|
||||
\*args
|
||||
The arguments to to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
"""
|
||||
|
||||
try:
|
||||
command = args[0]
|
||||
except IndexError:
|
||||
raise TypeError("Missing command to invoke.") from None
|
||||
|
||||
arguments = []
|
||||
if command.instance is not None:
|
||||
arguments.append(command.instance)
|
||||
|
||||
arguments.append(self)
|
||||
arguments.extend(args[1:])
|
||||
|
||||
ret = await command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
|
||||
async def reinvoke(self, *, call_hooks=False, restart=True):
|
||||
"""|coro|
|
||||
|
||||
Calls the command again.
|
||||
|
||||
This is similar to :meth:`~.Context.invoke` except that it bypasses
|
||||
checks, cooldowns, and error handlers.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to bypass :exc:`.UserInputError` derived exceptions,
|
||||
it is recommended to use the regular :meth:`~.Context.invoke`
|
||||
as it will work more naturally. After all, this will end up
|
||||
using the old arguments the user has used and will thus just
|
||||
fail again.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
call_hooks: bool
|
||||
Whether to call the before and after invoke hooks.
|
||||
restart: bool
|
||||
Whether to start the call chain from the very beginning
|
||||
or where we left off (i.e. the command that caused the error).
|
||||
The default is to start where we left off.
|
||||
"""
|
||||
cmd = self.command
|
||||
view = self.view
|
||||
if cmd is None:
|
||||
raise ValueError("This context is not valid.")
|
||||
|
||||
# some state to revert to when we're done
|
||||
index, previous = view.index, view.previous
|
||||
invoked_with = self.invoked_with
|
||||
invoked_subcommand = self.invoked_subcommand
|
||||
subcommand_passed = self.subcommand_passed
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.previous = 0
|
||||
view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
try:
|
||||
await to_call.reinvoke(self, call_hooks=call_hooks)
|
||||
finally:
|
||||
self.command = cmd
|
||||
view.index = index
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""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):
|
||||
return self.channel
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
"""Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
return None
|
||||
return self.command.instance
|
||||
|
||||
@discord.utils.cached_property
|
||||
def guild(self):
|
||||
"""Returns the guild associated with this context's command. None if not available."""
|
||||
return self.message.guild
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
r"""Optional[:class:`VoiceClient`]: A shortcut to :attr:`Guild.voice_client`\, if applicable."""
|
||||
g = self.guild
|
||||
return g.voice_client if g else None
|
||||
@@ -1,560 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import re
|
||||
import inspect
|
||||
|
||||
import discord
|
||||
|
||||
from .errors import BadArgument, NoPrivateMessage
|
||||
|
||||
__all__ = [
|
||||
"Converter",
|
||||
"MemberConverter",
|
||||
"UserConverter",
|
||||
"TextChannelConverter",
|
||||
"InviteConverter",
|
||||
"RoleConverter",
|
||||
"GameConverter",
|
||||
"ColourConverter",
|
||||
"VoiceChannelConverter",
|
||||
"EmojiConverter",
|
||||
"PartialEmojiConverter",
|
||||
"CategoryChannelConverter",
|
||||
"IDConverter",
|
||||
"clean_content",
|
||||
"Greedy",
|
||||
]
|
||||
|
||||
|
||||
def _get_from_guilds(bot, getter, argument):
|
||||
result = None
|
||||
for guild in bot.guilds:
|
||||
result = getattr(guild, getter)(argument)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
class Converter:
|
||||
"""The base class of custom converters that require the :class:`.Context`
|
||||
to be passed to be useful.
|
||||
|
||||
This allows you to implement converters that function similar to the
|
||||
special cased ``discord`` classes.
|
||||
|
||||
Classes that derive from this should override the :meth:`~.Converter.convert`
|
||||
method to do its conversion logic. This method must be a coroutine.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
"""|coro|
|
||||
|
||||
The method to override to do conversion logic.
|
||||
|
||||
If an error is found while converting, it is recommended to
|
||||
raise a :exc:`.CommandError` derived exception as it will
|
||||
properly propagate to the error handlers.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context that the argument is being used in.
|
||||
argument: str
|
||||
The argument that is being converted.
|
||||
"""
|
||||
raise NotImplementedError("Derived classes need to implement this.")
|
||||
|
||||
|
||||
class IDConverter(Converter):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r"([0-9]{15,21})$")
|
||||
super().__init__()
|
||||
|
||||
def _get_id_match(self, argument):
|
||||
return self._id_regex.match(argument)
|
||||
|
||||
|
||||
class MemberConverter(IDConverter):
|
||||
"""Converts to a :class:`Member`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
5. Lookup by nickname
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]+)>$", argument)
|
||||
guild = ctx.guild
|
||||
result = None
|
||||
if match is None:
|
||||
# not a mention...
|
||||
if guild:
|
||||
result = guild.get_member_named(argument)
|
||||
else:
|
||||
result = _get_from_guilds(bot, "get_member_named", argument)
|
||||
else:
|
||||
user_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_member(user_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, "get_member", user_id)
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('Member "{}" not found'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class UserConverter(IDConverter):
|
||||
"""Converts to a :class:`User`.
|
||||
|
||||
All lookups are via the global user cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]+)>$", argument)
|
||||
result = None
|
||||
state = ctx._state
|
||||
|
||||
if match is not None:
|
||||
user_id = int(match.group(1))
|
||||
result = ctx.bot.get_user(user_id)
|
||||
else:
|
||||
arg = argument
|
||||
# check for discriminator if it exists
|
||||
if len(arg) > 5 and arg[-5] == "#":
|
||||
discrim = arg[-4:]
|
||||
name = arg[:-5]
|
||||
predicate = lambda u: u.name == name and u.discriminator == discrim
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
predicate = lambda u: u.name == arg
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('User "{}" not found'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class TextChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`TextChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.text_channels, name=argument)
|
||||
else:
|
||||
|
||||
def check(c):
|
||||
return isinstance(c, discord.TextChannel) and c.name == argument
|
||||
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, "get_channel", channel_id)
|
||||
|
||||
if not isinstance(result, discord.TextChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class VoiceChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`VoiceChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.voice_channels, name=argument)
|
||||
else:
|
||||
|
||||
def check(c):
|
||||
return isinstance(c, discord.VoiceChannel) and c.name == argument
|
||||
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, "get_channel", channel_id)
|
||||
|
||||
if not isinstance(result, discord.VoiceChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`CategoryChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.categories, name=argument)
|
||||
else:
|
||||
|
||||
def check(c):
|
||||
return isinstance(c, discord.CategoryChannel) and c.name == argument
|
||||
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, "get_channel", channel_id)
|
||||
|
||||
if not isinstance(result, discord.CategoryChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ColourConverter(Converter):
|
||||
"""Converts to a :class:`Colour`.
|
||||
|
||||
The following formats are accepted:
|
||||
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- Any of the ``classmethod`` in :class:`Colour`
|
||||
|
||||
- The ``_`` in the name can be optionally replaced with spaces.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
arg = argument.replace("0x", "").lower()
|
||||
|
||||
if arg[0] == "#":
|
||||
arg = arg[1:]
|
||||
try:
|
||||
value = int(arg, base=16)
|
||||
return discord.Colour(value=value)
|
||||
except ValueError:
|
||||
method = getattr(discord.Colour, arg.replace(" ", "_"), None)
|
||||
if method is None or not inspect.ismethod(method):
|
||||
raise BadArgument('Colour "{}" is invalid.'.format(arg))
|
||||
return method()
|
||||
|
||||
|
||||
class RoleConverter(IDConverter):
|
||||
"""Converts to a :class:`Role`.
|
||||
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
guild = ctx.guild
|
||||
if not guild:
|
||||
raise NoPrivateMessage()
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r"<@&([0-9]+)>$", argument)
|
||||
if match:
|
||||
result = guild.get_role(int(match.group(1)))
|
||||
else:
|
||||
result = discord.utils.get(guild._roles.values(), name=argument)
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('Role "{}" not found.'.format(argument))
|
||||
return result
|
||||
|
||||
|
||||
class GameConverter(Converter):
|
||||
"""Converts to :class:`Game`."""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
return discord.Game(name=argument)
|
||||
|
||||
|
||||
class InviteConverter(Converter):
|
||||
"""Converts to a :class:`Invite`.
|
||||
|
||||
This is done via an HTTP request using :meth:`.Bot.get_invite`.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
try:
|
||||
invite = await ctx.bot.get_invite(argument)
|
||||
return invite
|
||||
except Exception as exc:
|
||||
raise BadArgument("Invite is invalid or expired") from exc
|
||||
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
"""Converts to a :class:`Emoji`.
|
||||
|
||||
|
||||
All lookups are done for the local guild first, if available. If that lookup
|
||||
fails, then it checks the client's global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by extracting ID from the emoji.
|
||||
3. Lookup by name
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(
|
||||
r"<a?:[a-zA-Z0-9\_]+:([0-9]+)>$", argument
|
||||
)
|
||||
result = None
|
||||
bot = ctx.bot
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# Try to get the emoji by name. Try local guild first.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, name=argument)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, name=argument)
|
||||
else:
|
||||
emoji_id = int(match.group(1))
|
||||
|
||||
# Try to look up emoji by id.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('Emoji "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PartialEmojiConverter(Converter):
|
||||
"""Converts to a :class:`PartialEmoji`.
|
||||
|
||||
|
||||
This is done by extracting the animated flag, name and ID from the emoji.
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
match = re.match(r"<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$", argument)
|
||||
|
||||
if match:
|
||||
emoji_animated = bool(match.group(1))
|
||||
emoji_name = match.group(2)
|
||||
emoji_id = int(match.group(3))
|
||||
|
||||
return discord.PartialEmoji(animated=emoji_animated, name=emoji_name, id=emoji_id)
|
||||
|
||||
raise BadArgument('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
||||
|
||||
|
||||
class clean_content(Converter):
|
||||
"""Converts the argument to mention scrubbed version of
|
||||
said content.
|
||||
|
||||
This behaves similarly to :attr:`.Message.clean_content`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
fix_channel_mentions: :obj:`bool`
|
||||
Whether to clean channel mentions.
|
||||
use_nicknames: :obj:`bool`
|
||||
Whether to use nicknames when transforming mentions.
|
||||
escape_markdown: :obj:`bool`
|
||||
Whether to also escape special markdown characters.
|
||||
"""
|
||||
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
|
||||
self.fix_channel_mentions = fix_channel_mentions
|
||||
self.use_nicknames = use_nicknames
|
||||
self.escape_markdown = escape_markdown
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
message = ctx.message
|
||||
transformations = {}
|
||||
|
||||
if self.fix_channel_mentions and ctx.guild:
|
||||
|
||||
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
||||
ch = _get(id)
|
||||
return ("<#%s>" % id), ("#" + ch.name if ch else "#deleted-channel")
|
||||
|
||||
transformations.update(
|
||||
resolve_channel(channel) for channel in message.raw_channel_mentions
|
||||
)
|
||||
|
||||
if self.use_nicknames and ctx.guild:
|
||||
|
||||
def resolve_member(id, *, _get=ctx.guild.get_member):
|
||||
m = _get(id)
|
||||
return "@" + m.display_name if m else "@deleted-user"
|
||||
|
||||
else:
|
||||
|
||||
def resolve_member(id, *, _get=ctx.bot.get_user):
|
||||
m = _get(id)
|
||||
return "@" + m.name if m else "@deleted-user"
|
||||
|
||||
transformations.update(
|
||||
("<@%s>" % member_id, resolve_member(member_id)) for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
transformations.update(
|
||||
("<@!%s>" % member_id, resolve_member(member_id)) for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
if ctx.guild:
|
||||
|
||||
def resolve_role(_id, *, _find=ctx.guild.get_role):
|
||||
r = _find(_id)
|
||||
return "@" + r.name if r else "@deleted-role"
|
||||
|
||||
transformations.update(
|
||||
("<@&%s>" % role_id, resolve_role(role_id))
|
||||
for role_id in message.raw_role_mentions
|
||||
)
|
||||
|
||||
def repl(obj):
|
||||
return transformations.get(obj.group(0), "")
|
||||
|
||||
pattern = re.compile("|".join(transformations.keys()))
|
||||
result = pattern.sub(repl, argument)
|
||||
|
||||
if self.escape_markdown:
|
||||
transformations = {re.escape(c): "\\" + c for c in ("*", "`", "_", "~", "\\")}
|
||||
|
||||
def replace(obj):
|
||||
return transformations.get(re.escape(obj.group(0)), "")
|
||||
|
||||
pattern = re.compile("|".join(transformations.keys()))
|
||||
result = pattern.sub(replace, result)
|
||||
|
||||
# Completely ensure no mentions escape:
|
||||
return re.sub(r"@(everyone|here|[!&]?[0-9]{17,21})", "@\u200b\\1", result)
|
||||
|
||||
|
||||
class _Greedy:
|
||||
__slots__ = ("converter",)
|
||||
|
||||
def __init__(self, *, converter=None):
|
||||
self.converter = converter
|
||||
|
||||
def __getitem__(self, params):
|
||||
if not isinstance(params, tuple):
|
||||
params = (params,)
|
||||
if len(params) != 1:
|
||||
raise TypeError("Greedy[...] only takes a single argument")
|
||||
converter = params[0]
|
||||
|
||||
if not inspect.isclass(converter):
|
||||
raise TypeError("Greedy[...] expects a type.")
|
||||
|
||||
if converter is str or converter is type(None) or converter is _Greedy:
|
||||
raise TypeError("Greedy[%s] is invalid." % converter.__name__)
|
||||
|
||||
return self.__class__(converter=converter)
|
||||
|
||||
|
||||
Greedy = _Greedy()
|
||||
@@ -1,148 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import time
|
||||
|
||||
__all__ = ["BucketType", "Cooldown", "CooldownMapping"]
|
||||
|
||||
|
||||
class BucketType(enum.Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
guild = 2
|
||||
channel = 3
|
||||
member = 4
|
||||
category = 5
|
||||
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ("rate", "per", "type", "_window", "_tokens", "_last")
|
||||
|
||||
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
|
||||
|
||||
if not isinstance(self.type, BucketType):
|
||||
raise TypeError("Cooldown type must be a BucketType")
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
if not current:
|
||||
current = time.time()
|
||||
|
||||
tokens = self._tokens
|
||||
|
||||
if current > self._window + self.per:
|
||||
tokens = self.rate
|
||||
return tokens
|
||||
|
||||
def update_rate_limit(self):
|
||||
current = time.time()
|
||||
self._last = current
|
||||
|
||||
self._tokens = self.get_tokens(current)
|
||||
|
||||
# first token used means that we start a new rate limit window
|
||||
if self._tokens == self.rate:
|
||||
self._window = current
|
||||
|
||||
# check if we are rate limited
|
||||
if self._tokens == 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
# 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):
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Cooldown(self.rate, self.per, self.type)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self._cooldown is not None
|
||||
|
||||
@classmethod
|
||||
def from_cooldown(cls, rate, per, type):
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
bucket_type = self._cooldown.type
|
||||
if bucket_type is BucketType.user:
|
||||
return msg.author.id
|
||||
elif bucket_type is BucketType.guild:
|
||||
return (msg.guild or msg.author).id
|
||||
elif bucket_type is BucketType.channel:
|
||||
return msg.channel.id
|
||||
elif bucket_type is BucketType.member:
|
||||
return ((msg.guild and msg.guild.id), msg.author.id)
|
||||
elif bucket_type is BucketType.category:
|
||||
return (msg.channel.category or msg.channel).id
|
||||
|
||||
def _verify_cache_integrity(self):
|
||||
# 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
|
||||
current = time.time()
|
||||
dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per]
|
||||
for k in dead_keys:
|
||||
del self._cache[k]
|
||||
|
||||
def get_bucket(self, message):
|
||||
if self._cooldown.type is BucketType.default:
|
||||
return self._cooldown
|
||||
|
||||
self._verify_cache_integrity()
|
||||
key = self._bucket_key(message)
|
||||
if key not in self._cache:
|
||||
bucket = self._cooldown.copy()
|
||||
self._cache[key] = bucket
|
||||
else:
|
||||
bucket = self._cache[key]
|
||||
|
||||
return bucket
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,279 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 discord.errors import DiscordException
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CommandError",
|
||||
"MissingRequiredArgument",
|
||||
"BadArgument",
|
||||
"NoPrivateMessage",
|
||||
"CheckFailure",
|
||||
"CommandNotFound",
|
||||
"DisabledCommand",
|
||||
"CommandInvokeError",
|
||||
"TooManyArguments",
|
||||
"UserInputError",
|
||||
"CommandOnCooldown",
|
||||
"NotOwner",
|
||||
"MissingPermissions",
|
||||
"BotMissingPermissions",
|
||||
"ConversionError",
|
||||
"BadUnionArgument",
|
||||
]
|
||||
|
||||
|
||||
class CommandError(DiscordException):
|
||||
r"""The base exception type for all command related errors.
|
||||
|
||||
This inherits from :exc:`discord.DiscordException`.
|
||||
|
||||
This exception and exceptions derived from it are handled
|
||||
in a special way as they are caught and passed into a special event
|
||||
from :class:`.Bot`\, :func:`on_command_error`.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, *args):
|
||||
if message is not None:
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere")
|
||||
super().__init__(m, *args)
|
||||
else:
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class ConversionError(CommandError):
|
||||
"""Exception raised when a Converter class raises non-CommandError.
|
||||
|
||||
This inherits from :exc:`.CommandError`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
converter: :class:`discord.ext.commands.Converter`
|
||||
The converter that failed.
|
||||
original
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, converter, original):
|
||||
self.converter = converter
|
||||
self.original = original
|
||||
|
||||
|
||||
class UserInputError(CommandError):
|
||||
"""The base exception type for errors that involve errors
|
||||
regarding user input.
|
||||
|
||||
This inherits from :exc:`.CommandError`.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandNotFound(CommandError):
|
||||
"""Exception raised when a command is attempted to be invoked
|
||||
but no command under that name is found.
|
||||
|
||||
This is not raised for invalid subcommands, rather just the
|
||||
initial main command that is attempted to be invoked.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingRequiredArgument(UserInputError):
|
||||
"""Exception raised when parsing a command and a parameter
|
||||
that is required is not encountered.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
param: :class:`inspect.Parameter`
|
||||
The argument that is missing.
|
||||
"""
|
||||
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
super().__init__("{0.name} is a required argument that is missing.".format(param))
|
||||
|
||||
|
||||
class TooManyArguments(UserInputError):
|
||||
"""Exception raised when the command was passed too many arguments and its
|
||||
:attr:`.Command.ignore_extra` attribute was not set to ``True``.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BadArgument(UserInputError):
|
||||
"""Exception raised when a parsing or conversion failure is encountered
|
||||
on an argument to pass into a command.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CheckFailure(CommandError):
|
||||
"""Exception raised when the predicates in :attr:`.Command.checks` have failed."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoPrivateMessage(CheckFailure):
|
||||
"""Exception raised when an operation does not work in private message
|
||||
contexts.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotOwner(CheckFailure):
|
||||
"""Exception raised when the message author is not the owner of the bot."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DisabledCommand(CommandError):
|
||||
"""Exception raised when the command being invoked is disabled."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandInvokeError(CommandError):
|
||||
"""Exception raised when the command being invoked raised an exception.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, e):
|
||||
self.original = e
|
||||
super().__init__("Command raised an exception: {0.__class__.__name__}: {0}".format(e))
|
||||
|
||||
|
||||
class CommandOnCooldown(CommandError):
|
||||
"""Exception raised when the command being invoked is on cooldown.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
cooldown: Cooldown
|
||||
A class with attributes ``rate``, ``per``, and ``type`` similar to
|
||||
the :func:`.cooldown` decorator.
|
||||
retry_after: :class:`float`
|
||||
The amount of seconds to wait before you can retry again.
|
||||
"""
|
||||
|
||||
def __init__(self, cooldown, retry_after):
|
||||
self.cooldown = cooldown
|
||||
self.retry_after = retry_after
|
||||
super().__init__("You are on cooldown. Try again in {:.2f}s".format(retry_after))
|
||||
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks permissions to run
|
||||
command.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: :class:`list`
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [
|
||||
perm.replace("_", " ").replace("guild", "server").title() for perm in missing_perms
|
||||
]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = "{}, and {}".format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = " and ".join(missing)
|
||||
message = "You are missing {} permission(s) to run command.".format(fmt)
|
||||
super().__init__(message, *args)
|
||||
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
"""Exception raised when the bot lacks permissions to run command.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: :class:`list`
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [
|
||||
perm.replace("_", " ").replace("guild", "server").title() for perm in missing_perms
|
||||
]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = "{}, and {}".format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = " and ".join(missing)
|
||||
message = "Bot requires {} permission(s) to run command.".format(fmt)
|
||||
super().__init__(message, *args)
|
||||
|
||||
|
||||
class BadUnionArgument(UserInputError):
|
||||
"""Exception raised when a :class:`typing.Union` converter fails for all
|
||||
its associated types.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
param: :class:`inspect.Parameter`
|
||||
The parameter that failed being converted.
|
||||
converters: Tuple[Type, ...]
|
||||
A tuple of converters attempted in conversion, in order of failure.
|
||||
errors: List[:class:`CommandError`]
|
||||
A list of errors that were caught from failing the conversion.
|
||||
"""
|
||||
|
||||
def __init__(self, param, converters, errors):
|
||||
self.param = param
|
||||
self.converters = converters
|
||||
self.errors = errors
|
||||
|
||||
def _get_name(x):
|
||||
try:
|
||||
return x.__name__
|
||||
except AttributeError:
|
||||
return x.__class__.__name__
|
||||
|
||||
to_string = [_get_name(x) for x in converters]
|
||||
if len(to_string) > 2:
|
||||
fmt = "{}, or {}".format(", ".join(to_string[:-1]), to_string[-1])
|
||||
else:
|
||||
fmt = " or ".join(to_string)
|
||||
|
||||
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
|
||||
@@ -1,365 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import inspect
|
||||
|
||||
from .core import GroupMixin, Command
|
||||
from .errors import CommandError
|
||||
|
||||
# from discord.iterators import _FilteredAsyncIterator
|
||||
|
||||
# help -> shows info of bot on top/bottom and lists subcommands
|
||||
# help command -> shows detailed info of command
|
||||
# help command <subcommand chain> -> same as above
|
||||
|
||||
# <description>
|
||||
|
||||
# <command signature with aliases>
|
||||
|
||||
# <long doc>
|
||||
|
||||
# Cog:
|
||||
# <command> <shortdoc>
|
||||
# <command> <shortdoc>
|
||||
# Other Cog:
|
||||
# <command> <shortdoc>
|
||||
# No Category:
|
||||
# <command> <shortdoc>
|
||||
|
||||
# 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.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
prefix: :class:`str`
|
||||
The prefix inserted to every page. e.g. three backticks.
|
||||
suffix: :class:`str`
|
||||
The suffix appended at the end of every page. e.g. three backticks.
|
||||
max_size: :class:`int`
|
||||
The maximum amount of codepoints allowed in a page.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix="```", suffix="```", max_size=2000):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size - len(suffix)
|
||||
self._current_page = [prefix]
|
||||
self._count = len(prefix) + 1 # prefix + newline
|
||||
self._pages = []
|
||||
|
||||
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
|
||||
is raised.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
line: str
|
||||
The line to add.
|
||||
empty: bool
|
||||
Indicates if another empty line should be added.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
The line was too big for the current :attr:`max_size`.
|
||||
"""
|
||||
if len(line) > self.max_size - len(self.prefix) - 2:
|
||||
raise RuntimeError(
|
||||
"Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2)
|
||||
)
|
||||
|
||||
if self._count + len(line) + 1 > self.max_size:
|
||||
self.close_page()
|
||||
|
||||
self._count += len(line) + 1
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append("")
|
||||
self._count += 1
|
||||
|
||||
def close_page(self):
|
||||
"""Prematurely terminate a page."""
|
||||
self._current_page.append(self.suffix)
|
||||
self._pages.append("\n".join(self._current_page))
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
"""Returns the rendered list of pages."""
|
||||
# we have more than just the prefix in our current page
|
||||
if len(self._current_page) > 1:
|
||||
self.close_page()
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = "<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>"
|
||||
return fmt.format(self)
|
||||
|
||||
|
||||
class HelpFormatter:
|
||||
"""The default base implementation that handles formatting of the help
|
||||
command.
|
||||
|
||||
To override the behaviour of the formatter, :meth:`~.HelpFormatter.format`
|
||||
should be overridden. A number of utility functions are provided for use
|
||||
inside that method.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
show_hidden: :class:`bool`
|
||||
Dictates if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
show_check_failure: :class:`bool`
|
||||
Dictates if commands that have their :attr:`.Command.checks` failed
|
||||
shown. Defaults to ``False``.
|
||||
width: :class:`int`
|
||||
The maximum number of characters that fit in a line.
|
||||
Defaults to 80.
|
||||
"""
|
||||
|
||||
def __init__(self, show_hidden=False, show_check_failure=False, width=80):
|
||||
self.width = width
|
||||
self.show_hidden = show_hidden
|
||||
self.show_check_failure = show_check_failure
|
||||
|
||||
def has_subcommands(self):
|
||||
""":class:`bool`: Specifies if the command has subcommands."""
|
||||
return isinstance(self.command, GroupMixin)
|
||||
|
||||
def is_bot(self):
|
||||
""":class:`bool`: Specifies if the command being formatted is the bot itself."""
|
||||
return self.command is self.context.bot
|
||||
|
||||
def is_cog(self):
|
||||
""":class:`bool`: Specifies if the command being formatted is actually a cog."""
|
||||
return not self.is_bot() and not isinstance(self.command, Command)
|
||||
|
||||
def shorten(self, text):
|
||||
"""Shortens text to fit into the :attr:`width`."""
|
||||
if len(text) > self.width:
|
||||
return text[: self.width - 3] + "..."
|
||||
return text
|
||||
|
||||
@property
|
||||
def max_name_size(self):
|
||||
""":class:`int`: Returns the largest name length of a command or if it has subcommands
|
||||
the largest subcommand name."""
|
||||
try:
|
||||
commands = (
|
||||
self.command.all_commands if not self.is_cog() else self.context.bot.all_commands
|
||||
)
|
||||
if commands:
|
||||
return max(
|
||||
map(
|
||||
lambda c: len(c.name) if self.show_hidden or not c.hidden else 0,
|
||||
commands.values(),
|
||||
)
|
||||
)
|
||||
return 0
|
||||
except AttributeError:
|
||||
return len(self.command.name)
|
||||
|
||||
@property
|
||||
def clean_prefix(self):
|
||||
"""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.
|
||||
return self.context.prefix.replace(user.mention, "@" + user.display_name)
|
||||
|
||||
def get_command_signature(self):
|
||||
"""Retrieves the signature portion of the help page."""
|
||||
prefix = self.clean_prefix
|
||||
cmd = self.command
|
||||
return prefix + cmd.signature
|
||||
|
||||
def get_ending_note(self):
|
||||
command_name = self.context.invoked_with
|
||||
return (
|
||||
"Type {0}{1} command for more info on a command.\n"
|
||||
"You can also type {0}{1} category for more info on a category.".format(
|
||||
self.clean_prefix, command_name
|
||||
)
|
||||
)
|
||||
|
||||
async def filter_command_list(self):
|
||||
"""Returns a filtered list of commands based on the two attributes
|
||||
provided, :attr:`show_check_failure` and :attr:`show_hidden`.
|
||||
Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid.
|
||||
|
||||
Returns
|
||||
--------
|
||||
iterable
|
||||
An iterable with the filter being applied. The resulting value is
|
||||
a (key, value) :class:`tuple` of the command name and the command itself.
|
||||
"""
|
||||
|
||||
def sane_no_suspension_point_predicate(tup):
|
||||
cmd = tup[1]
|
||||
if self.is_cog():
|
||||
# filter commands that don't exist to this cog.
|
||||
if cmd.instance is not self.command:
|
||||
return False
|
||||
|
||||
if cmd.hidden and not self.show_hidden:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def predicate(tup):
|
||||
if sane_no_suspension_point_predicate(tup) is False:
|
||||
return False
|
||||
|
||||
cmd = tup[1]
|
||||
try:
|
||||
return await cmd.can_run(self.context)
|
||||
except CommandError:
|
||||
return False
|
||||
|
||||
iterator = (
|
||||
self.command.all_commands.items()
|
||||
if not self.is_cog()
|
||||
else self.context.bot.all_commands.items()
|
||||
)
|
||||
if self.show_check_failure:
|
||||
return filter(sane_no_suspension_point_predicate, iterator)
|
||||
|
||||
# Gotta run every check and verify it
|
||||
ret = []
|
||||
for elem in iterator:
|
||||
valid = await predicate(elem)
|
||||
if valid:
|
||||
ret.append(elem)
|
||||
|
||||
return ret
|
||||
|
||||
def _add_subcommands_to_page(self, max_width, commands):
|
||||
for name, command in commands:
|
||||
if name in command.aliases:
|
||||
# skip aliases
|
||||
continue
|
||||
|
||||
entry = " {0:<{width}} {1}".format(name, command.short_doc, width=max_width)
|
||||
shortened = self.shorten(entry)
|
||||
self._paginator.add_line(shortened)
|
||||
|
||||
async def format_help_for(self, context, command_or_bot):
|
||||
"""Formats the help page and handles the actual heavy lifting of how
|
||||
the help command looks like. To change the behaviour, override the
|
||||
:meth:`~.HelpFormatter.format` method.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
context: :class:`.Context`
|
||||
The context of the invoked help command.
|
||||
command_or_bot: :class:`.Command` or :class:`.Bot`
|
||||
The bot or command that we are getting the help of.
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
A paginated output of the help command.
|
||||
"""
|
||||
self.context = context
|
||||
self.command = command_or_bot
|
||||
return await self.format()
|
||||
|
||||
async def format(self):
|
||||
"""Handles the actual behaviour involved with formatting.
|
||||
|
||||
To change the behaviour, this method should be overridden.
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
A paginated output of the help command.
|
||||
"""
|
||||
self._paginator = Paginator()
|
||||
|
||||
# we need a padding of ~80 or so
|
||||
|
||||
description = (
|
||||
self.command.description if not self.is_cog() else inspect.getdoc(self.command)
|
||||
)
|
||||
|
||||
if description:
|
||||
# <description> portion
|
||||
self._paginator.add_line(description, empty=True)
|
||||
|
||||
if isinstance(self.command, Command):
|
||||
# <signature portion>
|
||||
signature = self.get_command_signature()
|
||||
self._paginator.add_line(signature, empty=True)
|
||||
|
||||
# <long doc> section
|
||||
if self.command.help:
|
||||
self._paginator.add_line(self.command.help, empty=True)
|
||||
|
||||
# end it here if it's just a regular command
|
||||
if not self.has_subcommands():
|
||||
self._paginator.close_page()
|
||||
return self._paginator.pages
|
||||
|
||||
max_width = self.max_name_size
|
||||
|
||||
def category(tup):
|
||||
cog = tup[1].cog_name
|
||||
# we insert the zero width space there to give it approximate
|
||||
# last place sorting position.
|
||||
return cog + ":" if cog is not None else "\u200bNo Category:"
|
||||
|
||||
filtered = await self.filter_command_list()
|
||||
if self.is_bot():
|
||||
data = sorted(filtered, key=category)
|
||||
for category, commands in itertools.groupby(data, key=category):
|
||||
# there simply is no prettier way of doing this.
|
||||
commands = sorted(commands)
|
||||
if len(commands) > 0:
|
||||
self._paginator.add_line(category)
|
||||
|
||||
self._add_subcommands_to_page(max_width, commands)
|
||||
else:
|
||||
filtered = sorted(filtered)
|
||||
if filtered:
|
||||
self._paginator.add_line("Commands:")
|
||||
self._add_subcommands_to_page(max_width, filtered)
|
||||
|
||||
# add the ending note
|
||||
self._paginator.add_line()
|
||||
ending_note = self.get_ending_note()
|
||||
self._paginator.add_line(ending_note)
|
||||
return self._paginator.pages
|
||||
@@ -1,201 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 .errors import BadArgument
|
||||
|
||||
|
||||
class StringView:
|
||||
def __init__(self, buffer):
|
||||
self.index = 0
|
||||
self.buffer = buffer
|
||||
self.end = len(buffer)
|
||||
self.previous = 0
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
return None if self.eof else self.buffer[self.index]
|
||||
|
||||
@property
|
||||
def eof(self):
|
||||
return self.index >= self.end
|
||||
|
||||
def undo(self):
|
||||
self.index = self.previous
|
||||
|
||||
def skip_ws(self):
|
||||
pos = 0
|
||||
while not self.eof:
|
||||
try:
|
||||
current = self.buffer[self.index + pos]
|
||||
if not current.isspace():
|
||||
break
|
||||
pos += 1
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
self.previous = self.index
|
||||
self.index += pos
|
||||
return self.previous != self.index
|
||||
|
||||
def skip_string(self, string):
|
||||
strlen = len(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 :]
|
||||
self.previous = self.index
|
||||
self.index = self.end
|
||||
return result
|
||||
|
||||
def read(self, n):
|
||||
result = self.buffer[self.index : self.index + n]
|
||||
self.previous = self.index
|
||||
self.index += n
|
||||
return result
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
result = self.buffer[self.index + 1]
|
||||
except IndexError:
|
||||
result = None
|
||||
|
||||
self.previous = self.index
|
||||
self.index += 1
|
||||
return result
|
||||
|
||||
def get_word(self):
|
||||
pos = 0
|
||||
while not self.eof:
|
||||
try:
|
||||
current = self.buffer[self.index + pos]
|
||||
if current.isspace():
|
||||
break
|
||||
pos += 1
|
||||
except IndexError:
|
||||
break
|
||||
self.previous = self.index
|
||||
result = self.buffer[self.index : self.index + pos]
|
||||
self.index += pos
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
return "<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>".format(
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
# Parser
|
||||
|
||||
# map from opening quotes to closing quotes
|
||||
_quotes = {
|
||||
'"': '"',
|
||||
"‘": "’",
|
||||
"‚": "‛",
|
||||
"“": "”",
|
||||
"„": "‟",
|
||||
"⹂": "⹂",
|
||||
"「": "」",
|
||||
"『": "』",
|
||||
"〝": "〞",
|
||||
"﹁": "﹂",
|
||||
"﹃": "﹄",
|
||||
""": """,
|
||||
"「": "」",
|
||||
"«": "»",
|
||||
"‹": "›",
|
||||
"《": "》",
|
||||
"〈": "〉",
|
||||
}
|
||||
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
|
||||
|
||||
|
||||
def quoted_word(view):
|
||||
current = view.current
|
||||
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
close_quote = _quotes.get(current)
|
||||
is_quoted = bool(close_quote)
|
||||
if is_quoted:
|
||||
result = []
|
||||
_escaped_quotes = (current, close_quote)
|
||||
else:
|
||||
result = [current]
|
||||
_escaped_quotes = _all_quotes
|
||||
|
||||
while not view.eof:
|
||||
current = view.get()
|
||||
if not current:
|
||||
if is_quoted:
|
||||
# unexpected EOF
|
||||
raise BadArgument("Expected closing {}.".format(close_quote))
|
||||
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 == "\\":
|
||||
next_char = view.get()
|
||||
if not next_char:
|
||||
# string ends with \ and no character after it
|
||||
if is_quoted:
|
||||
# if we're quoted then we're expecting a closing quote
|
||||
raise BadArgument("Expected closing {}.".format(close_quote))
|
||||
# if we aren't then we just let it through
|
||||
return "".join(result)
|
||||
|
||||
if next_char in _escaped_quotes:
|
||||
# escaped quote
|
||||
result.append(next_char)
|
||||
else:
|
||||
# different escape character, ignore it
|
||||
view.undo()
|
||||
result.append(current)
|
||||
continue
|
||||
|
||||
if not is_quoted and current in _all_quotes:
|
||||
# we aren't quoted
|
||||
raise BadArgument("Unexpected quote mark in non-quoted string")
|
||||
|
||||
# closing quote
|
||||
if is_quoted and current == close_quote:
|
||||
next_char = view.get()
|
||||
valid_eof = not next_char or next_char.isspace()
|
||||
if not valid_eof:
|
||||
raise BadArgument("Expected space after closing quotation")
|
||||
|
||||
# we're quoted so it's okay
|
||||
return "".join(result)
|
||||
|
||||
if current.isspace() and not is_quoted:
|
||||
# end of word found
|
||||
return "".join(result)
|
||||
|
||||
result.append(current)
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
|
||||
class File:
|
||||
"""A parameter object used for :meth:`abc.Messageable.send`
|
||||
for sending file objects.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
fp: Union[:class:`str`, BinaryIO]
|
||||
A file-like object opened in binary mode and read mode
|
||||
or a filename representing a file in the hard drive to
|
||||
open.
|
||||
|
||||
.. note::
|
||||
|
||||
If the file-like object passed is opened via ``open`` then the
|
||||
modes 'rb' should be used.
|
||||
|
||||
To pass binary data, consider usage of ``io.BytesIO``.
|
||||
|
||||
filename: Optional[:class:`str`]
|
||||
The filename to display when uploading to Discord.
|
||||
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
|
||||
a string then the ``filename`` will default to the string given.
|
||||
spoiler: :class:`bool`
|
||||
Whether the attachment is a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = ("fp", "filename", "_true_fp")
|
||||
|
||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
||||
self.fp = fp
|
||||
self._true_fp = None
|
||||
|
||||
if filename is None:
|
||||
if isinstance(fp, str):
|
||||
_, self.filename = os.path.split(fp)
|
||||
else:
|
||||
self.filename = getattr(fp, "name", None)
|
||||
else:
|
||||
self.filename = filename
|
||||
|
||||
if spoiler and not self.filename.startswith("SPOILER_"):
|
||||
self.filename = "SPOILER_" + self.filename
|
||||
|
||||
def open_file(self):
|
||||
fp = self.fp
|
||||
if isinstance(fp, str):
|
||||
self._true_fp = fp = open(fp, "rb")
|
||||
return fp
|
||||
|
||||
def close(self):
|
||||
if self._true_fp:
|
||||
self._true_fp.close()
|
||||
@@ -1,701 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import logging
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import zlib
|
||||
|
||||
import websockets
|
||||
|
||||
from . import utils
|
||||
from .activity import _ActivityTag
|
||||
from .errors import ConnectionClosed, InvalidArgument
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"DiscordWebSocket",
|
||||
"KeepAliveHandler",
|
||||
"VoiceKeepAliveHandler",
|
||||
"DiscordVoiceWebSocket",
|
||||
"ResumeWebSocket",
|
||||
]
|
||||
|
||||
|
||||
class ResumeWebSocket(Exception):
|
||||
"""Signals to initialise via RESUME opcode instead of IDENTIFY."""
|
||||
|
||||
def __init__(self, shard_id):
|
||||
self.shard_id = shard_id
|
||||
|
||||
|
||||
EventListener = namedtuple("EventListener", "predicate event result future")
|
||||
|
||||
|
||||
class KeepAliveHandler(threading.Thread):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ws = kwargs.pop("ws", None)
|
||||
interval = kwargs.pop("interval", None)
|
||||
shard_id = kwargs.pop("shard_id", None)
|
||||
threading.Thread.__init__(self, *args, **kwargs)
|
||||
self.ws = ws
|
||||
self.interval = interval
|
||||
self.daemon = True
|
||||
self.shard_id = shard_id
|
||||
self.msg = "Keeping websocket alive with sequence %s."
|
||||
self._stop_ev = threading.Event()
|
||||
self._last_ack = time.perf_counter()
|
||||
self._last_send = time.perf_counter()
|
||||
self.latency = float("inf")
|
||||
self.heartbeat_timeout = ws._max_heartbeat_timeout
|
||||
|
||||
def run(self):
|
||||
while not self._stop_ev.wait(self.interval):
|
||||
if self._last_ack + self.heartbeat_timeout < time.perf_counter():
|
||||
log.warning(
|
||||
"Shard ID %s has stopped responding to the gateway. Closing and restarting.",
|
||||
self.shard_id,
|
||||
)
|
||||
coro = self.ws.close(4000)
|
||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
||||
|
||||
try:
|
||||
f.result()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
data = self.get_payload()
|
||||
log.debug(self.msg, data["d"])
|
||||
coro = self.ws.send_as_json(data)
|
||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
||||
try:
|
||||
# block until sending is complete
|
||||
f.result()
|
||||
except Exception:
|
||||
self.stop()
|
||||
else:
|
||||
self._last_send = time.perf_counter()
|
||||
|
||||
def get_payload(self):
|
||||
return {"op": self.ws.HEARTBEAT, "d": self.ws.sequence}
|
||||
|
||||
def stop(self):
|
||||
self._stop_ev.set()
|
||||
|
||||
def ack(self):
|
||||
ack_time = time.perf_counter()
|
||||
self._last_ack = ack_time
|
||||
self.latency = ack_time - self._last_send
|
||||
|
||||
|
||||
class VoiceKeepAliveHandler(KeepAliveHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.msg = "Keeping voice websocket alive with timestamp %s."
|
||||
|
||||
def get_payload(self):
|
||||
return {"op": self.ws.HEARTBEAT, "d": int(time.time() * 1000)}
|
||||
|
||||
|
||||
class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
|
||||
"""Implements a WebSocket for Discord's gateway v6.
|
||||
|
||||
This is created through :func:`create_main_websocket`. Library
|
||||
users should never create this manually.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
DISPATCH
|
||||
Receive only. Denotes an event to be sent to Discord, such as READY.
|
||||
HEARTBEAT
|
||||
When received tells Discord to keep the connection alive.
|
||||
When sent asks if your connection is currently alive.
|
||||
IDENTIFY
|
||||
Send only. Starts a new session.
|
||||
PRESENCE
|
||||
Send only. Updates your presence.
|
||||
VOICE_STATE
|
||||
Send only. Starts a new connection to a voice guild.
|
||||
VOICE_PING
|
||||
Send only. Checks ping time to a voice guild, do not use.
|
||||
RESUME
|
||||
Send only. Resumes an existing connection.
|
||||
RECONNECT
|
||||
Receive only. Tells the client to reconnect to a new gateway.
|
||||
REQUEST_MEMBERS
|
||||
Send only. Asks for the full member list of a guild.
|
||||
INVALIDATE_SESSION
|
||||
Receive only. Tells the client to optionally invalidate the session
|
||||
and IDENTIFY again.
|
||||
HELLO
|
||||
Receive only. Tells the client the heartbeat interval.
|
||||
HEARTBEAT_ACK
|
||||
Receive only. Confirms receiving of a heartbeat. Not having it implies
|
||||
a connection issue.
|
||||
GUILD_SYNC
|
||||
Send only. Requests a guild sync.
|
||||
gateway
|
||||
The gateway we are currently connected to.
|
||||
token
|
||||
The authentication token for discord.
|
||||
"""
|
||||
|
||||
DISPATCH = 0
|
||||
HEARTBEAT = 1
|
||||
IDENTIFY = 2
|
||||
PRESENCE = 3
|
||||
VOICE_STATE = 4
|
||||
VOICE_PING = 5
|
||||
RESUME = 6
|
||||
RECONNECT = 7
|
||||
REQUEST_MEMBERS = 8
|
||||
INVALIDATE_SESSION = 9
|
||||
HELLO = 10
|
||||
HEARTBEAT_ACK = 11
|
||||
GUILD_SYNC = 12
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.max_size = None
|
||||
# an empty dispatcher to prevent crashes
|
||||
self._dispatch = lambda *args: None
|
||||
# generic event listeners
|
||||
self._dispatch_listeners = []
|
||||
# the keep alive
|
||||
self._keep_alive = None
|
||||
|
||||
# ws related stuff
|
||||
self.session_id = None
|
||||
self.sequence = None
|
||||
self._zlib = zlib.decompressobj()
|
||||
self._buffer = bytearray()
|
||||
|
||||
@classmethod
|
||||
async def from_client(
|
||||
cls, client, *, shard_id=None, session=None, sequence=None, resume=False
|
||||
):
|
||||
"""Creates a main websocket for Discord from a :class:`Client`.
|
||||
|
||||
This is for internal use only.
|
||||
"""
|
||||
gateway = await client.http.get_gateway()
|
||||
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
|
||||
|
||||
# dynamically add attributes needed
|
||||
ws.token = client.http.token
|
||||
ws._connection = client._connection
|
||||
ws._dispatch = client.dispatch
|
||||
ws.gateway = gateway
|
||||
ws.shard_id = shard_id
|
||||
ws.shard_count = client._connection.shard_count
|
||||
ws.session_id = session
|
||||
ws.sequence = sequence
|
||||
ws._max_heartbeat_timeout = client._connection.heartbeat_timeout
|
||||
|
||||
client._connection._update_references(ws)
|
||||
|
||||
log.info("Created websocket connected to %s", gateway)
|
||||
|
||||
# poll event for OP Hello
|
||||
await ws.poll_event()
|
||||
|
||||
if not resume:
|
||||
await ws.identify()
|
||||
return ws
|
||||
|
||||
await ws.resume()
|
||||
try:
|
||||
await ws.ensure_open()
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
# ws got closed so let's just do a regular IDENTIFY connect.
|
||||
log.info(
|
||||
"RESUME failed (the websocket decided to close) for Shard ID %s. Retrying.",
|
||||
shard_id,
|
||||
)
|
||||
return await cls.from_client(client, shard_id=shard_id)
|
||||
else:
|
||||
return ws
|
||||
|
||||
def wait_for(self, event, predicate, result=None):
|
||||
"""Waits for a DISPATCH'd event that meets the predicate.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
event : str
|
||||
The event name in all upper case to wait for.
|
||||
predicate
|
||||
A function that takes a data parameter to check for event
|
||||
properties. The data parameter is the 'd' key in the JSON message.
|
||||
result
|
||||
A function that takes the same data parameter and executes to send
|
||||
the result to the future. If None, returns the data.
|
||||
|
||||
Returns
|
||||
--------
|
||||
asyncio.Future
|
||||
A future to wait for.
|
||||
"""
|
||||
|
||||
future = self.loop.create_future()
|
||||
entry = EventListener(event=event, predicate=predicate, result=result, future=future)
|
||||
self._dispatch_listeners.append(entry)
|
||||
return future
|
||||
|
||||
async def identify(self):
|
||||
"""Sends the IDENTIFY packet."""
|
||||
payload = {
|
||||
"op": self.IDENTIFY,
|
||||
"d": {
|
||||
"token": self.token,
|
||||
"properties": {
|
||||
"$os": sys.platform,
|
||||
"$browser": "discord.py",
|
||||
"$device": "discord.py",
|
||||
"$referrer": "",
|
||||
"$referring_domain": "",
|
||||
},
|
||||
"compress": True,
|
||||
"large_threshold": 250,
|
||||
"v": 3,
|
||||
},
|
||||
}
|
||||
|
||||
if not self._connection.is_bot:
|
||||
payload["d"]["synced_guilds"] = []
|
||||
|
||||
if self.shard_id is not None and self.shard_count is not None:
|
||||
payload["d"]["shard"] = [self.shard_id, self.shard_count]
|
||||
|
||||
state = self._connection
|
||||
if state._activity is not None or state._status is not None:
|
||||
payload["d"]["presence"] = {
|
||||
"status": state._status,
|
||||
"game": state._activity,
|
||||
"since": 0,
|
||||
"afk": False,
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
log.info("Shard ID %s has sent the IDENTIFY payload.", self.shard_id)
|
||||
|
||||
async def resume(self):
|
||||
"""Sends the RESUME packet."""
|
||||
payload = {
|
||||
"op": self.RESUME,
|
||||
"d": {"seq": self.sequence, "session_id": self.session_id, "token": self.token},
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
log.info("Shard ID %s has sent the RESUME payload.", self.shard_id)
|
||||
|
||||
async def received_message(self, msg):
|
||||
self._dispatch("socket_raw_receive", msg)
|
||||
|
||||
if type(msg) is bytes:
|
||||
self._buffer.extend(msg)
|
||||
|
||||
if len(msg) >= 4:
|
||||
if msg[-4:] == b"\x00\x00\xff\xff":
|
||||
msg = self._zlib.decompress(self._buffer)
|
||||
msg = msg.decode("utf-8")
|
||||
self._buffer = bytearray()
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
msg = json.loads(msg)
|
||||
|
||||
log.debug("For Shard ID %s: WebSocket Event: %s", self.shard_id, msg)
|
||||
self._dispatch("socket_response", msg)
|
||||
|
||||
op = msg.get("op")
|
||||
data = msg.get("d")
|
||||
seq = msg.get("s")
|
||||
if seq is not None:
|
||||
self.sequence = seq
|
||||
|
||||
if op != self.DISPATCH:
|
||||
if op == self.RECONNECT:
|
||||
# "reconnect" can only be handled by the Client
|
||||
# so we terminate our connection and raise an
|
||||
# internal exception signalling to reconnect.
|
||||
log.info("Received RECONNECT opcode.")
|
||||
await self.close()
|
||||
raise ResumeWebSocket(self.shard_id)
|
||||
|
||||
if op == self.HEARTBEAT_ACK:
|
||||
self._keep_alive.ack()
|
||||
return
|
||||
|
||||
if op == self.HEARTBEAT:
|
||||
beat = self._keep_alive.get_payload()
|
||||
await self.send_as_json(beat)
|
||||
return
|
||||
|
||||
if op == self.HELLO:
|
||||
interval = data["heartbeat_interval"] / 1000.0
|
||||
self._keep_alive = KeepAliveHandler(
|
||||
ws=self, interval=interval, shard_id=self.shard_id
|
||||
)
|
||||
# send a heartbeat immediately
|
||||
await self.send_as_json(self._keep_alive.get_payload())
|
||||
self._keep_alive.start()
|
||||
return
|
||||
|
||||
if op == self.INVALIDATE_SESSION:
|
||||
if data is True:
|
||||
await asyncio.sleep(5.0, loop=self.loop)
|
||||
await self.close()
|
||||
raise ResumeWebSocket(self.shard_id)
|
||||
|
||||
self.sequence = None
|
||||
self.session_id = None
|
||||
log.info("Shard ID %s session has been invalidated.", self.shard_id)
|
||||
await self.identify()
|
||||
return
|
||||
|
||||
log.warning("Unknown OP code %s.", op)
|
||||
return
|
||||
|
||||
event = msg.get("t")
|
||||
|
||||
if event == "READY":
|
||||
self._trace = trace = data.get("_trace", [])
|
||||
self.sequence = msg["s"]
|
||||
self.session_id = data["session_id"]
|
||||
log.info(
|
||||
"Shard ID %s has connected to Gateway: %s (Session ID: %s).",
|
||||
self.shard_id,
|
||||
", ".join(trace),
|
||||
self.session_id,
|
||||
)
|
||||
|
||||
elif event == "RESUMED":
|
||||
self._trace = trace = data.get("_trace", [])
|
||||
log.info(
|
||||
"Shard ID %s has successfully RESUMED session %s under trace %s.",
|
||||
self.shard_id,
|
||||
self.session_id,
|
||||
", ".join(trace),
|
||||
)
|
||||
|
||||
parser = "parse_" + event.lower()
|
||||
|
||||
try:
|
||||
func = getattr(self._connection, parser)
|
||||
except AttributeError:
|
||||
log.warning("Unknown event %s.", event)
|
||||
else:
|
||||
func(data)
|
||||
|
||||
# remove the dispatched listeners
|
||||
removed = []
|
||||
for index, entry in enumerate(self._dispatch_listeners):
|
||||
if entry.event != event:
|
||||
continue
|
||||
|
||||
future = entry.future
|
||||
if future.cancelled():
|
||||
removed.append(index)
|
||||
continue
|
||||
|
||||
try:
|
||||
valid = entry.predicate(data)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
removed.append(index)
|
||||
else:
|
||||
if valid:
|
||||
ret = data if entry.result is None else entry.result(data)
|
||||
future.set_result(ret)
|
||||
removed.append(index)
|
||||
|
||||
for index in reversed(removed):
|
||||
del self._dispatch_listeners[index]
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":obj:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
|
||||
heartbeat = self._keep_alive
|
||||
return float("inf") if heartbeat is None else heartbeat.latency
|
||||
|
||||
def _can_handle_close(self, code):
|
||||
return code not in (1000, 4004, 4010, 4011)
|
||||
|
||||
async def poll_event(self):
|
||||
"""Polls for a DISPATCH event and handles the general gateway loop.
|
||||
|
||||
Raises
|
||||
------
|
||||
ConnectionClosed
|
||||
The websocket connection was terminated for unhandled reasons.
|
||||
"""
|
||||
try:
|
||||
msg = await self.recv()
|
||||
await self.received_message(msg)
|
||||
except websockets.exceptions.ConnectionClosed as exc:
|
||||
if self._can_handle_close(exc.code):
|
||||
log.info(
|
||||
"Websocket closed with %s (%s), attempting a reconnect.", exc.code, exc.reason
|
||||
)
|
||||
raise ResumeWebSocket(self.shard_id) from exc
|
||||
else:
|
||||
log.info("Websocket closed with %s (%s), cannot reconnect.", exc.code, exc.reason)
|
||||
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
|
||||
|
||||
async def send(self, data):
|
||||
self._dispatch("socket_raw_send", data)
|
||||
await super().send(data)
|
||||
|
||||
async def send_as_json(self, data):
|
||||
try:
|
||||
await super().send(utils.to_json(data))
|
||||
except websockets.exceptions.ConnectionClosed as exc:
|
||||
if not self._can_handle_close(exc.code):
|
||||
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
|
||||
|
||||
async def change_presence(self, *, activity=None, status=None, afk=False, since=0.0):
|
||||
if activity is not None:
|
||||
if not isinstance(activity, _ActivityTag):
|
||||
raise InvalidArgument("activity must be one of Game, Streaming, or Activity.")
|
||||
activity = activity.to_dict()
|
||||
|
||||
if status == "idle":
|
||||
since = int(time.time() * 1000)
|
||||
|
||||
payload = {
|
||||
"op": self.PRESENCE,
|
||||
"d": {"game": activity, "afk": afk, "since": since, "status": status},
|
||||
}
|
||||
|
||||
sent = utils.to_json(payload)
|
||||
log.debug('Sending "%s" to change status', sent)
|
||||
await self.send(sent)
|
||||
|
||||
async def request_sync(self, guild_ids):
|
||||
payload = {"op": self.GUILD_SYNC, "d": list(guild_ids)}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False):
|
||||
payload = {
|
||||
"op": self.VOICE_STATE,
|
||||
"d": {
|
||||
"guild_id": guild_id,
|
||||
"channel_id": channel_id,
|
||||
"self_mute": self_mute,
|
||||
"self_deaf": self_deaf,
|
||||
},
|
||||
}
|
||||
|
||||
log.debug("Updating our voice state to %s.", payload)
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def close(self, code=1000, reason=""):
|
||||
if self._keep_alive:
|
||||
self._keep_alive.stop()
|
||||
|
||||
await super().close(code, reason)
|
||||
|
||||
async def close_connection(self, *args, **kwargs):
|
||||
if self._keep_alive:
|
||||
self._keep_alive.stop()
|
||||
|
||||
await super().close_connection(*args, **kwargs)
|
||||
|
||||
|
||||
class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
|
||||
"""Implements the websocket protocol for handling voice connections.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
IDENTIFY
|
||||
Send only. Starts a new voice session.
|
||||
SELECT_PROTOCOL
|
||||
Send only. Tells discord what encryption mode and how to connect for voice.
|
||||
READY
|
||||
Receive only. Tells the websocket that the initial connection has completed.
|
||||
HEARTBEAT
|
||||
Send only. Keeps your websocket connection alive.
|
||||
SESSION_DESCRIPTION
|
||||
Receive only. Gives you the secret key required for voice.
|
||||
SPEAKING
|
||||
Send only. Notifies the client if you are currently speaking.
|
||||
HEARTBEAT_ACK
|
||||
Receive only. Tells you your heartbeat has been acknowledged.
|
||||
RESUME
|
||||
Sent only. Tells the client to resume its session.
|
||||
HELLO
|
||||
Receive only. Tells you that your websocket connection was acknowledged.
|
||||
INVALIDATE_SESSION
|
||||
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
|
||||
"""
|
||||
|
||||
IDENTIFY = 0
|
||||
SELECT_PROTOCOL = 1
|
||||
READY = 2
|
||||
HEARTBEAT = 3
|
||||
SESSION_DESCRIPTION = 4
|
||||
SPEAKING = 5
|
||||
HEARTBEAT_ACK = 6
|
||||
RESUME = 7
|
||||
HELLO = 8
|
||||
INVALIDATE_SESSION = 9
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.max_size = None
|
||||
self._keep_alive = None
|
||||
|
||||
async def send_as_json(self, data):
|
||||
log.debug("Sending voice websocket frame: %s.", data)
|
||||
await self.send(utils.to_json(data))
|
||||
|
||||
async def resume(self):
|
||||
state = self._connection
|
||||
payload = {
|
||||
"op": self.RESUME,
|
||||
"d": {
|
||||
"token": state.token,
|
||||
"server_id": str(state.server_id),
|
||||
"session_id": state.session_id,
|
||||
},
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def identify(self):
|
||||
state = self._connection
|
||||
payload = {
|
||||
"op": self.IDENTIFY,
|
||||
"d": {
|
||||
"server_id": str(state.server_id),
|
||||
"user_id": str(state.user.id),
|
||||
"session_id": state.session_id,
|
||||
"token": state.token,
|
||||
},
|
||||
}
|
||||
await self.send_as_json(payload)
|
||||
|
||||
@classmethod
|
||||
async def from_client(cls, client, *, resume=False):
|
||||
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
||||
gateway = "wss://" + client.endpoint + "/?v=3"
|
||||
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
|
||||
ws.gateway = gateway
|
||||
ws._connection = client
|
||||
ws._max_heartbeat_timeout = 60.0
|
||||
|
||||
if resume:
|
||||
await ws.resume()
|
||||
else:
|
||||
await ws.identify()
|
||||
|
||||
return ws
|
||||
|
||||
async def select_protocol(self, ip, port):
|
||||
payload = {
|
||||
"op": self.SELECT_PROTOCOL,
|
||||
"d": {
|
||||
"protocol": "udp",
|
||||
"data": {"address": ip, "port": port, "mode": "xsalsa20_poly1305"},
|
||||
},
|
||||
}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def speak(self, is_speaking=True):
|
||||
payload = {"op": self.SPEAKING, "d": {"speaking": is_speaking, "delay": 0}}
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def received_message(self, msg):
|
||||
log.debug("Voice websocket frame received: %s", msg)
|
||||
op = msg["op"]
|
||||
data = msg.get("d")
|
||||
|
||||
if op == self.READY:
|
||||
interval = data["heartbeat_interval"] / 1000.0
|
||||
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
|
||||
self._keep_alive.start()
|
||||
await self.initial_connection(data)
|
||||
elif op == self.HEARTBEAT_ACK:
|
||||
self._keep_alive.ack()
|
||||
elif op == self.INVALIDATE_SESSION:
|
||||
log.info("Voice RESUME failed.")
|
||||
await self.identify()
|
||||
elif op == self.SESSION_DESCRIPTION:
|
||||
await self.load_secret_key(data)
|
||||
|
||||
async def initial_connection(self, data):
|
||||
state = self._connection
|
||||
state.ssrc = data["ssrc"]
|
||||
state.voice_port = data["port"]
|
||||
|
||||
packet = bytearray(70)
|
||||
struct.pack_into(">I", packet, 0, state.ssrc)
|
||||
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
|
||||
recv = await self.loop.sock_recv(state.socket, 70)
|
||||
log.debug("received packet in initial_connection: %s", recv)
|
||||
|
||||
# the ip is ascii starting at the 4th byte and ending at the first null
|
||||
ip_start = 4
|
||||
ip_end = recv.index(0, ip_start)
|
||||
state.ip = recv[ip_start:ip_end].decode("ascii")
|
||||
|
||||
# the port is a little endian unsigned short in the last two bytes
|
||||
# yes, this is different endianness from everything else
|
||||
state.port = struct.unpack_from("<H", recv, len(recv) - 2)[0]
|
||||
|
||||
log.debug("detected ip: %s port: %s", state.ip, state.port)
|
||||
await self.select_protocol(state.ip, state.port)
|
||||
log.info("selected the voice protocol for use")
|
||||
|
||||
async def load_secret_key(self, data):
|
||||
log.info("received secret key for voice connection")
|
||||
self._connection.secret_key = data.get("secret_key")
|
||||
await self.speak()
|
||||
|
||||
async def poll_event(self):
|
||||
try:
|
||||
msg = await asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
|
||||
await self.received_message(json.loads(msg))
|
||||
except websockets.exceptions.ConnectionClosed as exc:
|
||||
raise ConnectionClosed(exc, shard_id=None) from exc
|
||||
|
||||
async def close_connection(self, *args, **kwargs):
|
||||
if self._keep_alive:
|
||||
self._keep_alive.stop()
|
||||
|
||||
await super().close_connection(*args, **kwargs)
|
||||
1419
discord/guild.py
1419
discord/guild.py
File diff suppressed because it is too large
Load Diff
911
discord/http.py
911
discord/http.py
@@ -1,911 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from urllib.parse import quote as _uriquote
|
||||
import weakref
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .errors import HTTPException, Forbidden, NotFound, LoginFailure, GatewayNotFound
|
||||
from . import __version__, utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def json_or_text(response):
|
||||
text = await response.text(encoding="utf-8")
|
||||
if response.headers["content-type"] == "application/json":
|
||||
return json.loads(text)
|
||||
return text
|
||||
|
||||
|
||||
class Route:
|
||||
BASE = "https://discordapp.com/api/v7"
|
||||
|
||||
def __init__(self, method, path, **parameters):
|
||||
self.path = path
|
||||
self.method = method
|
||||
url = self.BASE + self.path
|
||||
if parameters:
|
||||
self.url = url.format(
|
||||
**{k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()}
|
||||
)
|
||||
else:
|
||||
self.url = url
|
||||
|
||||
# major parameters:
|
||||
self.channel_id = parameters.get("channel_id")
|
||||
self.guild_id = parameters.get("guild_id")
|
||||
|
||||
@property
|
||||
def bucket(self):
|
||||
# the bucket is just method + path w/ major parameters
|
||||
return "{0.method}:{0.channel_id}:{0.guild_id}:{0.path}".format(self)
|
||||
|
||||
|
||||
class MaybeUnlock:
|
||||
def __init__(self, lock):
|
||||
self.lock = lock
|
||||
self._unlock = True
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def defer(self):
|
||||
self._unlock = False
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self._unlock:
|
||||
self.lock.release()
|
||||
|
||||
|
||||
class HTTPClient:
|
||||
"""Represents an HTTP client sending HTTP requests to the Discord API."""
|
||||
|
||||
SUCCESS_LOG = "{method} {url} has received {text}"
|
||||
REQUEST_LOG = "{method} {url} with {json} has returned {status}"
|
||||
|
||||
def __init__(self, connector=None, *, proxy=None, proxy_auth=None, loop=None):
|
||||
self.loop = asyncio.get_event_loop() if loop is None else loop
|
||||
self.connector = connector
|
||||
self._session = aiohttp.ClientSession(connector=connector, loop=self.loop)
|
||||
self._locks = weakref.WeakValueDictionary()
|
||||
self._global_over = asyncio.Event(loop=self.loop)
|
||||
self._global_over.set()
|
||||
self.token = None
|
||||
self.bot_token = False
|
||||
self.proxy = proxy
|
||||
self.proxy_auth = proxy_auth
|
||||
|
||||
user_agent = "DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
|
||||
self.user_agent = user_agent.format(__version__, sys.version_info, aiohttp.__version__)
|
||||
|
||||
def recreate(self):
|
||||
if self._session.closed:
|
||||
self._session = aiohttp.ClientSession(connector=self.connector, loop=self.loop)
|
||||
|
||||
async def request(self, route, *, header_bypass_delay=None, **kwargs):
|
||||
bucket = route.bucket
|
||||
method = route.method
|
||||
url = route.url
|
||||
|
||||
lock = self._locks.get(bucket)
|
||||
if lock is None:
|
||||
lock = asyncio.Lock(loop=self.loop)
|
||||
if bucket is not None:
|
||||
self._locks[bucket] = lock
|
||||
|
||||
# header creation
|
||||
headers = {"User-Agent": self.user_agent}
|
||||
|
||||
if self.token is not None:
|
||||
headers["Authorization"] = "Bot " + self.token if self.bot_token else self.token
|
||||
# some checking if it's a JSON request
|
||||
if "json" in kwargs:
|
||||
headers["Content-Type"] = "application/json"
|
||||
kwargs["data"] = utils.to_json(kwargs.pop("json"))
|
||||
|
||||
try:
|
||||
reason = kwargs.pop("reason")
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if reason:
|
||||
headers["X-Audit-Log-Reason"] = _uriquote(reason, safe="/ ")
|
||||
|
||||
kwargs["headers"] = headers
|
||||
|
||||
# Proxy support
|
||||
if self.proxy is not None:
|
||||
kwargs["proxy"] = self.proxy
|
||||
if self.proxy_auth is not None:
|
||||
kwargs["proxy_auth"] = self.proxy_auth
|
||||
|
||||
if not self._global_over.is_set():
|
||||
# wait until the global lock is complete
|
||||
await self._global_over.wait()
|
||||
|
||||
await lock
|
||||
with MaybeUnlock(lock) as maybe_lock:
|
||||
for tries in range(5):
|
||||
async with self._session.request(method, url, **kwargs) as r:
|
||||
log.debug(
|
||||
"%s %s with %s has returned %s", method, url, kwargs.get("data"), r.status
|
||||
)
|
||||
|
||||
# even errors have text involved in them so this is safe to call
|
||||
data = await json_or_text(r)
|
||||
|
||||
# check if we have rate limit header information
|
||||
remaining = r.headers.get("X-Ratelimit-Remaining")
|
||||
if remaining == "0" and r.status != 429:
|
||||
# we've depleted our current bucket
|
||||
if header_bypass_delay is None:
|
||||
delta = utils._parse_ratelimit_header(r)
|
||||
else:
|
||||
delta = header_bypass_delay
|
||||
|
||||
log.debug(
|
||||
"A rate limit bucket has been exhausted (bucket: %s, retry: %s).",
|
||||
bucket,
|
||||
delta,
|
||||
)
|
||||
maybe_lock.defer()
|
||||
self.loop.call_later(delta, lock.release)
|
||||
|
||||
# the request was successful so just return the text/json
|
||||
if 300 > r.status >= 200:
|
||||
log.debug("%s %s has received %s", method, url, data)
|
||||
return data
|
||||
|
||||
# we are being rate limited
|
||||
if r.status == 429:
|
||||
fmt = 'We are being rate limited. Retrying in %.2f seconds. Handled under the bucket "%s"'
|
||||
|
||||
# sleep a bit
|
||||
retry_after = data["retry_after"] / 1000.0
|
||||
log.warning(fmt, retry_after, bucket)
|
||||
|
||||
# check if it's a global rate limit
|
||||
is_global = data.get("global", False)
|
||||
if is_global:
|
||||
log.warning(
|
||||
"Global rate limit has been hit. Retrying in %.2f seconds.",
|
||||
retry_after,
|
||||
)
|
||||
self._global_over.clear()
|
||||
|
||||
await asyncio.sleep(retry_after, loop=self.loop)
|
||||
log.debug("Done sleeping for the rate limit. Retrying...")
|
||||
|
||||
# release the global lock now that the
|
||||
# global rate limit has passed
|
||||
if is_global:
|
||||
self._global_over.set()
|
||||
log.debug("Global rate limit is now over.")
|
||||
|
||||
continue
|
||||
|
||||
# we've received a 500 or 502, unconditional retry
|
||||
if r.status in {500, 502}:
|
||||
await asyncio.sleep(1 + tries * 2, loop=self.loop)
|
||||
continue
|
||||
|
||||
# the usual error cases
|
||||
if r.status == 403:
|
||||
raise Forbidden(r, data)
|
||||
elif r.status == 404:
|
||||
raise NotFound(r, data)
|
||||
else:
|
||||
raise HTTPException(r, data)
|
||||
|
||||
# We've run out of retries, raise.
|
||||
raise HTTPException(r, data)
|
||||
|
||||
async def get_attachment(self, url):
|
||||
async with self._session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.read()
|
||||
elif resp.status == 404:
|
||||
raise NotFound(resp, "attachment not found")
|
||||
elif resp.status == 403:
|
||||
raise Forbidden(resp, "cannot retrieve attachment")
|
||||
else:
|
||||
raise HTTPException(resp, "failed to get attachment")
|
||||
|
||||
# state management
|
||||
|
||||
async def close(self):
|
||||
await self._session.close()
|
||||
|
||||
def _token(self, token, *, bot=True):
|
||||
self.token = token
|
||||
self.bot_token = bot
|
||||
self._ack_token = None
|
||||
|
||||
# login management
|
||||
|
||||
async def static_login(self, token, *, bot):
|
||||
old_token, old_bot = self.token, self.bot_token
|
||||
self._token(token, bot=bot)
|
||||
|
||||
try:
|
||||
data = await self.request(Route("GET", "/users/@me"))
|
||||
except HTTPException as exc:
|
||||
self._token(old_token, bot=old_bot)
|
||||
if exc.response.status == 401:
|
||||
raise LoginFailure("Improper token has been passed.") from exc
|
||||
raise
|
||||
|
||||
return data
|
||||
|
||||
def logout(self):
|
||||
return self.request(Route("POST", "/auth/logout"))
|
||||
|
||||
# Group functionality
|
||||
|
||||
def start_group(self, user_id, recipients):
|
||||
payload = {"recipients": recipients}
|
||||
|
||||
return self.request(
|
||||
Route("POST", "/users/{user_id}/channels", user_id=user_id), json=payload
|
||||
)
|
||||
|
||||
def leave_group(self, channel_id):
|
||||
return self.request(Route("DELETE", "/channels/{channel_id}", channel_id=channel_id))
|
||||
|
||||
def add_group_recipient(self, channel_id, user_id):
|
||||
r = Route(
|
||||
"PUT",
|
||||
"/channels/{channel_id}/recipients/{user_id}",
|
||||
channel_id=channel_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return self.request(r)
|
||||
|
||||
def remove_group_recipient(self, channel_id, user_id):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/recipients/{user_id}",
|
||||
channel_id=channel_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return self.request(r)
|
||||
|
||||
def edit_group(self, channel_id, **options):
|
||||
valid_keys = ("name", "icon")
|
||||
payload = {k: v for k, v in options.items() if k in valid_keys}
|
||||
|
||||
return self.request(
|
||||
Route("PATCH", "/channels/{channel_id}", channel_id=channel_id), json=payload
|
||||
)
|
||||
|
||||
def convert_group(self, channel_id):
|
||||
return self.request(Route("POST", "/channels/{channel_id}/convert", channel_id=channel_id))
|
||||
|
||||
# Message management
|
||||
|
||||
def start_private_message(self, user_id):
|
||||
payload = {"recipient_id": user_id}
|
||||
|
||||
return self.request(Route("POST", "/users/@me/channels"), json=payload)
|
||||
|
||||
def send_message(self, channel_id, content, *, tts=False, embed=None, nonce=None):
|
||||
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
|
||||
payload = {}
|
||||
|
||||
if content:
|
||||
payload["content"] = content
|
||||
|
||||
if tts:
|
||||
payload["tts"] = True
|
||||
|
||||
if embed:
|
||||
payload["embed"] = embed
|
||||
|
||||
if nonce:
|
||||
payload["nonce"] = nonce
|
||||
|
||||
return self.request(r, json=payload)
|
||||
|
||||
def send_typing(self, channel_id):
|
||||
return self.request(Route("POST", "/channels/{channel_id}/typing", channel_id=channel_id))
|
||||
|
||||
def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, nonce=None):
|
||||
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
|
||||
form = aiohttp.FormData()
|
||||
|
||||
payload = {"tts": tts}
|
||||
if content:
|
||||
payload["content"] = content
|
||||
if embed:
|
||||
payload["embed"] = embed
|
||||
if nonce:
|
||||
payload["nonce"] = nonce
|
||||
|
||||
form.add_field("payload_json", utils.to_json(payload))
|
||||
if len(files) == 1:
|
||||
fp = files[0]
|
||||
form.add_field("file", fp[0], filename=fp[1], content_type="application/octet-stream")
|
||||
else:
|
||||
for index, (buffer, filename) in enumerate(files):
|
||||
form.add_field(
|
||||
"file%s" % index,
|
||||
buffer,
|
||||
filename=filename,
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
|
||||
return self.request(r, data=form)
|
||||
|
||||
async def ack_message(self, channel_id, message_id):
|
||||
r = Route(
|
||||
"POST",
|
||||
"/channels/{channel_id}/messages/{message_id}/ack",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
data = await self.request(r, json={"token": self._ack_token})
|
||||
self._ack_token = data["token"]
|
||||
|
||||
def ack_guild(self, guild_id):
|
||||
return self.request(Route("POST", "/guilds/{guild_id}/ack", guild_id=guild_id))
|
||||
|
||||
def delete_message(self, channel_id, message_id, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/messages/{message_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def delete_messages(self, channel_id, message_ids, *, reason=None):
|
||||
r = Route("POST", "/channels/{channel_id}/messages/bulk_delete", channel_id=channel_id)
|
||||
payload = {"messages": message_ids}
|
||||
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def edit_message(self, message_id, channel_id, **fields):
|
||||
r = Route(
|
||||
"PATCH",
|
||||
"/channels/{channel_id}/messages/{message_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
return self.request(r, json=fields)
|
||||
|
||||
def add_reaction(self, message_id, channel_id, emoji):
|
||||
r = Route(
|
||||
"PUT",
|
||||
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
emoji=emoji,
|
||||
)
|
||||
return self.request(r, header_bypass_delay=0.25)
|
||||
|
||||
def remove_reaction(self, message_id, channel_id, emoji, member_id):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
member_id=member_id,
|
||||
emoji=emoji,
|
||||
)
|
||||
return self.request(r, header_bypass_delay=0.25)
|
||||
|
||||
def remove_own_reaction(self, message_id, channel_id, emoji):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
emoji=emoji,
|
||||
)
|
||||
return self.request(r, header_bypass_delay=0.25)
|
||||
|
||||
def get_reaction_users(self, message_id, channel_id, emoji, limit, after=None):
|
||||
r = Route(
|
||||
"GET",
|
||||
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
emoji=emoji,
|
||||
)
|
||||
|
||||
params = {"limit": limit}
|
||||
if after:
|
||||
params["after"] = after
|
||||
return self.request(r, params=params)
|
||||
|
||||
def clear_reactions(self, message_id, channel_id):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/messages/{message_id}/reactions",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
return self.request(r)
|
||||
|
||||
def get_message(self, channel_id, message_id):
|
||||
r = Route(
|
||||
"GET",
|
||||
"/channels/{channel_id}/messages/{message_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
return self.request(r)
|
||||
|
||||
def logs_from(self, channel_id, limit, before=None, after=None, around=None):
|
||||
params = {"limit": limit}
|
||||
|
||||
if before:
|
||||
params["before"] = before
|
||||
if after:
|
||||
params["after"] = after
|
||||
if around:
|
||||
params["around"] = around
|
||||
|
||||
return self.request(
|
||||
Route("GET", "/channels/{channel_id}/messages", channel_id=channel_id), params=params
|
||||
)
|
||||
|
||||
def pin_message(self, channel_id, message_id):
|
||||
return self.request(
|
||||
Route(
|
||||
"PUT",
|
||||
"/channels/{channel_id}/pins/{message_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
)
|
||||
|
||||
def unpin_message(self, channel_id, message_id):
|
||||
return self.request(
|
||||
Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/pins/{message_id}",
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
)
|
||||
)
|
||||
|
||||
def pins_from(self, channel_id):
|
||||
return self.request(Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id))
|
||||
|
||||
# Member management
|
||||
|
||||
def kick(self, user_id, guild_id, reason=None):
|
||||
r = Route(
|
||||
"DELETE", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
|
||||
)
|
||||
if reason:
|
||||
# thanks aiohttp
|
||||
r.url = "{0.url}?reason={1}".format(r, _uriquote(reason))
|
||||
|
||||
return self.request(r)
|
||||
|
||||
def ban(self, user_id, guild_id, delete_message_days=1, reason=None):
|
||||
r = Route("PUT", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id)
|
||||
params = {"delete-message-days": delete_message_days}
|
||||
|
||||
if reason:
|
||||
# thanks aiohttp
|
||||
r.url = "{0.url}?reason={1}".format(r, _uriquote(reason))
|
||||
|
||||
return self.request(r, params=params)
|
||||
|
||||
def unban(self, user_id, guild_id, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None, reason=None):
|
||||
r = Route(
|
||||
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
|
||||
)
|
||||
payload = {}
|
||||
if mute is not None:
|
||||
payload["mute"] = mute
|
||||
|
||||
if deafen is not None:
|
||||
payload["deaf"] = deafen
|
||||
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def edit_profile(self, password, username, avatar, **fields):
|
||||
payload = {"password": password, "username": username, "avatar": avatar}
|
||||
|
||||
if "email" in fields:
|
||||
payload["email"] = fields["email"]
|
||||
|
||||
if "new_password" in fields:
|
||||
payload["new_password"] = fields["new_password"]
|
||||
|
||||
return self.request(Route("PATCH", "/users/@me"), json=payload)
|
||||
|
||||
def change_my_nickname(self, guild_id, nickname, *, reason=None):
|
||||
r = Route("PATCH", "/guilds/{guild_id}/members/@me/nick", guild_id=guild_id)
|
||||
payload = {"nick": nickname}
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def change_nickname(self, guild_id, user_id, nickname, *, reason=None):
|
||||
r = Route(
|
||||
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
|
||||
)
|
||||
payload = {"nick": nickname}
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def edit_member(self, guild_id, user_id, *, reason=None, **fields):
|
||||
r = Route(
|
||||
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
|
||||
)
|
||||
return self.request(r, json=fields, reason=reason)
|
||||
|
||||
# Channel management
|
||||
|
||||
def edit_channel(self, channel_id, *, reason=None, **options):
|
||||
r = Route("PATCH", "/channels/{channel_id}", channel_id=channel_id)
|
||||
valid_keys = (
|
||||
"name",
|
||||
"parent_id",
|
||||
"topic",
|
||||
"bitrate",
|
||||
"nsfw",
|
||||
"user_limit",
|
||||
"position",
|
||||
"permission_overwrites",
|
||||
"rate_limit_per_user",
|
||||
)
|
||||
payload = {k: v for k, v in options.items() if k in valid_keys}
|
||||
|
||||
return self.request(r, reason=reason, json=payload)
|
||||
|
||||
def bulk_channel_update(self, guild_id, data, *, reason=None):
|
||||
r = Route("PATCH", "/guilds/{guild_id}/channels", guild_id=guild_id)
|
||||
return self.request(r, json=data, reason=reason)
|
||||
|
||||
def create_channel(
|
||||
self,
|
||||
guild_id,
|
||||
name,
|
||||
channel_type,
|
||||
parent_id=None,
|
||||
permission_overwrites=None,
|
||||
*,
|
||||
reason=None
|
||||
):
|
||||
payload = {"name": name, "type": channel_type}
|
||||
|
||||
if permission_overwrites is not None:
|
||||
payload["permission_overwrites"] = permission_overwrites
|
||||
|
||||
if parent_id is not None:
|
||||
payload["parent_id"] = parent_id
|
||||
|
||||
return self.request(
|
||||
Route("POST", "/guilds/{guild_id}/channels", guild_id=guild_id),
|
||||
json=payload,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
def delete_channel(self, channel_id, *, reason=None):
|
||||
return self.request(
|
||||
Route("DELETE", "/channels/{channel_id}", channel_id=channel_id), reason=reason
|
||||
)
|
||||
|
||||
# Webhook management
|
||||
|
||||
def create_webhook(self, channel_id, *, name, avatar=None):
|
||||
payload = {"name": name}
|
||||
if avatar is not None:
|
||||
payload["avatar"] = avatar
|
||||
|
||||
return self.request(
|
||||
Route("POST", "/channels/{channel_id}/webhooks", channel_id=channel_id), json=payload
|
||||
)
|
||||
|
||||
def channel_webhooks(self, channel_id):
|
||||
return self.request(Route("GET", "/channels/{channel_id}/webhooks", channel_id=channel_id))
|
||||
|
||||
def guild_webhooks(self, guild_id):
|
||||
return self.request(Route("GET", "/guilds/{guild_id}/webhooks", guild_id=guild_id))
|
||||
|
||||
def get_webhook(self, webhook_id):
|
||||
return self.request(Route("GET", "/webhooks/{webhook_id}", webhook_id=webhook_id))
|
||||
|
||||
# Guild management
|
||||
|
||||
def leave_guild(self, guild_id):
|
||||
return self.request(Route("DELETE", "/users/@me/guilds/{guild_id}", guild_id=guild_id))
|
||||
|
||||
def delete_guild(self, guild_id):
|
||||
return self.request(Route("DELETE", "/guilds/{guild_id}", guild_id=guild_id))
|
||||
|
||||
def create_guild(self, name, region, icon):
|
||||
payload = {"name": name, "icon": icon, "region": region}
|
||||
|
||||
return self.request(Route("POST", "/guilds"), json=payload)
|
||||
|
||||
def edit_guild(self, guild_id, *, reason=None, **fields):
|
||||
valid_keys = (
|
||||
"name",
|
||||
"region",
|
||||
"icon",
|
||||
"afk_timeout",
|
||||
"owner_id",
|
||||
"afk_channel_id",
|
||||
"splash",
|
||||
"verification_level",
|
||||
"system_channel_id",
|
||||
"default_message_notifications",
|
||||
"explicit_content_filter",
|
||||
)
|
||||
|
||||
payload = {k: v for k, v in fields.items() if k in valid_keys}
|
||||
|
||||
return self.request(
|
||||
Route("PATCH", "/guilds/{guild_id}", guild_id=guild_id), json=payload, reason=reason
|
||||
)
|
||||
|
||||
def get_bans(self, guild_id):
|
||||
return self.request(Route("GET", "/guilds/{guild_id}/bans", guild_id=guild_id))
|
||||
|
||||
def get_ban(self, user_id, guild_id):
|
||||
return self.request(
|
||||
Route("GET", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id)
|
||||
)
|
||||
|
||||
def get_vanity_code(self, guild_id):
|
||||
return self.request(Route("GET", "/guilds/{guild_id}/vanity-url", guild_id=guild_id))
|
||||
|
||||
def change_vanity_code(self, guild_id, code, *, reason=None):
|
||||
payload = {"code": code}
|
||||
return self.request(
|
||||
Route("PATCH", "/guilds/{guild_id}/vanity-url", guild_id=guild_id),
|
||||
json=payload,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
def prune_members(self, guild_id, days, *, reason=None):
|
||||
params = {"days": days}
|
||||
return self.request(
|
||||
Route("POST", "/guilds/{guild_id}/prune", guild_id=guild_id),
|
||||
params=params,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
def estimate_pruned_members(self, guild_id, days):
|
||||
params = {"days": days}
|
||||
return self.request(
|
||||
Route("GET", "/guilds/{guild_id}/prune", guild_id=guild_id), params=params
|
||||
)
|
||||
|
||||
def create_custom_emoji(self, guild_id, name, image, *, roles=None, reason=None):
|
||||
payload = {"name": name, "image": image, "roles": roles or []}
|
||||
|
||||
r = Route("POST", "/guilds/{guild_id}/emojis", guild_id=guild_id)
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def delete_custom_emoji(self, guild_id, emoji_id, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def edit_custom_emoji(self, guild_id, emoji_id, *, name, roles=None, reason=None):
|
||||
payload = {"name": name, "roles": roles or []}
|
||||
r = Route(
|
||||
"PATCH", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id
|
||||
)
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def get_audit_logs(
|
||||
self, guild_id, limit=100, before=None, after=None, user_id=None, action_type=None
|
||||
):
|
||||
params = {"limit": limit}
|
||||
if before:
|
||||
params["before"] = before
|
||||
if after:
|
||||
params["after"] = after
|
||||
if user_id:
|
||||
params["user_id"] = user_id
|
||||
if action_type:
|
||||
params["action_type"] = action_type
|
||||
|
||||
r = Route("GET", "/guilds/{guild_id}/audit-logs", guild_id=guild_id)
|
||||
return self.request(r, params=params)
|
||||
|
||||
# Invite management
|
||||
|
||||
def create_invite(self, channel_id, *, reason=None, **options):
|
||||
r = Route("POST", "/channels/{channel_id}/invites", channel_id=channel_id)
|
||||
payload = {
|
||||
"max_age": options.get("max_age", 0),
|
||||
"max_uses": options.get("max_uses", 0),
|
||||
"temporary": options.get("temporary", False),
|
||||
"unique": options.get("unique", True),
|
||||
}
|
||||
|
||||
return self.request(r, reason=reason, json=payload)
|
||||
|
||||
def get_invite(self, invite_id):
|
||||
return self.request(Route("GET", "/invite/{invite_id}", invite_id=invite_id))
|
||||
|
||||
def invites_from(self, guild_id):
|
||||
return self.request(Route("GET", "/guilds/{guild_id}/invites", guild_id=guild_id))
|
||||
|
||||
def invites_from_channel(self, channel_id):
|
||||
return self.request(Route("GET", "/channels/{channel_id}/invites", channel_id=channel_id))
|
||||
|
||||
def delete_invite(self, invite_id, *, reason=None):
|
||||
return self.request(
|
||||
Route("DELETE", "/invite/{invite_id}", invite_id=invite_id), reason=reason
|
||||
)
|
||||
|
||||
# Role management
|
||||
|
||||
def edit_role(self, guild_id, role_id, *, reason=None, **fields):
|
||||
r = Route(
|
||||
"PATCH", "/guilds/{guild_id}/roles/{role_id}", guild_id=guild_id, role_id=role_id
|
||||
)
|
||||
valid_keys = ("name", "permissions", "color", "hoist", "mentionable")
|
||||
payload = {k: v for k, v in fields.items() if k in valid_keys}
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def delete_role(self, guild_id, role_id, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE", "/guilds/{guild_id}/roles/{role_id}", guild_id=guild_id, role_id=role_id
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def replace_roles(self, user_id, guild_id, role_ids, *, reason=None):
|
||||
return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids, reason=reason)
|
||||
|
||||
def create_role(self, guild_id, *, reason=None, **fields):
|
||||
r = Route("POST", "/guilds/{guild_id}/roles", guild_id=guild_id)
|
||||
return self.request(r, json=fields, reason=reason)
|
||||
|
||||
def move_role_position(self, guild_id, positions, *, reason=None):
|
||||
r = Route("PATCH", "/guilds/{guild_id}/roles", guild_id=guild_id)
|
||||
return self.request(r, json=positions, reason=reason)
|
||||
|
||||
def add_role(self, guild_id, user_id, role_id, *, reason=None):
|
||||
r = Route(
|
||||
"PUT",
|
||||
"/guilds/{guild_id}/members/{user_id}/roles/{role_id}",
|
||||
guild_id=guild_id,
|
||||
user_id=user_id,
|
||||
role_id=role_id,
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def remove_role(self, guild_id, user_id, role_id, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/guilds/{guild_id}/members/{user_id}/roles/{role_id}",
|
||||
guild_id=guild_id,
|
||||
user_id=user_id,
|
||||
role_id=role_id,
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
def edit_channel_permissions(self, channel_id, target, allow, deny, type, *, reason=None):
|
||||
payload = {"id": target, "allow": allow, "deny": deny, "type": type}
|
||||
r = Route(
|
||||
"PUT",
|
||||
"/channels/{channel_id}/permissions/{target}",
|
||||
channel_id=channel_id,
|
||||
target=target,
|
||||
)
|
||||
return self.request(r, json=payload, reason=reason)
|
||||
|
||||
def delete_channel_permissions(self, channel_id, target, *, reason=None):
|
||||
r = Route(
|
||||
"DELETE",
|
||||
"/channels/{channel_id}/permissions/{target}",
|
||||
channel_id=channel_id,
|
||||
target=target,
|
||||
)
|
||||
return self.request(r, reason=reason)
|
||||
|
||||
# Voice management
|
||||
|
||||
def move_member(self, user_id, guild_id, channel_id, *, reason=None):
|
||||
return self.edit_member(
|
||||
guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason
|
||||
)
|
||||
|
||||
# Relationship related
|
||||
|
||||
def remove_relationship(self, user_id):
|
||||
r = Route("DELETE", "/users/@me/relationships/{user_id}", user_id=user_id)
|
||||
return self.request(r)
|
||||
|
||||
def add_relationship(self, user_id, type=None):
|
||||
r = Route("PUT", "/users/@me/relationships/{user_id}", user_id=user_id)
|
||||
payload = {}
|
||||
if type is not None:
|
||||
payload["type"] = type
|
||||
|
||||
return self.request(r, json=payload)
|
||||
|
||||
def send_friend_request(self, username, discriminator):
|
||||
r = Route("POST", "/users/@me/relationships")
|
||||
payload = {"username": username, "discriminator": int(discriminator)}
|
||||
return self.request(r, json=payload)
|
||||
|
||||
# Misc
|
||||
|
||||
def application_info(self):
|
||||
return self.request(Route("GET", "/oauth2/applications/@me"))
|
||||
|
||||
async def get_gateway(self, *, encoding="json", v=6, zlib=True):
|
||||
try:
|
||||
data = await self.request(Route("GET", "/gateway"))
|
||||
except HTTPException as exc:
|
||||
raise GatewayNotFound() from exc
|
||||
if zlib:
|
||||
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
|
||||
else:
|
||||
value = "{0}?encoding={1}&v={2}"
|
||||
return value.format(data["url"], encoding, v)
|
||||
|
||||
async def get_bot_gateway(self, *, encoding="json", v=6, zlib=True):
|
||||
try:
|
||||
data = await self.request(Route("GET", "/gateway/bot"))
|
||||
except HTTPException as exc:
|
||||
raise GatewayNotFound() from exc
|
||||
|
||||
if zlib:
|
||||
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
|
||||
else:
|
||||
value = "{0}?encoding={1}&v={2}"
|
||||
return data["shards"], value.format(data["url"], encoding, v)
|
||||
|
||||
def get_user_info(self, user_id):
|
||||
return self.request(Route("GET", "/users/{user_id}", user_id=user_id))
|
||||
|
||||
def get_user_profile(self, user_id):
|
||||
return self.request(Route("GET", "/users/{user_id}/profile", user_id=user_id))
|
||||
|
||||
def get_mutual_friends(self, user_id):
|
||||
return self.request(Route("GET", "/users/{user_id}/relationships", user_id=user_id))
|
||||
|
||||
def change_hypesquad_house(self, house_id):
|
||||
payload = {"house_id": house_id}
|
||||
return self.request(Route("POST", "/hypesquad/online"), json=payload)
|
||||
|
||||
def leave_hypesquad_house(self):
|
||||
return self.request(Route("DELETE", "/hypesquad/online"))
|
||||
@@ -1,176 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 .utils import parse_time
|
||||
from .mixins import Hashable
|
||||
from .object import Object
|
||||
|
||||
|
||||
class Invite(Hashable):
|
||||
"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two invites are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two invites are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the invite hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the invite URL.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
max_age: :class:`int`
|
||||
How long the 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.
|
||||
guild: :class:`Guild`
|
||||
The guild the invite is for.
|
||||
revoked: :class:`bool`
|
||||
Indicates if the invite has been revoked.
|
||||
created_at: `datetime.datetime`
|
||||
A datetime object denoting the time the invite was created.
|
||||
temporary: :class:`bool`
|
||||
Indicates that the invite grants temporary membership.
|
||||
If True, members who joined via this invite will be kicked upon disconnect.
|
||||
uses: :class:`int`
|
||||
How many times the invite has been used.
|
||||
max_uses: :class:`int`
|
||||
How many times the invite can be used.
|
||||
inviter: :class:`User`
|
||||
The user who created the invite.
|
||||
channel: :class:`abc.GuildChannel`
|
||||
The channel the invite is for.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"max_age",
|
||||
"code",
|
||||
"guild",
|
||||
"revoked",
|
||||
"created_at",
|
||||
"uses",
|
||||
"temporary",
|
||||
"max_uses",
|
||||
"inviter",
|
||||
"channel",
|
||||
"_state",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
def from_incomplete(cls, *, state, data):
|
||||
guild_id = int(data["guild"]["id"])
|
||||
channel_id = int(data["channel"]["id"])
|
||||
guild = state._get_guild(guild_id)
|
||||
if guild is not None:
|
||||
channel = guild.get_channel(channel_id)
|
||||
else:
|
||||
guild = Object(id=guild_id)
|
||||
channel = Object(id=channel_id)
|
||||
guild.name = data["guild"]["name"]
|
||||
|
||||
guild.splash = data["guild"]["splash"]
|
||||
guild.splash_url = ""
|
||||
if guild.splash:
|
||||
guild.splash_url = "https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048".format(
|
||||
guild
|
||||
)
|
||||
|
||||
channel.name = data["channel"]["name"]
|
||||
|
||||
data["guild"] = guild
|
||||
data["channel"] = channel
|
||||
return cls(state=state, data=data)
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
def __repr__(self):
|
||||
return "<Invite code={0.code!r}>".format(self)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.code)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Returns the proper code portion of the invite."""
|
||||
return self.code
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""A property that retrieves the invite URL."""
|
||||
return "http://discord.gg/" + self.code
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Revokes the instant invite.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_channels` permission to do this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[str]
|
||||
The reason for deleting this invite. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to revoke invites.
|
||||
NotFound
|
||||
The invite is invalid or expired.
|
||||
HTTPException
|
||||
Revoking the invite failed.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_invite(self.code, reason=reason)
|
||||
@@ -1,489 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
from .errors import NoMoreItems
|
||||
from .utils import time_snowflake, maybe_coroutine
|
||||
from .object import Object
|
||||
from .audit_logs import AuditLogEntry
|
||||
|
||||
|
||||
class _AsyncIterator:
|
||||
__slots__ = ()
|
||||
|
||||
def get(self, **attrs):
|
||||
def predicate(elem):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split("__")
|
||||
obj = elem
|
||||
for attribute in nested:
|
||||
obj = getattr(obj, attribute)
|
||||
|
||||
if obj != val:
|
||||
return False
|
||||
return True
|
||||
|
||||
return self.find(predicate)
|
||||
|
||||
async def find(self, predicate):
|
||||
while True:
|
||||
try:
|
||||
elem = await self.next()
|
||||
except NoMoreItems:
|
||||
return None
|
||||
|
||||
ret = await maybe_coroutine(predicate, elem)
|
||||
if ret:
|
||||
return elem
|
||||
|
||||
def map(self, func):
|
||||
return _MappedAsyncIterator(self, func)
|
||||
|
||||
def filter(self, predicate):
|
||||
return _FilteredAsyncIterator(self, predicate)
|
||||
|
||||
async def flatten(self):
|
||||
ret = []
|
||||
while True:
|
||||
try:
|
||||
item = await self.next()
|
||||
except NoMoreItems:
|
||||
return ret
|
||||
else:
|
||||
ret.append(item)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
msg = await self.next()
|
||||
except NoMoreItems:
|
||||
raise StopAsyncIteration()
|
||||
else:
|
||||
return msg
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
|
||||
class _MappedAsyncIterator(_AsyncIterator):
|
||||
def __init__(self, iterator, func):
|
||||
self.iterator = iterator
|
||||
self.func = func
|
||||
|
||||
async def next(self):
|
||||
# this raises NoMoreItems and will propagate appropriately
|
||||
item = await self.iterator.next()
|
||||
return await maybe_coroutine(self.func, item)
|
||||
|
||||
|
||||
class _FilteredAsyncIterator(_AsyncIterator):
|
||||
def __init__(self, iterator, predicate):
|
||||
self.iterator = iterator
|
||||
|
||||
if predicate is None:
|
||||
predicate = _identity
|
||||
|
||||
self.predicate = predicate
|
||||
|
||||
async def next(self):
|
||||
getter = self.iterator.next
|
||||
pred = self.predicate
|
||||
while True:
|
||||
# propagate NoMoreItems similar to _MappedAsyncIterator
|
||||
item = await getter()
|
||||
ret = await maybe_coroutine(pred, item)
|
||||
if ret:
|
||||
return item
|
||||
|
||||
|
||||
class ReactionIterator(_AsyncIterator):
|
||||
def __init__(self, message, emoji, limit=100, after=None):
|
||||
self.message = message
|
||||
self.limit = limit
|
||||
self.after = after
|
||||
state = message._state
|
||||
self.getter = state.http.get_reaction_users
|
||||
self.state = state
|
||||
self.emoji = emoji
|
||||
self.guild = message.guild
|
||||
self.channel_id = message.channel.id
|
||||
self.users = asyncio.Queue(loop=state.loop)
|
||||
|
||||
async def next(self):
|
||||
if self.users.empty():
|
||||
await self.fill_users()
|
||||
|
||||
try:
|
||||
return self.users.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
async def fill_users(self):
|
||||
# this is a hack because >circular imports<
|
||||
from .user import User
|
||||
|
||||
if self.limit > 0:
|
||||
retrieve = self.limit if self.limit <= 100 else 100
|
||||
|
||||
after = self.after.id if self.after else None
|
||||
data = await self.getter(
|
||||
self.message.id, self.channel_id, self.emoji, retrieve, after=after
|
||||
)
|
||||
|
||||
if data:
|
||||
self.limit -= retrieve
|
||||
self.after = Object(id=int(data[0]["id"]))
|
||||
|
||||
if self.guild is None:
|
||||
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 = 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):
|
||||
"""Iterator for receiving a channel's message history.
|
||||
|
||||
The messages endpoint has two behaviours we care about here:
|
||||
If `before` is specified, the messages endpoint returns the `limit`
|
||||
newest messages before `before`, sorted with newest first. For filling over
|
||||
100 messages, update the `before` parameter to the oldest message received.
|
||||
Messages will be returned in order by time.
|
||||
If `after` is specified, it returns the `limit` oldest messages after
|
||||
`after`, sorted with newest first. For filling over 100 messages, update the
|
||||
`after` parameter to the newest message received. If messages are not
|
||||
reversed, they will be out of order (99-0, 199-100, so on)
|
||||
|
||||
A note that if both before and after are specified, before is ignored by the
|
||||
messages endpoint.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
messageable: :class:`abc.Messageable`
|
||||
Messageable class to retrieve message history fro.
|
||||
limit : int
|
||||
Maximum number of messages to retrieve
|
||||
before : :class:`Message` or id-like
|
||||
Message before which all messages must be.
|
||||
after : :class:`Message` or id-like
|
||||
Message after which all messages must be.
|
||||
around : :class:`Message` or id-like
|
||||
Message around which all messages must be. Limit max 101. Note that if
|
||||
limit is an even number, this will return at most limit+1 messages.
|
||||
reverse: bool
|
||||
If set to true, return messages in oldest->newest order. Recommended
|
||||
when using with "after" queries with limit over 100, otherwise messages
|
||||
will be out of order.
|
||||
"""
|
||||
|
||||
def __init__(self, messageable, limit, before=None, after=None, around=None, reverse=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 isinstance(around, datetime.datetime):
|
||||
around = Object(id=time_snowflake(around))
|
||||
|
||||
self.messageable = messageable
|
||||
self.limit = limit
|
||||
self.before = before
|
||||
self.after = after
|
||||
self.around = around
|
||||
|
||||
if reverse is None:
|
||||
self.reverse = after is not None
|
||||
else:
|
||||
self.reverse = reverse
|
||||
|
||||
self._filter = None # message dict -> bool
|
||||
|
||||
self.state = self.messageable._state
|
||||
self.logs_from = self.state.http.logs_from
|
||||
self.messages = asyncio.Queue(loop=self.state.loop)
|
||||
|
||||
if self.around:
|
||||
if self.limit is 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
|
||||
elif self.limit == 1:
|
||||
raise ValueError("Use get_message.")
|
||||
|
||||
self._retrieve_messages = self._retrieve_messages_around_strategy
|
||||
if self.before and self.after:
|
||||
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
|
||||
elif self.after:
|
||||
self._filter = lambda m: self.after.id < int(m["id"])
|
||||
elif self.before and self.after:
|
||||
if self.reverse:
|
||||
self._retrieve_messages = self._retrieve_messages_after_strategy
|
||||
self._filter = lambda m: int(m["id"]) < self.before.id
|
||||
else:
|
||||
self._retrieve_messages = self._retrieve_messages_before_strategy
|
||||
self._filter = lambda m: int(m["id"]) > self.after.id
|
||||
elif self.after:
|
||||
self._retrieve_messages = self._retrieve_messages_after_strategy
|
||||
else:
|
||||
self._retrieve_messages = self._retrieve_messages_before_strategy
|
||||
|
||||
async def next(self):
|
||||
if self.messages.empty():
|
||||
await self.fill_messages()
|
||||
|
||||
try:
|
||||
return self.messages.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def flatten(self):
|
||||
# this is similar to fill_messages except it uses a list instead
|
||||
# of a queue to place the messages in.
|
||||
result = []
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
while self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for element in data:
|
||||
result.append(self.state.create_message(channel=channel, data=element))
|
||||
return result
|
||||
|
||||
async def fill_messages(self):
|
||||
if not hasattr(self, "channel"):
|
||||
# do the required set up
|
||||
channel = await self.messageable._get_channel()
|
||||
self.channel = channel
|
||||
|
||||
if self._get_retrieve():
|
||||
data = await self._retrieve_messages(self.retrieve)
|
||||
if self.limit is None and len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
channel = self.channel
|
||||
for element in data:
|
||||
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
||||
|
||||
async def _retrieve_messages(self, retrieve):
|
||||
"""Retrieve messages and update next parameters."""
|
||||
pass
|
||||
|
||||
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)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
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)
|
||||
if len(data):
|
||||
if self.limit is not None:
|
||||
self.limit -= retrieve
|
||||
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)
|
||||
self.around = None
|
||||
return data
|
||||
return []
|
||||
|
||||
|
||||
class AuditLogIterator(_AsyncIterator):
|
||||
def __init__(
|
||||
self,
|
||||
guild,
|
||||
limit=None,
|
||||
before=None,
|
||||
after=None,
|
||||
reverse=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))
|
||||
|
||||
self.guild = guild
|
||||
self.loop = guild._state.loop
|
||||
self.request = guild._state.http.get_audit_logs
|
||||
self.limit = limit
|
||||
self.before = before
|
||||
self.user_id = user_id
|
||||
self.action_type = action_type
|
||||
self.after = after
|
||||
self._users = {}
|
||||
self._state = guild._state
|
||||
|
||||
if reverse is None:
|
||||
self.reverse = after is not None
|
||||
else:
|
||||
self.reverse = reverse
|
||||
|
||||
self._filter = None # entry dict -> bool
|
||||
|
||||
self.entries = asyncio.Queue(loop=self.loop)
|
||||
|
||||
if self.before and self.after:
|
||||
if self.reverse:
|
||||
self._strategy = self._after_strategy
|
||||
self._filter = lambda m: int(m["id"]) < self.before.id
|
||||
else:
|
||||
self._strategy = self._before_strategy
|
||||
self._filter = lambda m: int(m["id"]) > self.after.id
|
||||
elif self.after:
|
||||
self._strategy = self._after_strategy
|
||||
else:
|
||||
self._strategy = self._before_strategy
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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", [])
|
||||
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
|
||||
|
||||
async def next(self):
|
||||
if self.entries.empty():
|
||||
await self._fill()
|
||||
|
||||
try:
|
||||
return self.entries.get_nowait()
|
||||
except asyncio.QueueEmpty:
|
||||
raise NoMoreItems()
|
||||
|
||||
def _get_retrieve(self):
|
||||
l = self.limit
|
||||
if l is None:
|
||||
r = 100
|
||||
elif l <= 100:
|
||||
r = l
|
||||
else:
|
||||
r = 100
|
||||
|
||||
self.retrieve = r
|
||||
return r > 0
|
||||
|
||||
async def _fill(self):
|
||||
from .user import User
|
||||
|
||||
if self._get_retrieve():
|
||||
users, data = await self._strategy(self.retrieve)
|
||||
if self.limit is None and len(data) < 100:
|
||||
self.limit = 0 # terminate the infinite loop
|
||||
|
||||
if self.reverse:
|
||||
data = reversed(data)
|
||||
if self._filter:
|
||||
data = filter(self._filter, data)
|
||||
|
||||
for user in users:
|
||||
u = User(data=user, state=self._state)
|
||||
self._users[u.id] = u
|
||||
|
||||
for element in data:
|
||||
# TODO: remove this if statement later
|
||||
if element["action_type"] is None:
|
||||
continue
|
||||
|
||||
await self.entries.put(
|
||||
AuditLogEntry(data=element, users=self._users, guild=self.guild)
|
||||
)
|
||||
@@ -1,621 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
|
||||
import discord.abc
|
||||
|
||||
from . import utils
|
||||
from .user import BaseUser, User
|
||||
from .activity import create_activity
|
||||
from .permissions import Permissions
|
||||
from .enums import Status, try_enum
|
||||
from .colour import Colour
|
||||
from .object import Object
|
||||
|
||||
|
||||
class VoiceState:
|
||||
"""Represents a Discord user's voice state.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
deaf: :class:`bool`
|
||||
Indicates if the user is currently deafened by the guild.
|
||||
mute: :class:`bool`
|
||||
Indicates if the user is currently muted by the guild.
|
||||
self_mute: :class:`bool`
|
||||
Indicates if the user is currently muted by their own accord.
|
||||
self_deaf: :class:`bool`
|
||||
Indicates if the user is currently deafened by their own accord.
|
||||
afk: :class:`bool`
|
||||
Indicates if the user is currently in the AFK channel in the guild.
|
||||
channel: :class:`VoiceChannel`
|
||||
The voice channel that the user is currently connected to. None if the user
|
||||
is not currently in a voice channel.
|
||||
"""
|
||||
|
||||
__slots__ = ("session_id", "deaf", "mute", "self_mute", "self_deaf", "afk", "channel")
|
||||
|
||||
def __init__(self, *, data, channel=None):
|
||||
self.session_id = 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.afk = data.get("suppress", False)
|
||||
self.mute = data.get("mute", False)
|
||||
self.deaf = data.get("deaf", False)
|
||||
self.channel = channel
|
||||
|
||||
def __repr__(self):
|
||||
return "<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} channel={0.channel!r}>".format(
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
def flatten_user(cls):
|
||||
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
|
||||
# ignore private/special methods
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
|
||||
# don't override what we already have
|
||||
if attr in cls.__dict__:
|
||||
continue
|
||||
|
||||
# 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__"):
|
||||
|
||||
def getter(self, x=attr):
|
||||
return getattr(self._user, x)
|
||||
|
||||
setattr(cls, attr, property(getter, doc="Equivalent to :attr:`User.%s`" % attr))
|
||||
else:
|
||||
# probably a member function by now
|
||||
def generate_function(x):
|
||||
def general(self, *args, **kwargs):
|
||||
return getattr(self._user, x)(*args, **kwargs)
|
||||
|
||||
general.__name__ = x
|
||||
return general
|
||||
|
||||
func = generate_function(attr)
|
||||
func.__doc__ = value.__doc__
|
||||
setattr(cls, attr, func)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
_BaseUser = discord.abc.User
|
||||
|
||||
|
||||
@flatten_user
|
||||
class Member(discord.abc.Messageable, _BaseUser):
|
||||
"""Represents a Discord member to a :class:`Guild`.
|
||||
|
||||
This implements a lot of the functionality of :class:`User`.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two members are equal.
|
||||
Note that this works with :class:`User` instances too.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two members are not equal.
|
||||
Note that this works with :class:`User` instances too.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the member's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the member's name with the discriminator.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
joined_at: `datetime.datetime`
|
||||
A datetime object that specifies the date and time in UTC that the member joined the guild for
|
||||
the first time.
|
||||
activities: Tuple[Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`]]
|
||||
The activities that the user is currently doing.
|
||||
guild: :class:`Guild`
|
||||
The guild that the member belongs to.
|
||||
nick: Optional[:class:`str`]
|
||||
The guild specific nickname of the user.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_roles",
|
||||
"joined_at",
|
||||
"_client_status",
|
||||
"activities",
|
||||
"guild",
|
||||
"nick",
|
||||
"_user",
|
||||
"_state",
|
||||
)
|
||||
|
||||
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._update_roles(data)
|
||||
self._client_status = {None: Status.offline}
|
||||
self.activities = tuple(map(create_activity, data.get("activities", [])))
|
||||
self.nick = data.get("nick", None)
|
||||
|
||||
def __str__(self):
|
||||
return str(self._user)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}"
|
||||
" bot={1.bot} nick={0.nick!r} guild={0.guild!r}>".format(self, self._user)
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _BaseUser) and other.id == self.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._user)
|
||||
|
||||
@classmethod
|
||||
def _copy(cls, member):
|
||||
self = cls.__new__(cls) # to bypass __init__
|
||||
|
||||
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
|
||||
self.joined_at = member.joined_at
|
||||
self._client_status = member._client_status.copy()
|
||||
self.guild = member.guild
|
||||
self.nick = member.nick
|
||||
self.activities = member.activities
|
||||
self._state = member._state
|
||||
self._user = User._copy(member._user)
|
||||
return self
|
||||
|
||||
async def _get_channel(self):
|
||||
ch = await self.create_dm()
|
||||
return ch
|
||||
|
||||
def _update_roles(self, data):
|
||||
self._roles = utils.SnowflakeList(map(int, data["roles"]))
|
||||
|
||||
def _update(self, data, user=None):
|
||||
if user:
|
||||
self._user.name = user["username"]
|
||||
self._user.discriminator = user["discriminator"]
|
||||
self._user.avatar = user["avatar"]
|
||||
self._user.bot = user.get("bot", False)
|
||||
|
||||
# the nickname change is optional,
|
||||
# if it isn't in the payload then it didn't change
|
||||
try:
|
||||
self.nick = data["nick"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self._update_roles(data)
|
||||
|
||||
def _presence_update(self, data, user):
|
||||
self.activities = tuple(map(create_activity, data.get("activities", [])))
|
||||
self._client_status = {key: value for key, value in data.get("client_status", {}).items()}
|
||||
self._client_status[None] = data["status"]
|
||||
|
||||
if len(user) > 1:
|
||||
u = self._user
|
||||
u.name = user.get("username", u.name)
|
||||
u.avatar = user.get("avatar", u.avatar)
|
||||
u.discriminator = user.get("discriminator", u.discriminator)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
""":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])
|
||||
|
||||
@status.setter
|
||||
def status(self, value):
|
||||
# internal use only
|
||||
self._client_status[None] = str(value)
|
||||
|
||||
@property
|
||||
def mobile_status(self):
|
||||
""":class:`Status`: The member's status on a mobile device, if applicable."""
|
||||
return try_enum(Status, self._client_status.get("mobile", "offline"))
|
||||
|
||||
@property
|
||||
def desktop_status(self):
|
||||
""":class:`Status`: The member's status on the desktop client, if applicable."""
|
||||
return try_enum(Status, self._client_status.get("desktop", "offline"))
|
||||
|
||||
@property
|
||||
def web_status(self):
|
||||
""":class:`Status`: The member's status on the web client, if applicable."""
|
||||
return try_enum(Status, self._client_status.get("web", "offline"))
|
||||
|
||||
def is_on_mobile(self):
|
||||
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
|
||||
return "mobile" in self._client_status
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
"""A property that returns a :class:`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.
|
||||
|
||||
There is an alias for this under ``color``.
|
||||
"""
|
||||
|
||||
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
|
||||
# is chosen instead
|
||||
for role in reversed(roles):
|
||||
if role.colour.value:
|
||||
return role.colour
|
||||
return Colour.default()
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
"""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.
|
||||
|
||||
These roles are sorted by their position in the role hierarchy.
|
||||
"""
|
||||
result = []
|
||||
g = self.guild
|
||||
for role_id in self._roles:
|
||||
role = g.get_role(role_id)
|
||||
if role:
|
||||
result.append(role)
|
||||
result.append(g.default_role)
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
"""Returns a string that mentions the member."""
|
||||
if self.nick:
|
||||
return "<@!%s>" % self.id
|
||||
return "<@%s>" % self.id
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Returns the user's display name.
|
||||
|
||||
For regular users this is just their username, but
|
||||
if they have a guild specific nickname then that
|
||||
is returned instead.
|
||||
"""
|
||||
return self.nick if self.nick is not None else self.name
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
"""Returns a class Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`] for the primary
|
||||
activity the user is currently doing. Could be None if no activity is being done.
|
||||
|
||||
.. note::
|
||||
|
||||
A user may have multiple activities, these can be accessed under :attr:`activities`.
|
||||
"""
|
||||
if self.activities:
|
||||
return self.activities[0]
|
||||
|
||||
def mentioned_in(self, message):
|
||||
"""Checks if the member is mentioned in the specified message.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message: :class:`Message`
|
||||
The message to check if you're mentioned in.
|
||||
"""
|
||||
if self._user.mentioned_in(message):
|
||||
return True
|
||||
|
||||
for role in message.role_mentions:
|
||||
has_role = utils.get(self.roles, id=role.id) is not None
|
||||
if has_role:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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
|
||||
The channel to check your permissions for.
|
||||
"""
|
||||
return channel.permissions_for(self)
|
||||
|
||||
@property
|
||||
def top_role(self):
|
||||
"""Returns the member's highest role.
|
||||
|
||||
This is useful for figuring where a member stands in the role
|
||||
hierarchy chain.
|
||||
"""
|
||||
return self.roles[-1]
|
||||
|
||||
@property
|
||||
def guild_permissions(self):
|
||||
"""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`.
|
||||
|
||||
This does take into consideration guild ownership and the
|
||||
administrator implication.
|
||||
"""
|
||||
|
||||
if self.guild.owner == self:
|
||||
return Permissions.all()
|
||||
|
||||
base = Permissions.none()
|
||||
for r in self.roles:
|
||||
base.value |= r.permissions.value
|
||||
|
||||
if base.administrator:
|
||||
return Permissions.all()
|
||||
|
||||
return base
|
||||
|
||||
@property
|
||||
def voice(self):
|
||||
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
|
||||
return self.guild._voice_state_for(self._user.id)
|
||||
|
||||
async def ban(self, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Bans this member. Equivalent to :meth:`Guild.ban`
|
||||
"""
|
||||
await self.guild.ban(self, **kwargs)
|
||||
|
||||
async def unban(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Unbans this member. Equivalent to :meth:`Guild.unban`
|
||||
"""
|
||||
await self.guild.unban(self, reason=reason)
|
||||
|
||||
async def kick(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Kicks this member. Equivalent to :meth:`Guild.kick`
|
||||
"""
|
||||
await self.guild.kick(self, reason=reason)
|
||||
|
||||
async def edit(self, *, reason=None, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the member's data.
|
||||
|
||||
Depending on the parameter passed, this requires different permissions listed below:
|
||||
|
||||
+---------------+--------------------------------------+
|
||||
| Parameter | Permission |
|
||||
+---------------+--------------------------------------+
|
||||
| nick | :attr:`Permissions.manage_nicknames` |
|
||||
+---------------+--------------------------------------+
|
||||
| mute | :attr:`Permissions.mute_members` |
|
||||
+---------------+--------------------------------------+
|
||||
| deafen | :attr:`Permissions.deafen_members` |
|
||||
+---------------+--------------------------------------+
|
||||
| roles | :attr:`Permissions.manage_roles` |
|
||||
+---------------+--------------------------------------+
|
||||
| voice_channel | :attr:`Permissions.move_members` |
|
||||
+---------------+--------------------------------------+
|
||||
|
||||
All parameters are optional.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
nick: str
|
||||
The member's new nickname. Use ``None`` to remove the nickname.
|
||||
mute: bool
|
||||
Indicates if the member should be guild muted or un-muted.
|
||||
deafen: bool
|
||||
Indicates if the member should be guild deafened or un-deafened.
|
||||
roles: List[:class:`Roles`]
|
||||
The member's new list of roles. This *replaces* the roles.
|
||||
voice_channel: :class:`VoiceChannel`
|
||||
The voice channel to move the member to.
|
||||
reason: Optional[str]
|
||||
The reason for editing this member. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have the proper permissions to the action requested.
|
||||
HTTPException
|
||||
The operation failed.
|
||||
"""
|
||||
http = self._state.http
|
||||
guild_id = self.guild.id
|
||||
payload = {}
|
||||
|
||||
try:
|
||||
nick = fields["nick"]
|
||||
except KeyError:
|
||||
# nick not present so...
|
||||
pass
|
||||
else:
|
||||
nick = nick if nick else ""
|
||||
if self._state.self_id == self.id:
|
||||
await http.change_my_nickname(guild_id, nick, reason=reason)
|
||||
else:
|
||||
payload["nick"] = nick
|
||||
|
||||
deafen = fields.get("deafen")
|
||||
if deafen is not None:
|
||||
payload["deaf"] = deafen
|
||||
|
||||
mute = fields.get("mute")
|
||||
if mute is not None:
|
||||
payload["mute"] = mute
|
||||
|
||||
try:
|
||||
vc = fields["voice_channel"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
payload["channel_id"] = vc.id
|
||||
|
||||
try:
|
||||
roles = fields["roles"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
payload["roles"] = tuple(r.id for r in roles)
|
||||
|
||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
||||
|
||||
# TODO: wait for WS event for modify-in-place behaviour
|
||||
|
||||
async def move_to(self, channel, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Moves a member to a new voice channel (they must be connected first).
|
||||
|
||||
You must have the :attr:`~Permissions.move_members` permission to
|
||||
use this.
|
||||
|
||||
This raises the same exceptions as :meth:`edit`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
channel: :class:`VoiceChannel`
|
||||
The new voice channel to move the member to.
|
||||
reason: Optional[str]
|
||||
The reason for doing this action. Shows up on the audit log.
|
||||
"""
|
||||
await self.edit(voice_channel=channel, reason=reason)
|
||||
|
||||
async def add_roles(self, *roles, reason=None, atomic=True):
|
||||
r"""|coro|
|
||||
|
||||
Gives the member a number of :class:`Role`\s.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*roles
|
||||
An argument list of :class:`abc.Snowflake` representing a :class:`Role`
|
||||
to give to the member.
|
||||
reason: Optional[str]
|
||||
The reason for adding these roles. Shows up on the audit log.
|
||||
atomic: bool
|
||||
Whether to atomically add roles. This will ensure that multiple
|
||||
operations will always be applied regardless of the current
|
||||
state of the cache.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to add these roles.
|
||||
HTTPException
|
||||
Adding roles failed.
|
||||
"""
|
||||
|
||||
if not atomic:
|
||||
new_roles = utils._unique(Object(id=r.id) for s in (self.roles[1:], roles) for r in s)
|
||||
await self.edit(roles=new_roles, reason=reason)
|
||||
else:
|
||||
req = self._state.http.add_role
|
||||
guild_id = self.guild.id
|
||||
user_id = self.id
|
||||
for role in roles:
|
||||
await req(guild_id, user_id, role.id, reason=reason)
|
||||
|
||||
async def remove_roles(self, *roles, reason=None, atomic=True):
|
||||
r"""|coro|
|
||||
|
||||
Removes :class:`Role`\s from this member.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*roles
|
||||
An argument list of :class:`abc.Snowflake` representing a :class:`Role`
|
||||
to remove from the member.
|
||||
reason: Optional[str]
|
||||
The reason for removing these roles. Shows up on the audit log.
|
||||
atomic: bool
|
||||
Whether to atomically remove roles. This will ensure that multiple
|
||||
operations will always be applied regardless of the current
|
||||
state of the cache.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to remove these roles.
|
||||
HTTPException
|
||||
Removing the roles failed.
|
||||
"""
|
||||
|
||||
if not atomic:
|
||||
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))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
await self.edit(roles=new_roles, reason=reason)
|
||||
else:
|
||||
req = self._state.http.remove_role
|
||||
guild_id = self.guild.id
|
||||
user_id = self.id
|
||||
for role in roles:
|
||||
await req(guild_id, user_id, role.id, reason=reason)
|
||||
@@ -1,799 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from . import utils
|
||||
from .reaction import Reaction
|
||||
from .emoji import Emoji, PartialEmoji
|
||||
from .calls import CallMessage
|
||||
from .enums import MessageType, try_enum
|
||||
from .errors import InvalidArgument, ClientException, HTTPException
|
||||
from .embeds import Embed
|
||||
|
||||
|
||||
class Attachment:
|
||||
"""Represents an attachment from Discord.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
id: :class:`int`
|
||||
The attachment ID.
|
||||
size: :class:`int`
|
||||
The attachment size in bytes.
|
||||
height: Optional[:class:`int`]
|
||||
The attachment's height, in pixels. Only applicable to images.
|
||||
width: Optional[:class:`int`]
|
||||
The attachment's width, in pixels. Only applicable to images.
|
||||
filename: :class:`str`
|
||||
The attachment's filename.
|
||||
url: :class:`str`
|
||||
The attachment URL. If the message this attachment was attached
|
||||
to is deleted, then this will 404.
|
||||
proxy_url: :class:`str`
|
||||
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
|
||||
case of images. When the message is deleted, this URL might be valid for a few
|
||||
minutes or not valid at all.
|
||||
"""
|
||||
|
||||
__slots__ = ("id", "size", "height", "width", "filename", "url", "proxy_url", "_http")
|
||||
|
||||
def __init__(self, *, data, state):
|
||||
self.id = int(data["id"])
|
||||
self.size = data["size"]
|
||||
self.height = data.get("height")
|
||||
self.width = data.get("width")
|
||||
self.filename = data["filename"]
|
||||
self.url = data.get("url")
|
||||
self.proxy_url = data.get("proxy_url")
|
||||
self._http = state.http
|
||||
|
||||
def is_spoiler(self):
|
||||
""":class:`bool`: Whether this attachment contains a spoiler."""
|
||||
return self.filename.startswith("SPOILER_")
|
||||
|
||||
async def save(self, fp, *, seek_begin=True):
|
||||
"""|coro|
|
||||
|
||||
Saves this attachment into a file-like object.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
fp: Union[BinaryIO, str]
|
||||
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: bool
|
||||
Whether to seek to the beginning of the file after saving is
|
||||
successfully done.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Saving the attachment failed.
|
||||
NotFound
|
||||
The attachment was deleted.
|
||||
|
||||
Returns
|
||||
--------
|
||||
int
|
||||
The number of bytes written.
|
||||
"""
|
||||
|
||||
data = await self._http.get_attachment(self.url)
|
||||
if isinstance(fp, str):
|
||||
with open(fp, "wb") as f:
|
||||
return f.write(data)
|
||||
else:
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
fp.seek(0)
|
||||
return written
|
||||
|
||||
|
||||
class Message:
|
||||
r"""Represents a message from Discord.
|
||||
|
||||
There should be no need to create one of these manually.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
tts: :class:`bool`
|
||||
Specifies if the message was done with text-to-speech.
|
||||
type: :class:`MessageType`
|
||||
The type of message. In most cases this should not be checked, but it is helpful
|
||||
in cases where it might be a system message for :attr:`system_content`.
|
||||
author
|
||||
A :class:`Member` that sent the message. If :attr:`channel` is a
|
||||
private channel or the user has the left the guild, then it is a :class:`User` instead.
|
||||
content: :class:`str`
|
||||
The actual contents of the message.
|
||||
nonce
|
||||
The value used by the discord guild and the client to verify that the message is successfully sent.
|
||||
This is typically non-important.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds the message has.
|
||||
channel
|
||||
The :class:`TextChannel` that the message was sent from.
|
||||
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
|
||||
call: Optional[:class:`CallMessage`]
|
||||
The call that the message refers to. This is only applicable to messages of type
|
||||
:attr:`MessageType.call`.
|
||||
mention_everyone: :class:`bool`
|
||||
Specifies if the message mentions everyone.
|
||||
|
||||
.. note::
|
||||
|
||||
This does not check if the ``@everyone`` or the ``@here`` text is in the message itself.
|
||||
Rather this boolean indicates if either the ``@everyone`` or the ``@here`` text is in the message
|
||||
**and** it did end up mentioning.
|
||||
|
||||
mentions: :class:`list`
|
||||
A list of :class:`Member` that were mentioned. If the message is in a private message
|
||||
then the list will be of :class:`User` instead. For messages that are not of type
|
||||
:attr:`MessageType.default`\, this array can be used to aid in system messages.
|
||||
For more information, see :attr:`system_content`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The order of the mentions list is not in any particular order so you should
|
||||
not rely on it. This is a discord limitation, not one with the library.
|
||||
|
||||
channel_mentions: :class:`list`
|
||||
A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message
|
||||
then the list is always empty.
|
||||
role_mentions: :class:`list`
|
||||
A list of :class:`Role` that were mentioned. If the message is in a private message
|
||||
then the list is always empty.
|
||||
id: :class:`int`
|
||||
The message ID.
|
||||
webhook_id: Optional[:class:`int`]
|
||||
If this message was sent by a webhook, then this is the webhook ID's that sent this
|
||||
message.
|
||||
attachments: List[:class:`Attachment`]
|
||||
A list of attachments given to a message.
|
||||
pinned: :class:`bool`
|
||||
Specifies if the message is currently pinned.
|
||||
reactions : List[:class:`Reaction`]
|
||||
Reactions to a message. Reactions can be either custom emoji or standard unicode emoji.
|
||||
activity: Optional[:class:`dict`]
|
||||
The activity associated with this message. Sent with Rich-Presence related messages that for
|
||||
example, request joining, spectating, or listening to or with another member.
|
||||
|
||||
It is a dictionary with the following optional keys:
|
||||
|
||||
- ``type``: An integer denoting the type of message activity being requested.
|
||||
- ``party_id``: The party ID associated with the party.
|
||||
application: Optional[:class:`dict`]
|
||||
The rich presence enabled application associated with this message.
|
||||
|
||||
It is a dictionary with the following keys:
|
||||
|
||||
- ``id``: A string representing the application's ID.
|
||||
- ``name``: A string representing the application's name.
|
||||
- ``description``: A string representing the application's description.
|
||||
- ``icon``: A string representing the icon ID of the application.
|
||||
- ``cover_image``: A string representing the embed's image asset ID.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_edited_timestamp",
|
||||
"tts",
|
||||
"content",
|
||||
"channel",
|
||||
"webhook_id",
|
||||
"mention_everyone",
|
||||
"embeds",
|
||||
"id",
|
||||
"mentions",
|
||||
"author",
|
||||
"_cs_channel_mentions",
|
||||
"_cs_raw_mentions",
|
||||
"attachments",
|
||||
"_cs_clean_content",
|
||||
"_cs_raw_channel_mentions",
|
||||
"nonce",
|
||||
"pinned",
|
||||
"role_mentions",
|
||||
"_cs_raw_role_mentions",
|
||||
"type",
|
||||
"call",
|
||||
"_cs_system_content",
|
||||
"_cs_guild",
|
||||
"_state",
|
||||
"reactions",
|
||||
"application",
|
||||
"activity",
|
||||
)
|
||||
|
||||
def __init__(self, *, state, channel, data):
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self.webhook_id = utils._get_as_snowflake(data, "webhook_id")
|
||||
self.reactions = [Reaction(message=self, data=d) for d in data.get("reactions", [])]
|
||||
self.application = data.get("application")
|
||||
self.activity = data.get("activity")
|
||||
self._update(channel, data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Message id={0.id} pinned={0.pinned} author={0.author!r}>".format(self)
|
||||
|
||||
def _try_patch(self, data, key, transform=None):
|
||||
try:
|
||||
value = data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if transform is None:
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
setattr(self, key, transform(value))
|
||||
|
||||
def _add_reaction(self, data, emoji, user_id):
|
||||
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
|
||||
is_me = data["me"] = user_id == self._state.self_id
|
||||
|
||||
if reaction is None:
|
||||
reaction = Reaction(message=self, data=data, emoji=emoji)
|
||||
self.reactions.append(reaction)
|
||||
else:
|
||||
reaction.count += 1
|
||||
if is_me:
|
||||
reaction.me = is_me
|
||||
|
||||
return reaction
|
||||
|
||||
def _remove_reaction(self, data, emoji, user_id):
|
||||
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
|
||||
|
||||
if reaction is None:
|
||||
# already removed?
|
||||
raise ValueError("Emoji already removed?")
|
||||
|
||||
# if reaction isn't in the list, we crash. This means discord
|
||||
# sent bad data, or we stored improperly
|
||||
reaction.count -= 1
|
||||
|
||||
if user_id == self._state.self_id:
|
||||
reaction.me = False
|
||||
if reaction.count == 0:
|
||||
# this raises ValueError if something went wrong as well.
|
||||
self.reactions.remove(reaction)
|
||||
|
||||
return reaction
|
||||
|
||||
def _update(self, channel, data):
|
||||
self.channel = channel
|
||||
self._edited_timestamp = utils.parse_time(data.get("edited_timestamp"))
|
||||
self._try_patch(data, "pinned")
|
||||
self._try_patch(data, "application")
|
||||
self._try_patch(data, "activity")
|
||||
self._try_patch(data, "mention_everyone")
|
||||
self._try_patch(data, "tts")
|
||||
self._try_patch(data, "type", lambda x: try_enum(MessageType, x))
|
||||
self._try_patch(data, "content")
|
||||
self._try_patch(
|
||||
data, "attachments", lambda x: [Attachment(data=a, state=self._state) for a in x]
|
||||
)
|
||||
self._try_patch(data, "embeds", lambda x: list(map(Embed.from_data, x)))
|
||||
self._try_patch(data, "nonce")
|
||||
|
||||
for handler in ("author", "mentions", "mention_roles", "call"):
|
||||
try:
|
||||
getattr(self, "_handle_%s" % handler)(data[handler])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
# clear the cached properties
|
||||
cached = filter(lambda attr: attr.startswith("_cs_"), self.__slots__)
|
||||
for attr in cached:
|
||||
try:
|
||||
delattr(self, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _handle_author(self, author):
|
||||
self.author = self._state.store_user(author)
|
||||
if self.guild is not None:
|
||||
found = self.guild.get_member(self.author.id)
|
||||
if found is not None:
|
||||
self.author = found
|
||||
|
||||
def _handle_mentions(self, mentions):
|
||||
self.mentions = []
|
||||
if self.guild is None:
|
||||
self.mentions = [self._state.store_user(m) for m in mentions]
|
||||
return
|
||||
|
||||
for mention in filter(None, mentions):
|
||||
id_search = int(mention["id"])
|
||||
member = self.guild.get_member(id_search)
|
||||
if member is not None:
|
||||
self.mentions.append(member)
|
||||
|
||||
def _handle_mention_roles(self, role_mentions):
|
||||
self.role_mentions = []
|
||||
if self.guild is not None:
|
||||
for role_id in map(int, role_mentions):
|
||||
role = self.guild.get_role(role_id)
|
||||
if role is not None:
|
||||
self.role_mentions.append(role)
|
||||
|
||||
def _handle_call(self, call):
|
||||
if call is None or self.type is not MessageType.call:
|
||||
self.call = None
|
||||
return
|
||||
|
||||
# we get the participant source from the mentions array or
|
||||
# the author
|
||||
|
||||
participants = []
|
||||
for uid in map(int, call.get("participants", [])):
|
||||
if uid == self.author.id:
|
||||
participants.append(self.author)
|
||||
else:
|
||||
user = utils.find(lambda u: u.id == uid, self.mentions)
|
||||
if user is not None:
|
||||
participants.append(user)
|
||||
|
||||
call["participants"] = participants
|
||||
self.call = CallMessage(message=self, **call)
|
||||
|
||||
@utils.cached_slot_property("_cs_guild")
|
||||
def guild(self):
|
||||
"""Optional[:class:`Guild`]: The guild that the message belongs to, if applicable."""
|
||||
return getattr(self.channel, "guild", None)
|
||||
|
||||
@utils.cached_slot_property("_cs_raw_mentions")
|
||||
def raw_mentions(self):
|
||||
"""A property that returns an array of user IDs matched with
|
||||
the syntax of <@user_id> in the message content.
|
||||
|
||||
This allows you to receive the user IDs of mentioned users
|
||||
even in a private message context.
|
||||
"""
|
||||
return [int(x) for x in re.findall(r"<@!?([0-9]+)>", self.content)]
|
||||
|
||||
@utils.cached_slot_property("_cs_raw_channel_mentions")
|
||||
def raw_channel_mentions(self):
|
||||
"""A property that returns an array of channel IDs matched with
|
||||
the syntax of <#channel_id> in the message content.
|
||||
"""
|
||||
return [int(x) for x in re.findall(r"<#([0-9]+)>", self.content)]
|
||||
|
||||
@utils.cached_slot_property("_cs_raw_role_mentions")
|
||||
def raw_role_mentions(self):
|
||||
"""A property that returns an array of role IDs matched with
|
||||
the syntax of <@&role_id> in the message content.
|
||||
"""
|
||||
return [int(x) for x in re.findall(r"<@&([0-9]+)>", self.content)]
|
||||
|
||||
@utils.cached_slot_property("_cs_channel_mentions")
|
||||
def channel_mentions(self):
|
||||
if self.guild is None:
|
||||
return []
|
||||
it = filter(None, map(self.guild.get_channel, self.raw_channel_mentions))
|
||||
return utils._unique(it)
|
||||
|
||||
@utils.cached_slot_property("_cs_clean_content")
|
||||
def clean_content(self):
|
||||
"""A property that returns the content in a "cleaned up"
|
||||
manner. This basically means that mentions are transformed
|
||||
into the way the client shows it. e.g. ``<#id>`` will transform
|
||||
into ``#name``.
|
||||
|
||||
This will also transform @everyone and @here mentions into
|
||||
non-mentions.
|
||||
"""
|
||||
|
||||
transformations = {
|
||||
re.escape("<#%s>" % channel.id): "#" + channel.name
|
||||
for channel in self.channel_mentions
|
||||
}
|
||||
|
||||
mention_transforms = {
|
||||
re.escape("<@%s>" % member.id): "@" + member.display_name for member in self.mentions
|
||||
}
|
||||
|
||||
# add the <@!user_id> cases as well..
|
||||
second_mention_transforms = {
|
||||
re.escape("<@!%s>" % member.id): "@" + member.display_name for member in self.mentions
|
||||
}
|
||||
|
||||
transformations.update(mention_transforms)
|
||||
transformations.update(second_mention_transforms)
|
||||
|
||||
if self.guild is not None:
|
||||
role_transforms = {
|
||||
re.escape("<@&%s>" % role.id): "@" + role.name for role in self.role_mentions
|
||||
}
|
||||
transformations.update(role_transforms)
|
||||
|
||||
def repl(obj):
|
||||
return transformations.get(re.escape(obj.group(0)), "")
|
||||
|
||||
pattern = re.compile("|".join(transformations.keys()))
|
||||
result = pattern.sub(repl, self.content)
|
||||
|
||||
transformations = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
|
||||
|
||||
def repl2(obj):
|
||||
return transformations.get(obj.group(0), "")
|
||||
|
||||
pattern = re.compile("|".join(transformations.keys()))
|
||||
return pattern.sub(repl2, result)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""datetime.datetime: The message's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def edited_at(self):
|
||||
"""Optional[datetime.datetime]: A naive UTC datetime object containing the edited time of the message."""
|
||||
return self._edited_timestamp
|
||||
|
||||
@property
|
||||
def jump_url(self):
|
||||
""":class:`str`: Returns a URL that allows the client to jump to this message."""
|
||||
guild_id = getattr(self.guild, "id", "@me")
|
||||
return "https://discordapp.com/channels/{0}/{1.channel.id}/{1.id}".format(guild_id, self)
|
||||
|
||||
@utils.cached_slot_property("_cs_system_content")
|
||||
def system_content(self):
|
||||
r"""A property that returns the content that is rendered
|
||||
regardless of the :attr:`Message.type`.
|
||||
|
||||
In the case of :attr:`MessageType.default`\, this just returns the
|
||||
regular :attr:`Message.content`. Otherwise this returns an English
|
||||
message denoting the contents of the system message.
|
||||
"""
|
||||
|
||||
if self.type is MessageType.default:
|
||||
return self.content
|
||||
|
||||
if self.type is MessageType.pins_add:
|
||||
return "{0.name} pinned a message to this channel.".format(self.author)
|
||||
|
||||
if self.type is MessageType.recipient_add:
|
||||
return "{0.name} added {1.name} to the group.".format(self.author, self.mentions[0])
|
||||
|
||||
if self.type is MessageType.recipient_remove:
|
||||
return "{0.name} removed {1.name} from the group.".format(
|
||||
self.author, self.mentions[0]
|
||||
)
|
||||
|
||||
if self.type is MessageType.channel_name_change:
|
||||
return "{0.author.name} changed the channel name: {0.content}".format(self)
|
||||
|
||||
if self.type is MessageType.channel_icon_change:
|
||||
return "{0.author.name} changed the channel icon.".format(self)
|
||||
|
||||
if self.type is MessageType.new_member:
|
||||
formats = [
|
||||
"{0} just joined the server - glhf!",
|
||||
"{0} just joined. Everyone, look busy!",
|
||||
"{0} just joined. Can I get a heal?",
|
||||
"{0} joined your party.",
|
||||
"{0} joined. You must construct additional pylons.",
|
||||
"Ermagherd. {0} is here.",
|
||||
"Welcome, {0}. Stay awhile and listen.",
|
||||
"Welcome, {0}. We were expecting you ( ͡° ͜ʖ ͡°)",
|
||||
"Welcome, {0}. We hope you brought pizza.",
|
||||
"Welcome {0}. Leave your weapons by the door.",
|
||||
"A wild {0} appeared.",
|
||||
"Swoooosh. {0} just landed.",
|
||||
"Brace yourselves. {0} just joined the server.",
|
||||
"{0} just joined. Hide your bananas.",
|
||||
"{0} just arrived. Seems OP - please nerf.",
|
||||
"{0} just slid into the server.",
|
||||
"A {0} has spawned in the server.",
|
||||
"Big {0} showed up!",
|
||||
"Where’s {0}? In the server!",
|
||||
"{0} hopped into the server. Kangaroo!!",
|
||||
"{0} just showed up. Hold my beer.",
|
||||
"Challenger approaching - {0} has appeared!",
|
||||
"It's a bird! It's a plane! Nevermind, it's just {0}.",
|
||||
"It's {0}! Praise the sun! [T]/",
|
||||
"Never gonna give {0} up. Never gonna let {0} down.",
|
||||
"Ha! {0} has joined! You activated my trap card!",
|
||||
"Cheers, love! {0}'s here!",
|
||||
"Hey! Listen! {0} has joined!",
|
||||
"We've been expecting you {0}",
|
||||
"It's dangerous to go alone, take {0}!",
|
||||
"{0} has joined the server! It's super effective!",
|
||||
"Cheers, love! {0} is here!",
|
||||
"{0} is here, as the prophecy foretold.",
|
||||
"{0} has arrived. Party's over.",
|
||||
"Ready player {0}",
|
||||
"{0} is here to kick butt and chew bubblegum. And {0} is all out of gum.",
|
||||
"Hello. Is it {0} you're looking for?",
|
||||
"{0} has joined. Stay a while and listen!",
|
||||
"Roses are red, violets are blue, {0} joined this server with you",
|
||||
]
|
||||
|
||||
index = int(self.created_at.timestamp()) % len(formats)
|
||||
return formats[index].format(self.author.name)
|
||||
|
||||
if self.type is MessageType.call:
|
||||
# we're at the call message type now, which is a bit more complicated.
|
||||
# we can make the assumption that Message.channel is a PrivateChannel
|
||||
# with the type ChannelType.group or ChannelType.private
|
||||
call_ended = self.call.ended_timestamp is not None
|
||||
|
||||
if self.channel.me in self.call.participants:
|
||||
return "{0.author.name} started a call.".format(self)
|
||||
elif call_ended:
|
||||
return "You missed a call from {0.author.name}".format(self)
|
||||
else:
|
||||
return "{0.author.name} started a call \N{EM DASH} Join the call.".format(self)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the message.
|
||||
|
||||
Your own messages could be deleted without any proper permissions. However to
|
||||
delete other people's messages, you need the :attr:`~Permissions.manage_messages`
|
||||
permission.
|
||||
|
||||
Raises
|
||||
------
|
||||
Forbidden
|
||||
You do not have proper permissions to delete the message.
|
||||
HTTPException
|
||||
Deleting the message failed.
|
||||
"""
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the message.
|
||||
|
||||
The content must be able to be transformed into a string via ``str(content)``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
content: Optional[str]
|
||||
The new content to replace the message with.
|
||||
Could be ``None`` to remove the content.
|
||||
embed: Optional[:class:`Embed`]
|
||||
The new embed to replace the original with.
|
||||
Could be ``None`` to remove the embed.
|
||||
delete_after: Optional[float]
|
||||
If provided, the number of seconds to wait in the background
|
||||
before deleting the message we just edited. If the deletion fails,
|
||||
then it is silently ignored.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the message failed.
|
||||
"""
|
||||
|
||||
try:
|
||||
content = fields["content"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if content is not None:
|
||||
fields["content"] = str(content)
|
||||
|
||||
try:
|
||||
embed = fields["embed"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if embed is not None:
|
||||
fields["embed"] = embed.to_dict()
|
||||
|
||||
data = await self._state.http.edit_message(self.id, self.channel.id, **fields)
|
||||
self._update(channel=self.channel, data=data)
|
||||
|
||||
try:
|
||||
delete_after = fields["delete_after"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if delete_after is not None:
|
||||
|
||||
async def delete():
|
||||
await asyncio.sleep(delete_after, loop=self._state.loop)
|
||||
try:
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(delete(), loop=self._state.loop)
|
||||
|
||||
async def pin(self):
|
||||
"""|coro|
|
||||
|
||||
Pins the message.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_messages` permission to do
|
||||
this in a non-private channel context.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to pin the message.
|
||||
NotFound
|
||||
The message or channel was not found or deleted.
|
||||
HTTPException
|
||||
Pinning the message failed, probably due to the channel
|
||||
having more than 50 pinned messages.
|
||||
"""
|
||||
|
||||
await self._state.http.pin_message(self.channel.id, self.id)
|
||||
self.pinned = True
|
||||
|
||||
async def unpin(self):
|
||||
"""|coro|
|
||||
|
||||
Unpins the message.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_messages` permission to do
|
||||
this in a non-private channel context.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to unpin the message.
|
||||
NotFound
|
||||
The message or channel was not found or deleted.
|
||||
HTTPException
|
||||
Unpinning the message failed.
|
||||
"""
|
||||
|
||||
await self._state.http.unpin_message(self.channel.id, self.id)
|
||||
self.pinned = False
|
||||
|
||||
async def add_reaction(self, emoji):
|
||||
"""|coro|
|
||||
|
||||
Add a reaction to the message.
|
||||
|
||||
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
|
||||
|
||||
You must have the :attr:`~Permissions.read_message_history` permission
|
||||
to use this. If nobody else has reacted to the message using this
|
||||
emoji, the :attr:`~Permissions.add_reactions` permission is required.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, str]
|
||||
The emoji to react with.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Adding the reaction failed.
|
||||
Forbidden
|
||||
You do not have the proper permissions to react to the message.
|
||||
NotFound
|
||||
The emoji you specified was not found.
|
||||
InvalidArgument
|
||||
The emoji parameter is invalid.
|
||||
"""
|
||||
|
||||
emoji = self._emoji_reaction(emoji)
|
||||
await self._state.http.add_reaction(self.id, self.channel.id, emoji)
|
||||
|
||||
async def remove_reaction(self, emoji, member):
|
||||
"""|coro|
|
||||
|
||||
Remove a reaction by the member from the message.
|
||||
|
||||
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
|
||||
|
||||
If the reaction is not your own (i.e. ``member`` parameter is not you) then
|
||||
the :attr:`~Permissions.manage_messages` permission is needed.
|
||||
|
||||
The ``member`` parameter must represent a member and meet
|
||||
the :class:`abc.Snowflake` abc.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, str]
|
||||
The emoji to remove.
|
||||
member: :class:`abc.Snowflake`
|
||||
The member for which to remove the reaction.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Removing the reaction failed.
|
||||
Forbidden
|
||||
You do not have the proper permissions to remove the reaction.
|
||||
NotFound
|
||||
The member or emoji you specified was not found.
|
||||
InvalidArgument
|
||||
The emoji parameter is invalid.
|
||||
"""
|
||||
|
||||
emoji = self._emoji_reaction(emoji)
|
||||
|
||||
if member.id == self._state.self_id:
|
||||
await self._state.http.remove_own_reaction(self.id, self.channel.id, emoji)
|
||||
else:
|
||||
await self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id)
|
||||
|
||||
@staticmethod
|
||||
def _emoji_reaction(emoji):
|
||||
if isinstance(emoji, Reaction):
|
||||
emoji = emoji.emoji
|
||||
|
||||
if isinstance(emoji, Emoji):
|
||||
return "%s:%s" % (emoji.name, emoji.id)
|
||||
if isinstance(emoji, PartialEmoji):
|
||||
return emoji._as_reaction()
|
||||
if isinstance(emoji, str):
|
||||
return emoji # this is okay
|
||||
|
||||
raise InvalidArgument(
|
||||
"emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.".format(
|
||||
emoji
|
||||
)
|
||||
)
|
||||
|
||||
async def clear_reactions(self):
|
||||
"""|coro|
|
||||
|
||||
Removes all the reactions from the message.
|
||||
|
||||
You need the :attr:`~Permissions.manage_messages` permission to use this.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Removing the reactions failed.
|
||||
Forbidden
|
||||
You do not have the proper permissions to remove all the reactions.
|
||||
"""
|
||||
await self._state.http.clear_reactions(self.id, self.channel.id)
|
||||
|
||||
def ack(self):
|
||||
"""|coro|
|
||||
|
||||
Marks this message as read.
|
||||
|
||||
The user must not be a bot user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Acking failed.
|
||||
ClientException
|
||||
You must not be a bot user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
if state.is_bot:
|
||||
raise ClientException("Must not be a bot account to ack messages.")
|
||||
return state.http.ack_message(self.channel.id, self.id)
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
|
||||
class EqualityComparable:
|
||||
__slots__ = ()
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and other.id == self.id
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.id != self.id
|
||||
return True
|
||||
|
||||
|
||||
class Hashable(EqualityComparable):
|
||||
__slots__ = ()
|
||||
|
||||
def __hash__(self):
|
||||
return self.id >> 22
|
||||
@@ -1,71 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 . import utils
|
||||
from .mixins import Hashable
|
||||
|
||||
|
||||
class Object(Hashable):
|
||||
"""Represents a generic Discord object.
|
||||
|
||||
The purpose of this class is to allow you to create 'miniature'
|
||||
versions of data classes if you want to pass in just an ID. Most functions
|
||||
that take in a specific data class with an ID can also take in this class
|
||||
as a substitute instead. Note that even though this is the case, not all
|
||||
objects (if any) actually inherit from this class.
|
||||
|
||||
There are also some cases where some websocket events are received
|
||||
in :issue:`strange order <21>` and when such events happened you would
|
||||
receive this class rather than the actual data class. These cases are
|
||||
extremely rare.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two objects are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two objects are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the object's hash.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id : :class:`str`
|
||||
The ID of the object.
|
||||
"""
|
||||
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the snowflake's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
286
discord/opus.py
286
discord/opus.py
@@ -1,286 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
|
||||
c_float_ptr = ctypes.POINTER(ctypes.c_float)
|
||||
|
||||
|
||||
class EncoderStruct(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
EncoderStructPtr = ctypes.POINTER(EncoderStruct)
|
||||
|
||||
|
||||
def _err_lt(result, func, args):
|
||||
if result < 0:
|
||||
log.info("error has happened in %s", func.__name__)
|
||||
raise OpusError(result)
|
||||
return result
|
||||
|
||||
|
||||
def _err_ne(result, func, args):
|
||||
ret = args[-1]._obj
|
||||
if ret.value != 0:
|
||||
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 = [
|
||||
("opus_strerror", [ctypes.c_int], ctypes.c_char_p, 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_encoder_ctl", None, ctypes.c_int32, _err_lt),
|
||||
("opus_encoder_destroy", [EncoderStructPtr], None, None),
|
||||
]
|
||||
|
||||
|
||||
def libopus_loader(name):
|
||||
# create the library...
|
||||
lib = ctypes.cdll.LoadLibrary(name)
|
||||
|
||||
# register the functions...
|
||||
for item in exported_functions:
|
||||
func = getattr(lib, item[0])
|
||||
|
||||
try:
|
||||
if item[1]:
|
||||
func.argtypes = item[1]
|
||||
|
||||
func.restype = item[2]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if item[3]:
|
||||
func.errcheck = item[3]
|
||||
except KeyError:
|
||||
log.exception("Error assigning check function to %s", func)
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
_basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
_bitness = "x64" if sys.maxsize > 2 ** 32 else "x86"
|
||||
_filename = os.path.join(_basedir, "bin", "libopus-0.{}.dll".format(_bitness))
|
||||
_lib = libopus_loader(_filename)
|
||||
else:
|
||||
_lib = libopus_loader(ctypes.util.find_library("opus"))
|
||||
except Exception:
|
||||
_lib = None
|
||||
|
||||
|
||||
def load_opus(name):
|
||||
"""Loads the libopus shared library for use with voice.
|
||||
|
||||
If this function is not called then the library uses the function
|
||||
`ctypes.util.find_library`__ and then loads that one
|
||||
if available.
|
||||
|
||||
.. _find library: https://docs.python.org/3.5/library/ctypes.html#finding-shared-libraries
|
||||
__ `find library`_
|
||||
|
||||
Not loading a library leads to voice not working.
|
||||
|
||||
This function propagates the exceptions thrown.
|
||||
|
||||
Warning
|
||||
--------
|
||||
The bitness of the library must match the bitness of your python
|
||||
interpreter. If the library is 64-bit then your python interpreter
|
||||
must be 64-bit as well. Usually if there's a mismatch in bitness then
|
||||
the load will throw an exception.
|
||||
|
||||
Note
|
||||
----
|
||||
On Windows, the .dll extension is not necessary. However, on Linux
|
||||
the full extension is required to load the library, e.g. ``libopus.so.1``.
|
||||
On Linux however, `find library`_ will usually find the library automatically
|
||||
without you having to call this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: str
|
||||
The filename of the shared library.
|
||||
"""
|
||||
global _lib
|
||||
_lib = libopus_loader(name)
|
||||
|
||||
|
||||
def is_loaded():
|
||||
"""Function to check if opus lib is successfully loaded either
|
||||
via the ``ctypes.util.find_library`` call of :func:`load_opus`.
|
||||
|
||||
This must return ``True`` for voice to work.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Indicates if the opus library has been loaded.
|
||||
"""
|
||||
global _lib
|
||||
return _lib is not None
|
||||
|
||||
|
||||
class OpusError(DiscordException):
|
||||
"""An exception that is thrown for libopus related errors.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
code : :class:`int`
|
||||
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)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class OpusNotLoaded(DiscordException):
|
||||
"""An exception that is thrown for when libopus is not loaded."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Some constants...
|
||||
OK = 0
|
||||
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
|
||||
|
||||
band_ctl = {"narrow": 1101, "medium": 1102, "wide": 1103, "superwide": 1104, "full": 1105}
|
||||
|
||||
signal_ctl = {"auto": -1000, "voice": 3001, "music": 3002}
|
||||
|
||||
|
||||
class Encoder:
|
||||
SAMPLING_RATE = 48000
|
||||
CHANNELS = 2
|
||||
FRAME_LENGTH = 20
|
||||
SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16)
|
||||
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
|
||||
|
||||
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
|
||||
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
self.application = application
|
||||
|
||||
if not is_loaded():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
self._state = 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")
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, "_state"):
|
||||
_lib.opus_encoder_destroy(self._state)
|
||||
self._state = None
|
||||
|
||||
def _create_state(self):
|
||||
ret = ctypes.c_int()
|
||||
return _lib.opus_encoder_create(
|
||||
self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret)
|
||||
)
|
||||
|
||||
def set_bitrate(self, kbps):
|
||||
kbps = min(128, max(16, int(kbps)))
|
||||
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024)
|
||||
return kbps
|
||||
|
||||
def set_bandwidth(self, req):
|
||||
if req not in band_ctl:
|
||||
raise KeyError(
|
||||
"%r is not a valid bandwidth setting. Try one of: %s" % (req, ",".join(band_ctl))
|
||||
)
|
||||
|
||||
k = band_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
|
||||
|
||||
def set_signal_type(self, req):
|
||||
if req not in signal_ctl:
|
||||
raise KeyError(
|
||||
"%r is not a valid signal setting. Try one of: %s" % (req, ",".join(signal_ctl))
|
||||
)
|
||||
|
||||
k = signal_ctl[req]
|
||||
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
|
||||
|
||||
def set_fec(self, enabled=True):
|
||||
_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 encode(self, pcm, frame_size):
|
||||
max_data_bytes = len(pcm)
|
||||
pcm = ctypes.cast(pcm, c_int16_ptr)
|
||||
data = (ctypes.c_char * max_data_bytes)()
|
||||
|
||||
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
|
||||
|
||||
return array.array("b", data[:ret]).tobytes()
|
||||
@@ -1,636 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
|
||||
class Permissions:
|
||||
"""Wraps up the Discord permission value.
|
||||
|
||||
The properties provided are two way. You can set and retrieve individual
|
||||
bits using the properties as if they were regular bools. This allows
|
||||
you to edit permissions.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two permissions are equal.
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two permissions are not equal.
|
||||
.. describe:: x <= y
|
||||
|
||||
Checks if a permission is a subset of another permission.
|
||||
.. describe:: x >= y
|
||||
|
||||
Checks if a permission is a superset of another permission.
|
||||
.. describe:: x < y
|
||||
|
||||
Checks if a permission is a strict subset of another permission.
|
||||
.. describe:: x > y
|
||||
|
||||
Checks if a permission is a strict superset of another permission.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the permission's hash.
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(perm, value)`` pairs. This allows it
|
||||
to be, for example, constructed as a dict or a list of pairs.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
value
|
||||
The raw value. This value is a bit array field of a 53-bit integer
|
||||
representing the currently available permissions. You should query
|
||||
permissions via the properties rather than using this raw value.
|
||||
"""
|
||||
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, permissions=0):
|
||||
if not isinstance(permissions, int):
|
||||
raise TypeError(
|
||||
"Expected int parameter, received %s instead." % permissions.__class__.__name__
|
||||
)
|
||||
|
||||
self.value = permissions
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Permissions) and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Permissions value=%s>" % self.value
|
||||
|
||||
def _perm_iterator(self):
|
||||
for attr in dir(self):
|
||||
# check if it's a property, because if so it's a permission
|
||||
is_property = isinstance(getattr(self.__class__, attr), property)
|
||||
if is_property:
|
||||
yield (attr, getattr(self, attr))
|
||||
|
||||
def __iter__(self):
|
||||
return self._perm_iterator()
|
||||
|
||||
def is_subset(self, other):
|
||||
"""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(
|
||||
"cannot compare {} with {}".format(
|
||||
self.__class__.__name__, other.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def is_superset(self, other):
|
||||
"""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(
|
||||
"cannot compare {} with {}".format(
|
||||
self.__class__.__name__, other.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def is_strict_subset(self, other):
|
||||
"""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):
|
||||
"""Returns True if the permissions on other are a strict superset of those on self."""
|
||||
return self.is_superset(other) and self != other
|
||||
|
||||
__le__ = is_subset
|
||||
__ge__ = is_superset
|
||||
__lt__ = is_strict_subset
|
||||
__gt__ = is_strict_superset
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to False."""
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
permissions set to True."""
|
||||
return cls(0b01111111111101111111110111111111)
|
||||
|
||||
@classmethod
|
||||
def all_channel(cls):
|
||||
"""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:
|
||||
|
||||
- manage_guild
|
||||
- kick_members
|
||||
- ban_members
|
||||
- administrator
|
||||
- change_nicknames
|
||||
- manage_nicknames
|
||||
"""
|
||||
return cls(0b00110011111101111111110001010001)
|
||||
|
||||
@classmethod
|
||||
def general(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"General" permissions from the official Discord UI set to True."""
|
||||
return cls(0b01111100000000000000000010111111)
|
||||
|
||||
@classmethod
|
||||
def text(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Text" permissions from the official Discord UI set to True."""
|
||||
return cls(0b00000000000001111111110001000000)
|
||||
|
||||
@classmethod
|
||||
def voice(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
"Voice" permissions from the official Discord UI set to True."""
|
||||
return cls(0b00000011111100000000000100000000)
|
||||
|
||||
def update(self, **kwargs):
|
||||
r"""Bulk updates this permission object.
|
||||
|
||||
Allows you to set multiple attributes by using keyword
|
||||
arguments. The names must be equivalent to the properties
|
||||
listed. Extraneous key/value pairs will be silently ignored.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*\*kwargs
|
||||
A list of key/value pairs to bulk update permissions with.
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
try:
|
||||
is_property = isinstance(getattr(self.__class__, key), property)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
if is_property:
|
||||
setattr(self, key, value)
|
||||
|
||||
def _bit(self, index):
|
||||
return bool((self.value >> index) & 1)
|
||||
|
||||
def _set(self, index, value):
|
||||
if value is True:
|
||||
self.value |= 1 << index
|
||||
elif value is False:
|
||||
self.value &= ~(1 << index)
|
||||
else:
|
||||
raise TypeError("Value to set for Permissions must be a bool.")
|
||||
|
||||
def handle_overwrite(self, allow, deny):
|
||||
# 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
|
||||
# And then we have the last one which is 'allowed', e.g. 0101
|
||||
# We want original OP denied to end up resulting in
|
||||
# whatever is in denied to be set to 0.
|
||||
# So 1010 OP 1111 -> 0000
|
||||
# Then we take this value and look at the allowed values.
|
||||
# And whatever is allowed is set to 1.
|
||||
# So 0000 OP2 0101 -> 0101
|
||||
# The OP is base & ~denied.
|
||||
# The OP2 is base | allowed.
|
||||
self.value = (self.value & ~deny) | allow
|
||||
|
||||
@property
|
||||
def create_instant_invite(self):
|
||||
"""Returns True if the user can create instant invites."""
|
||||
return self._bit(0)
|
||||
|
||||
@create_instant_invite.setter
|
||||
def create_instant_invite(self, value):
|
||||
self._set(0, value)
|
||||
|
||||
@property
|
||||
def kick_members(self):
|
||||
"""Returns True if the user can kick users from the guild."""
|
||||
return self._bit(1)
|
||||
|
||||
@kick_members.setter
|
||||
def kick_members(self, value):
|
||||
self._set(1, value)
|
||||
|
||||
@property
|
||||
def ban_members(self):
|
||||
"""Returns True if a user can ban users from the guild."""
|
||||
return self._bit(2)
|
||||
|
||||
@ban_members.setter
|
||||
def ban_members(self, value):
|
||||
self._set(2, value)
|
||||
|
||||
@property
|
||||
def administrator(self):
|
||||
"""Returns True if a user is an administrator. This role overrides all other permissions.
|
||||
|
||||
This also bypasses all channel-specific overrides.
|
||||
"""
|
||||
return self._bit(3)
|
||||
|
||||
@administrator.setter
|
||||
def administrator(self, value):
|
||||
self._set(3, value)
|
||||
|
||||
@property
|
||||
def manage_channels(self):
|
||||
"""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 self._bit(4)
|
||||
|
||||
@manage_channels.setter
|
||||
def manage_channels(self, value):
|
||||
self._set(4, value)
|
||||
|
||||
@property
|
||||
def manage_guild(self):
|
||||
"""Returns True if a user can edit guild properties."""
|
||||
return self._bit(5)
|
||||
|
||||
@manage_guild.setter
|
||||
def manage_guild(self, value):
|
||||
self._set(5, value)
|
||||
|
||||
@property
|
||||
def add_reactions(self):
|
||||
"""Returns True if a user can add reactions to messages."""
|
||||
return self._bit(6)
|
||||
|
||||
@add_reactions.setter
|
||||
def add_reactions(self, value):
|
||||
self._set(6, value)
|
||||
|
||||
@property
|
||||
def view_audit_log(self):
|
||||
"""Returns True if a user can view the guild's audit log."""
|
||||
return self._bit(7)
|
||||
|
||||
@view_audit_log.setter
|
||||
def view_audit_log(self, value):
|
||||
self._set(7, value)
|
||||
|
||||
@property
|
||||
def priority_speaker(self):
|
||||
"""Returns True if a user can be more easily heard while talking."""
|
||||
return self._bit(8)
|
||||
|
||||
@priority_speaker.setter
|
||||
def priority_speaker(self, value):
|
||||
self._set(8, value)
|
||||
|
||||
# 1 unused
|
||||
|
||||
@property
|
||||
def read_messages(self):
|
||||
"""Returns True if a user can read messages from all or specific text channels."""
|
||||
return self._bit(10)
|
||||
|
||||
@read_messages.setter
|
||||
def read_messages(self, value):
|
||||
self._set(10, value)
|
||||
|
||||
@property
|
||||
def send_messages(self):
|
||||
"""Returns True if a user can send messages from all or specific text channels."""
|
||||
return self._bit(11)
|
||||
|
||||
@send_messages.setter
|
||||
def send_messages(self, value):
|
||||
self._set(11, value)
|
||||
|
||||
@property
|
||||
def send_tts_messages(self):
|
||||
"""Returns True if a user can send TTS messages from all or specific text channels."""
|
||||
return self._bit(12)
|
||||
|
||||
@send_tts_messages.setter
|
||||
def send_tts_messages(self, value):
|
||||
self._set(12, value)
|
||||
|
||||
@property
|
||||
def manage_messages(self):
|
||||
"""Returns True if a user can delete or pin messages in a text channel. Note that there are currently no ways to edit other people's messages."""
|
||||
return self._bit(13)
|
||||
|
||||
@manage_messages.setter
|
||||
def manage_messages(self, value):
|
||||
self._set(13, value)
|
||||
|
||||
@property
|
||||
def embed_links(self):
|
||||
"""Returns True if a user's messages will automatically be embedded by Discord."""
|
||||
return self._bit(14)
|
||||
|
||||
@embed_links.setter
|
||||
def embed_links(self, value):
|
||||
self._set(14, value)
|
||||
|
||||
@property
|
||||
def attach_files(self):
|
||||
"""Returns True if a user can send files in their messages."""
|
||||
return self._bit(15)
|
||||
|
||||
@attach_files.setter
|
||||
def attach_files(self, value):
|
||||
self._set(15, value)
|
||||
|
||||
@property
|
||||
def read_message_history(self):
|
||||
"""Returns True if a user can read a text channel's previous messages."""
|
||||
return self._bit(16)
|
||||
|
||||
@read_message_history.setter
|
||||
def read_message_history(self, value):
|
||||
self._set(16, value)
|
||||
|
||||
@property
|
||||
def mention_everyone(self):
|
||||
"""Returns True if a user's @everyone or @here will mention everyone in the text channel."""
|
||||
return self._bit(17)
|
||||
|
||||
@mention_everyone.setter
|
||||
def mention_everyone(self, value):
|
||||
self._set(17, value)
|
||||
|
||||
@property
|
||||
def external_emojis(self):
|
||||
"""Returns True if a user can use emojis from other guilds."""
|
||||
return self._bit(18)
|
||||
|
||||
@external_emojis.setter
|
||||
def external_emojis(self, value):
|
||||
self._set(18, value)
|
||||
|
||||
# 1 unused
|
||||
|
||||
@property
|
||||
def connect(self):
|
||||
"""Returns True if a user can connect to a voice channel."""
|
||||
return self._bit(20)
|
||||
|
||||
@connect.setter
|
||||
def connect(self, value):
|
||||
self._set(20, value)
|
||||
|
||||
@property
|
||||
def speak(self):
|
||||
"""Returns True if a user can speak in a voice channel."""
|
||||
return self._bit(21)
|
||||
|
||||
@speak.setter
|
||||
def speak(self, value):
|
||||
self._set(21, value)
|
||||
|
||||
@property
|
||||
def mute_members(self):
|
||||
"""Returns True if a user can mute other users."""
|
||||
return self._bit(22)
|
||||
|
||||
@mute_members.setter
|
||||
def mute_members(self, value):
|
||||
self._set(22, value)
|
||||
|
||||
@property
|
||||
def deafen_members(self):
|
||||
"""Returns True if a user can deafen other users."""
|
||||
return self._bit(23)
|
||||
|
||||
@deafen_members.setter
|
||||
def deafen_members(self, value):
|
||||
self._set(23, value)
|
||||
|
||||
@property
|
||||
def move_members(self):
|
||||
"""Returns True if a user can move users between other voice channels."""
|
||||
return self._bit(24)
|
||||
|
||||
@move_members.setter
|
||||
def move_members(self, value):
|
||||
self._set(24, value)
|
||||
|
||||
@property
|
||||
def use_voice_activation(self):
|
||||
"""Returns True if a user can use voice activation in voice channels."""
|
||||
return self._bit(25)
|
||||
|
||||
@use_voice_activation.setter
|
||||
def use_voice_activation(self, value):
|
||||
self._set(25, value)
|
||||
|
||||
@property
|
||||
def change_nickname(self):
|
||||
"""Returns True if a user can change their nickname in the guild."""
|
||||
return self._bit(26)
|
||||
|
||||
@change_nickname.setter
|
||||
def change_nickname(self, value):
|
||||
self._set(26, value)
|
||||
|
||||
@property
|
||||
def manage_nicknames(self):
|
||||
"""Returns True if a user can change other user's nickname in the guild."""
|
||||
return self._bit(27)
|
||||
|
||||
@manage_nicknames.setter
|
||||
def manage_nicknames(self, value):
|
||||
self._set(27, value)
|
||||
|
||||
@property
|
||||
def manage_roles(self):
|
||||
"""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 self._bit(28)
|
||||
|
||||
@manage_roles.setter
|
||||
def manage_roles(self, value):
|
||||
self._set(28, value)
|
||||
|
||||
@property
|
||||
def manage_webhooks(self):
|
||||
"""Returns True if a user can create, edit, or delete webhooks."""
|
||||
return self._bit(29)
|
||||
|
||||
@manage_webhooks.setter
|
||||
def manage_webhooks(self, value):
|
||||
self._set(29, value)
|
||||
|
||||
@property
|
||||
def manage_emojis(self):
|
||||
"""Returns True if a user can create, edit, or delete emojis."""
|
||||
return self._bit(30)
|
||||
|
||||
@manage_emojis.setter
|
||||
def manage_emojis(self, value):
|
||||
self._set(30, value)
|
||||
|
||||
# 1 unused
|
||||
|
||||
# after these 32 bits, there's 21 more unused ones technically
|
||||
|
||||
|
||||
def augment_from_permissions(cls):
|
||||
cls.VALID_NAMES = {
|
||||
name for name in dir(Permissions) if isinstance(getattr(Permissions, name), property)
|
||||
}
|
||||
|
||||
# make descriptors for all the valid names
|
||||
for name in cls.VALID_NAMES:
|
||||
# god bless Python
|
||||
def getter(self, x=name):
|
||||
return self._values.get(x)
|
||||
|
||||
def setter(self, value, x=name):
|
||||
self._set(x, value)
|
||||
|
||||
prop = property(getter, setter)
|
||||
setattr(cls, name, prop)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
@augment_from_permissions
|
||||
class PermissionOverwrite:
|
||||
r"""A type that is used to represent a channel specific permission.
|
||||
|
||||
Unlike a regular :class:`Permissions`\, the default value of a
|
||||
permission is equivalent to ``None`` and not ``False``. Setting
|
||||
a value to ``False`` is **explicitly** denying that permission,
|
||||
while setting a value to ``True`` is **explicitly** allowing
|
||||
that permission.
|
||||
|
||||
The values supported by this are the same as :class:`Permissions`
|
||||
with the added possibility of it being set to ``None``.
|
||||
|
||||
Supported operations:
|
||||
|
||||
+-----------+------------------------------------------+
|
||||
| Operation | Description |
|
||||
+===========+==========================================+
|
||||
| iter(x) | Returns an iterator of (perm, value) |
|
||||
| | pairs. This allows this class to be used |
|
||||
| | as an iterable in e.g. set/list/dict |
|
||||
| | constructions. |
|
||||
+-----------+------------------------------------------+
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*\*kwargs
|
||||
Set the value of permissions by their name.
|
||||
"""
|
||||
|
||||
__slots__ = ("_values",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._values = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_NAMES:
|
||||
raise ValueError("no permission called {0}.".format(key))
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
def _set(self, key, value):
|
||||
if value not in (True, None, False):
|
||||
raise TypeError(
|
||||
"Expected bool or NoneType, received {0.__class__.__name__}".format(value)
|
||||
)
|
||||
|
||||
self._values[key] = value
|
||||
|
||||
def pair(self):
|
||||
"""Returns the (allow, deny) pair from this overwrite.
|
||||
|
||||
The value of these pairs is :class:`Permissions`.
|
||||
"""
|
||||
|
||||
allow = Permissions.none()
|
||||
deny = Permissions.none()
|
||||
|
||||
for key, value in self._values.items():
|
||||
if value is True:
|
||||
setattr(allow, key, True)
|
||||
elif value is False:
|
||||
setattr(deny, key, True)
|
||||
|
||||
return allow, deny
|
||||
|
||||
@classmethod
|
||||
def from_pair(cls, allow, deny):
|
||||
"""Creates an overwrite from an allow/deny pair of :class:`Permissions`."""
|
||||
ret = cls()
|
||||
for key, value in allow:
|
||||
if value is True:
|
||||
setattr(ret, key, True)
|
||||
|
||||
for key, value in deny:
|
||||
if value is True:
|
||||
setattr(ret, key, False)
|
||||
|
||||
return ret
|
||||
|
||||
def is_empty(self):
|
||||
"""Checks if the permission overwrite is currently empty.
|
||||
|
||||
An empty permission overwrite is one that has no overwrites set
|
||||
to True or False.
|
||||
"""
|
||||
return all(x is None for x in self._values.values())
|
||||
|
||||
def update(self, **kwargs):
|
||||
r"""Bulk updates this permission overwrite object.
|
||||
|
||||
Allows you to set multiple attributes by using keyword
|
||||
arguments. The names must be equivalent to the properties
|
||||
listed. Extraneous key/value pairs will be silently ignored.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
\*\*kwargs
|
||||
A list of key/value pairs to bulk update with.
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_NAMES:
|
||||
continue
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for key in self.VALID_NAMES:
|
||||
yield key, self._values.get(key)
|
||||
@@ -1,356 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import subprocess
|
||||
import audioop
|
||||
import logging
|
||||
import shlex
|
||||
import time
|
||||
|
||||
from .errors import ClientException
|
||||
from .opus import Encoder as OpusEncoder
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ["AudioSource", "PCMAudio", "FFmpegPCMAudio", "PCMVolumeTransformer"]
|
||||
|
||||
|
||||
class AudioSource:
|
||||
"""Represents an audio stream.
|
||||
|
||||
The audio stream can be Opus encoded or not, however if the audio stream
|
||||
is not Opus encoded then the audio format must be 16-bit 48KHz stereo PCM.
|
||||
|
||||
.. warning::
|
||||
|
||||
The audio source reads are done in a separate thread.
|
||||
"""
|
||||
|
||||
def read(self):
|
||||
"""Reads 20ms worth of audio.
|
||||
|
||||
Subclasses must implement this.
|
||||
|
||||
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
|
||||
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).
|
||||
|
||||
Returns
|
||||
--------
|
||||
bytes
|
||||
A bytes like object that represents the PCM or Opus data.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_opus(self):
|
||||
"""Checks if the audio source is already encoded in Opus.
|
||||
|
||||
Defaults to ``False``.
|
||||
"""
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""Called when clean-up is needed to be done.
|
||||
|
||||
Useful for clearing buffer data or processes after
|
||||
it is done playing audio.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class PCMAudio(AudioSource):
|
||||
"""Represents raw 16-bit 48KHz stereo PCM audio source.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
stream: file-like object
|
||||
A file-like object that reads byte data representing raw PCM.
|
||||
"""
|
||||
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
def read(self):
|
||||
ret = self.stream.read(OpusEncoder.FRAME_SIZE)
|
||||
if len(ret) != OpusEncoder.FRAME_SIZE:
|
||||
return b""
|
||||
return ret
|
||||
|
||||
|
||||
class FFmpegPCMAudio(AudioSource):
|
||||
"""An audio source from FFmpeg (or AVConv).
|
||||
|
||||
This launches a sub-process to a specific input file given.
|
||||
|
||||
.. warning::
|
||||
|
||||
You must have the ffmpeg or avconv executable in your path environment
|
||||
variable in order for this to work.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
source: Union[str, BinaryIO]
|
||||
The input that ffmpeg will take and convert to PCM bytes.
|
||||
If ``pipe`` is True then this is a file-like object that is
|
||||
passed to the stdin of ffmpeg.
|
||||
executable: str
|
||||
The executable name (and path) to use. Defaults to ``ffmpeg``.
|
||||
pipe: bool
|
||||
If true, denotes that ``source`` parameter will be passed
|
||||
to the stdin of ffmpeg. Defaults to ``False``.
|
||||
stderr: Optional[BinaryIO]
|
||||
A file-like object to pass to the Popen constructor.
|
||||
Could also be an instance of ``subprocess.PIPE``.
|
||||
options: Optional[str]
|
||||
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
|
||||
before_options: Optional[str]
|
||||
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
|
||||
|
||||
Raises
|
||||
--------
|
||||
ClientException
|
||||
The subprocess failed to be created.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source,
|
||||
*,
|
||||
executable="ffmpeg",
|
||||
pipe=False,
|
||||
stderr=None,
|
||||
before_options=None,
|
||||
options=None
|
||||
):
|
||||
stdin = None if not pipe else source
|
||||
|
||||
args = [executable]
|
||||
|
||||
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"))
|
||||
|
||||
if isinstance(options, str):
|
||||
args.extend(shlex.split(options))
|
||||
|
||||
args.append("pipe:1")
|
||||
|
||||
self._process = None
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr
|
||||
)
|
||||
self._stdout = self._process.stdout
|
||||
except FileNotFoundError:
|
||||
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
|
||||
|
||||
def read(self):
|
||||
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
|
||||
if len(ret) != OpusEncoder.FRAME_SIZE:
|
||||
return b""
|
||||
return ret
|
||||
|
||||
def cleanup(self):
|
||||
proc = self._process
|
||||
if proc is None:
|
||||
return
|
||||
|
||||
log.info("Preparing to terminate ffmpeg process %s.", proc.pid)
|
||||
proc.kill()
|
||||
if proc.poll() is None:
|
||||
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,
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
"ffmpeg process %s successfully terminated with return code of %s.",
|
||||
proc.pid,
|
||||
proc.returncode,
|
||||
)
|
||||
|
||||
self._process = None
|
||||
|
||||
|
||||
class PCMVolumeTransformer(AudioSource):
|
||||
"""Transforms a previous :class:`AudioSource` to have volume controls.
|
||||
|
||||
This does not work on audio sources that have :meth:`AudioSource.is_opus`
|
||||
set to ``True``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
original: :class:`AudioSource`
|
||||
The original AudioSource to transform.
|
||||
volume: float
|
||||
The initial volume to set it to.
|
||||
See :attr:`volume` for more info.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
Not an audio source.
|
||||
ClientException
|
||||
The audio source is opus encoded.
|
||||
"""
|
||||
|
||||
def __init__(self, original, volume=1.0):
|
||||
if not isinstance(original, AudioSource):
|
||||
raise TypeError("expected AudioSource not {0.__class__.__name__}.".format(original))
|
||||
|
||||
if original.is_opus():
|
||||
raise ClientException("AudioSource must not be Opus encoded.")
|
||||
|
||||
self.original = original
|
||||
self.volume = volume
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""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):
|
||||
self._volume = max(value, 0.0)
|
||||
|
||||
def cleanup(self):
|
||||
self.original.cleanup()
|
||||
|
||||
def read(self):
|
||||
ret = self.original.read()
|
||||
return audioop.mul(ret, 2, min(self._volume, 2.0))
|
||||
|
||||
|
||||
class AudioPlayer(threading.Thread):
|
||||
DELAY = OpusEncoder.FRAME_LENGTH / 1000.0
|
||||
|
||||
def __init__(self, source, client, *, after=None):
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.source = source
|
||||
self.client = client
|
||||
self.after = 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()
|
||||
|
||||
if after is not None and not callable(after):
|
||||
raise TypeError('Expected a callable for the "after" parameter.')
|
||||
|
||||
def _do_run(self):
|
||||
self.loops = 0
|
||||
self._start = time.time()
|
||||
|
||||
# getattr lookup speed ups
|
||||
play_audio = self.client.send_audio_packet
|
||||
|
||||
while not self._end.is_set():
|
||||
# are we paused?
|
||||
if not self._resumed.is_set():
|
||||
# wait until we aren't
|
||||
self._resumed.wait()
|
||||
continue
|
||||
|
||||
# are we disconnected from voice?
|
||||
if not self._connected.is_set():
|
||||
# wait until we are connected
|
||||
self._connected.wait()
|
||||
# reset our internal data
|
||||
self.loops = 0
|
||||
self._start = time.time()
|
||||
|
||||
self.loops += 1
|
||||
data = self.source.read()
|
||||
|
||||
if not data:
|
||||
self.stop()
|
||||
break
|
||||
|
||||
play_audio(data, encode=not self.source.is_opus())
|
||||
next_time = self._start + self.DELAY * self.loops
|
||||
delay = max(0, self.DELAY + (next_time - time.time()))
|
||||
time.sleep(delay)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._do_run()
|
||||
except Exception as exc:
|
||||
self._current_error = exc
|
||||
self.stop()
|
||||
finally:
|
||||
self.source.cleanup()
|
||||
self._call_after()
|
||||
|
||||
def _call_after(self):
|
||||
if self.after is not None:
|
||||
try:
|
||||
self.after(self._current_error)
|
||||
except Exception:
|
||||
log.exception("Calling the after function failed.")
|
||||
|
||||
def stop(self):
|
||||
self._end.set()
|
||||
self._resumed.set()
|
||||
|
||||
def pause(self):
|
||||
self._resumed.clear()
|
||||
|
||||
def resume(self):
|
||||
self.loops = 0
|
||||
self._start = time.time()
|
||||
self._resumed.set()
|
||||
|
||||
def is_playing(self):
|
||||
return self._resumed.is_set() and not self._end.is_set()
|
||||
|
||||
def is_paused(self):
|
||||
return not self._end.is_set() and not self._resumed.is_set()
|
||||
|
||||
def _set_source(self, source):
|
||||
with self._lock:
|
||||
self.pause()
|
||||
self.source = source
|
||||
self.resume()
|
||||
@@ -1,151 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 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.
|
||||
"""
|
||||
|
||||
|
||||
class RawMessageDeleteEvent:
|
||||
"""Represents the event payload for a :func:`on_raw_message_delete` event.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
channel_id: :class:`int`
|
||||
The channel ID where the deletion took place.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the deletion took place, if applicable.
|
||||
message_id: :class:`int`
|
||||
The message ID that got deleted.
|
||||
"""
|
||||
|
||||
__slots__ = ("message_id", "channel_id", "guild_id")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data["id"])
|
||||
self.channel_id = int(data["channel_id"])
|
||||
|
||||
try:
|
||||
self.guild_id = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
|
||||
|
||||
class RawBulkMessageDeleteEvent:
|
||||
"""Represents the event payload for a :func:`on_raw_bulk_message_delete` event.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message_ids: Set[:class:`int`]
|
||||
A :class:`set` of the message IDs that were deleted.
|
||||
channel_id: :class:`int`
|
||||
The channel ID where the message got deleted.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the message got deleted, if applicable.
|
||||
"""
|
||||
|
||||
__slots__ = ("message_ids", "channel_id", "guild_id")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_ids = {int(x) for x in data.get("ids", [])}
|
||||
self.channel_id = int(data["channel_id"])
|
||||
|
||||
try:
|
||||
self.guild_id = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
|
||||
|
||||
class RawMessageUpdateEvent:
|
||||
"""Represents the payload for a :func:`on_raw_message_edit` event.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message_id: :class:`int`
|
||||
The message ID that got updated.
|
||||
data: :class:`dict`
|
||||
The raw data given by the
|
||||
`gateway <https://discordapp.com/developers/docs/topics/gateway#message-update>`_
|
||||
"""
|
||||
|
||||
__slots__ = ("message_id", "data")
|
||||
|
||||
def __init__(self, data):
|
||||
self.message_id = int(data["id"])
|
||||
self.data = data
|
||||
|
||||
|
||||
class RawReactionActionEvent:
|
||||
"""Represents the payload for a :func:`on_raw_reaction_add` or
|
||||
:func:`on_raw_reaction_remove` event.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message_id: :class:`int`
|
||||
The message ID that got or lost a reaction.
|
||||
user_id: :class:`int`
|
||||
The user ID who added or removed the reaction.
|
||||
channel_id: :class:`int`
|
||||
The channel ID where the reaction got added or removed.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the reaction got added or removed, if applicable.
|
||||
emoji: :class:`PartialEmoji`
|
||||
The custom or unicode emoji being used.
|
||||
"""
|
||||
|
||||
__slots__ = ("message_id", "user_id", "channel_id", "guild_id", "emoji")
|
||||
|
||||
def __init__(self, data, emoji):
|
||||
self.message_id = int(data["message_id"])
|
||||
self.channel_id = int(data["channel_id"])
|
||||
self.user_id = int(data["user_id"])
|
||||
self.emoji = emoji
|
||||
|
||||
try:
|
||||
self.guild_id = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
|
||||
|
||||
class RawReactionClearEvent:
|
||||
"""Represents the payload for a :func:`on_raw_reaction_clear` event.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message_id: :class:`int`
|
||||
The message ID that got its reactions cleared.
|
||||
channel_id: :class:`int`
|
||||
The channel ID where the reactions got cleared.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID where the reactions got cleared.
|
||||
"""
|
||||
|
||||
__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"])
|
||||
|
||||
try:
|
||||
self.guild_id = int(data["guild_id"])
|
||||
except KeyError:
|
||||
self.guild_id = None
|
||||
@@ -1,151 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 .iterators import ReactionIterator
|
||||
|
||||
|
||||
class Reaction:
|
||||
"""Represents a reaction to a message.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two reactions are equal. This works by checking if the emoji
|
||||
is the same. So two messages with the same reaction will be considered
|
||||
"equal".
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two reactions are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the reaction's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the string form of the reaction's emoji.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
emoji: :class:`Emoji` or :class:`str`
|
||||
The reaction emoji. May be a custom emoji, or a unicode emoji.
|
||||
count: :class:`int`
|
||||
Number of times this reaction was made
|
||||
me: :class:`bool`
|
||||
If the user sent this 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")
|
||||
|
||||
@property
|
||||
def custom_emoji(self):
|
||||
""":class:`bool`: If this is a custom emoji."""
|
||||
return not isinstance(self.emoji, str)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and other.emoji == self.emoji
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.emoji != self.emoji
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.emoji)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.emoji)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>".format(self)
|
||||
|
||||
def users(self, limit=None, after=None):
|
||||
"""Returns an :class:`AsyncIterator` representing the users that have reacted to the message.
|
||||
|
||||
The ``after`` parameter must represent a member
|
||||
and meet the :class:`abc.Snowflake` abc.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
limit: int
|
||||
The maximum number of results to return.
|
||||
If not provided, returns all the users who
|
||||
reacted to the message.
|
||||
after: :class:`abc.Snowflake`
|
||||
For pagination, reactions are sorted by member.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Getting the users for the reaction failed.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Usage ::
|
||||
|
||||
# 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))
|
||||
|
||||
Flattening into a list: ::
|
||||
|
||||
users = await reaction.users().flatten()
|
||||
# users is now a list...
|
||||
winner = random.choice(users)
|
||||
await channel.send('{} has won the raffle.'.format(winner))
|
||||
|
||||
Yields
|
||||
--------
|
||||
Union[:class:`User`, :class:`Member`]
|
||||
The member (if retrievable) or the user that has reacted
|
||||
to this message. The case where it can be a :class:`Member` is
|
||||
in a guild message context. Sometimes it can be a :class:`User`
|
||||
if the member has left the guild.
|
||||
"""
|
||||
|
||||
if self.custom_emoji:
|
||||
emoji = "{0.name}:{0.id}".format(self.emoji)
|
||||
else:
|
||||
emoji = self.emoji
|
||||
|
||||
if limit is None:
|
||||
limit = self.count
|
||||
|
||||
return ReactionIterator(self.message, emoji, limit, after)
|
||||
@@ -1,79 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 .enums import RelationshipType, try_enum
|
||||
|
||||
|
||||
class Relationship:
|
||||
"""Represents a relationship in Discord.
|
||||
|
||||
A relationship is like a friendship, a person who is blocked, etc.
|
||||
Only non-bot accounts can have relationships.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
user: :class:`User`
|
||||
The user you have the relationship with.
|
||||
type: :class:`RelationshipType`
|
||||
The type of relationship you have.
|
||||
"""
|
||||
|
||||
__slots__ = ("type", "user", "_state")
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.type = try_enum(RelationshipType, data["type"])
|
||||
self.user = state.store_user(data["user"])
|
||||
|
||||
def __repr__(self):
|
||||
return "<Relationship user={0.user!r} type={0.type!r}>".format(self)
|
||||
|
||||
async def delete(self):
|
||||
"""|coro|
|
||||
|
||||
Deletes the relationship.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Deleting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.remove_relationship(self.user.id)
|
||||
|
||||
async def accept(self):
|
||||
"""|coro|
|
||||
|
||||
Accepts the relationship request. e.g. accepting a
|
||||
friend request.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Accepting the relationship failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.user.id)
|
||||
297
discord/role.py
297
discord/role.py
@@ -1,297 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 .permissions import Permissions
|
||||
from .errors import InvalidArgument
|
||||
from .colour import Colour
|
||||
from .mixins import Hashable
|
||||
from .utils import snowflake_time
|
||||
|
||||
|
||||
class Role(Hashable):
|
||||
"""Represents a Discord role in a :class:`Guild`.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two roles are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two roles are not equal.
|
||||
|
||||
.. describe:: x > y
|
||||
|
||||
Checks if a role is higher than another in the hierarchy.
|
||||
|
||||
.. describe:: x < y
|
||||
|
||||
Checks if a role is lower than another in the hierarchy.
|
||||
|
||||
.. describe:: x >= y
|
||||
|
||||
Checks if a role is higher or equal to another in the hierarchy.
|
||||
|
||||
.. describe:: x <= y
|
||||
|
||||
Checks if a role is lower or equal to another in the hierarchy.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the role's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the role's name.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID for the role.
|
||||
name: :class:`str`
|
||||
The name of the role.
|
||||
permissions: :class:`Permissions`
|
||||
Represents the role's permissions.
|
||||
guild: :class:`Guild`
|
||||
The guild the role belongs to.
|
||||
colour: :class:`Colour`
|
||||
Represents the role colour. An alias exists under ``color``.
|
||||
hoist: :class:`bool`
|
||||
Indicates if the role will be displayed separately from other members.
|
||||
position: :class:`int`
|
||||
The position of the role. This number is usually positive. The bottom
|
||||
role has a position of 0.
|
||||
managed: :class:`bool`
|
||||
Indicates if the role is managed by the guild through some form of
|
||||
integrations such as Twitch.
|
||||
mentionable: :class:`bool`
|
||||
Indicates if the role can be mentioned by users.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"id",
|
||||
"name",
|
||||
"permissions",
|
||||
"color",
|
||||
"colour",
|
||||
"position",
|
||||
"managed",
|
||||
"mentionable",
|
||||
"hoist",
|
||||
"guild",
|
||||
"_state",
|
||||
)
|
||||
|
||||
def __init__(self, *, guild, state, data):
|
||||
self.guild = guild
|
||||
self._state = state
|
||||
self.id = int(data["id"])
|
||||
self._update(data)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return "<Role id={0.id} name={0.name!r}>".format(self)
|
||||
|
||||
def __lt__(self, other):
|
||||
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.")
|
||||
|
||||
# the @everyone role is always the lowest role in hierarchy
|
||||
guild_id = self.guild.id
|
||||
if self.id == guild_id:
|
||||
# everyone_role < everyone_role -> False
|
||||
return other.id != guild_id
|
||||
|
||||
if self.position < other.position:
|
||||
return True
|
||||
|
||||
if self.position == other.position:
|
||||
return int(self.id) > int(other.id)
|
||||
|
||||
return False
|
||||
|
||||
def __le__(self, other):
|
||||
r = Role.__lt__(other, self)
|
||||
if r is NotImplemented:
|
||||
return NotImplemented
|
||||
return not r
|
||||
|
||||
def __gt__(self, other):
|
||||
return Role.__lt__(other, self)
|
||||
|
||||
def __ge__(self, other):
|
||||
r = Role.__lt__(self, other)
|
||||
if r is NotImplemented:
|
||||
return NotImplemented
|
||||
return not r
|
||||
|
||||
def _update(self, data):
|
||||
self.name = data["name"]
|
||||
self.permissions = Permissions(data.get("permissions", 0))
|
||||
self.position = data.get("position", 0)
|
||||
self.colour = Colour(data.get("color", 0))
|
||||
self.hoist = data.get("hoist", False)
|
||||
self.managed = data.get("managed", False)
|
||||
self.mentionable = data.get("mentionable", False)
|
||||
self.color = self.colour
|
||||
|
||||
def is_default(self):
|
||||
"""Checks if the role is the default role."""
|
||||
return self.guild.id == self.id
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the role's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
"""Returns a string that allows you to mention a role."""
|
||||
return "<@&%s>" % self.id
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""Returns a :class:`list` of :class:`Member` with this role."""
|
||||
all_members = self.guild.members
|
||||
if self.is_default():
|
||||
return all_members
|
||||
|
||||
role_id = self.id
|
||||
return [member for member in all_members if member._roles.has(role_id)]
|
||||
|
||||
async def _move(self, position, reason):
|
||||
if position <= 0:
|
||||
raise InvalidArgument("Cannot move role to position 0 or below")
|
||||
|
||||
if self.is_default():
|
||||
raise InvalidArgument("Cannot move default role")
|
||||
|
||||
if self.position == position:
|
||||
return # Save discord the extra request.
|
||||
|
||||
http = self._state.http
|
||||
|
||||
change_range = range(min(self.position, position), max(self.position, position) + 1)
|
||||
roles = [
|
||||
r.id for r in self.guild.roles[1:] if r.position in change_range and r.id != self.id
|
||||
]
|
||||
|
||||
if self.position > position:
|
||||
roles.insert(0, self.id)
|
||||
else:
|
||||
roles.append(self.id)
|
||||
|
||||
payload = [{"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):
|
||||
"""|coro|
|
||||
|
||||
Edits the role.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
use this.
|
||||
|
||||
All fields are optional.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: str
|
||||
The new role name to change to.
|
||||
permissions: :class:`Permissions`
|
||||
The new permissions to change to.
|
||||
colour: :class:`Colour`
|
||||
The new colour to change to. (aliased to color as well)
|
||||
hoist: bool
|
||||
Indicates if the role should be shown separately in the member list.
|
||||
mentionable: bool
|
||||
Indicates if the role should be mentionable by others.
|
||||
position: int
|
||||
The new role's position. This must be below your top role's
|
||||
position or it will fail.
|
||||
reason: Optional[str]
|
||||
The reason for editing this role. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
You do not have permissions to change the role.
|
||||
HTTPException
|
||||
Editing the role failed.
|
||||
InvalidArgument
|
||||
An invalid position was given or the default
|
||||
role was asked to be moved.
|
||||
"""
|
||||
|
||||
position = fields.get("position")
|
||||
if position is not None:
|
||||
await self._move(position, reason=reason)
|
||||
self.position = position
|
||||
|
||||
try:
|
||||
colour = fields["colour"]
|
||||
except KeyError:
|
||||
colour = fields.get("color", self.colour)
|
||||
|
||||
payload = {
|
||||
"name": fields.get("name", self.name),
|
||||
"permissions": fields.get("permissions", self.permissions).value,
|
||||
"color": colour.value,
|
||||
"hoist": fields.get("hoist", self.hoist),
|
||||
"mentionable": fields.get("mentionable", self.mentionable),
|
||||
}
|
||||
|
||||
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
|
||||
self._update(data)
|
||||
|
||||
async def delete(self, *, reason=None):
|
||||
"""|coro|
|
||||
|
||||
Deletes the role.
|
||||
|
||||
You must have the :attr:`~Permissions.manage_roles` permission to
|
||||
use this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
reason: Optional[str]
|
||||
The reason for deleting this role. Shows up on the audit log.
|
||||
|
||||
Raises
|
||||
--------
|
||||
Forbidden
|
||||
You do not have permissions to delete the role.
|
||||
HTTPException
|
||||
Deleting the role failed.
|
||||
"""
|
||||
|
||||
await self._state.http.delete_role(self.guild.id, self.id, reason=reason)
|
||||
370
discord/shard.py
370
discord/shard.py
@@ -1,370 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import websockets
|
||||
|
||||
from .state import AutoShardedConnectionState
|
||||
from .client import Client
|
||||
from .gateway import *
|
||||
from .errors import ClientException, InvalidArgument
|
||||
from . import utils
|
||||
from .enums import Status
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Shard:
|
||||
def __init__(self, ws, client):
|
||||
self.ws = ws
|
||||
self._client = client
|
||||
self.loop = self._client.loop
|
||||
self._current = self.loop.create_future()
|
||||
self._current.set_result(None) # we just need an already done future
|
||||
self._pending = asyncio.Event(loop=self.loop)
|
||||
self._pending_task = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.ws.shard_id
|
||||
|
||||
def is_pending(self):
|
||||
return not self._pending.is_set()
|
||||
|
||||
def complete_pending_reads(self):
|
||||
self._pending.set()
|
||||
|
||||
async def _pending_reads(self):
|
||||
try:
|
||||
while self.is_pending():
|
||||
await self.poll()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def launch_pending_reads(self):
|
||||
self._pending_task = asyncio.ensure_future(self._pending_reads(), loop=self.loop)
|
||||
|
||||
def wait(self):
|
||||
return self._pending_task
|
||||
|
||||
async def poll(self):
|
||||
try:
|
||||
await self.ws.poll_event()
|
||||
except ResumeWebSocket:
|
||||
log.info("Got a request to RESUME the websocket at Shard ID %s.", self.id)
|
||||
coro = DiscordWebSocket.from_client(
|
||||
self._client,
|
||||
resume=True,
|
||||
shard_id=self.id,
|
||||
session=self.ws.session_id,
|
||||
sequence=self.ws.sequence,
|
||||
)
|
||||
self.ws = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop)
|
||||
|
||||
def get_future(self):
|
||||
if self._current.done():
|
||||
self._current = asyncio.ensure_future(self.poll(), loop=self.loop)
|
||||
|
||||
return self._current
|
||||
|
||||
|
||||
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
|
||||
process bot.
|
||||
|
||||
When using this client, you will be able to use it as-if it was a regular
|
||||
:class:`Client` with a single shard when implementation wise internally it
|
||||
is split up into multiple shards. This allows you to not have to deal with
|
||||
IPC or other complicated infrastructure.
|
||||
|
||||
It is recommended to use this client only if you have surpassed at least
|
||||
1000 guilds.
|
||||
|
||||
If no :attr:`shard_count` is provided, then the library will use the
|
||||
Bot Gateway endpoint call to figure out how many shards to use.
|
||||
|
||||
If a ``shard_ids`` parameter is given, then those shard IDs will be used
|
||||
to launch the internal shards. Note that :attr:`shard_count` must be provided
|
||||
if this is used. By default, when omitted, the client will launch shards from
|
||||
0 to ``shard_count - 1``.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
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)
|
||||
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."
|
||||
)
|
||||
elif not isinstance(self.shard_ids, (list, tuple)):
|
||||
raise ClientException("shard_ids parameter must be a list or a tuple.")
|
||||
|
||||
self._connection = AutoShardedConnectionState(
|
||||
dispatch=self.dispatch,
|
||||
chunker=self._chunker,
|
||||
handlers=self._handlers,
|
||||
syncer=self._syncer,
|
||||
http=self.http,
|
||||
loop=self.loop,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# instead of a single websocket, we have multiple
|
||||
# the key is the shard_id
|
||||
self.shards = {}
|
||||
|
||||
def _get_websocket(guild_id):
|
||||
i = (guild_id >> 22) % self.shard_count
|
||||
return self.shards[i].ws
|
||||
|
||||
self._connection._get_websocket = _get_websocket
|
||||
|
||||
async def _chunker(self, guild, *, shard_id=None):
|
||||
try:
|
||||
guild_id = guild.id
|
||||
shard_id = shard_id or guild.shard_id
|
||||
except AttributeError:
|
||||
guild_id = [s.id for s in guild]
|
||||
|
||||
payload = {"op": 8, "d": {"guild_id": guild_id, "query": "", "limit": 0}}
|
||||
|
||||
ws = self.shards[shard_id].ws
|
||||
await ws.send_as_json(payload)
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":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
|
||||
latency of every shard's latency. To get a list of shard latency, check the
|
||||
:attr:`latencies` property. Returns ``nan`` if there are no shards ready.
|
||||
"""
|
||||
if not self.shards:
|
||||
return float("nan")
|
||||
return sum(latency for _, latency in self.latencies) / len(self.shards)
|
||||
|
||||
@property
|
||||
def latencies(self):
|
||||
"""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()]
|
||||
|
||||
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``.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*guilds
|
||||
An argument list of guilds to request offline members for.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
If any guild is unavailable or not large in the collection.
|
||||
"""
|
||||
if any(not g.large or 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):
|
||||
sub_guilds = list(sub_guilds)
|
||||
await self._connection.request_offline_members(sub_guilds, shard_id=shard_id)
|
||||
|
||||
async def launch_shard(self, gateway, shard_id):
|
||||
try:
|
||||
coro = websockets.connect(
|
||||
gateway, loop=self.loop, klass=DiscordWebSocket, compression=None
|
||||
)
|
||||
ws = await asyncio.wait_for(coro, loop=self.loop, timeout=180.0)
|
||||
except Exception:
|
||||
log.info("Failed to connect for shard_id: %s. Retrying...", shard_id)
|
||||
await asyncio.sleep(5.0, loop=self.loop)
|
||||
return await self.launch_shard(gateway, shard_id)
|
||||
|
||||
ws.token = self.http.token
|
||||
ws._connection = self._connection
|
||||
ws._dispatch = self.dispatch
|
||||
ws.gateway = gateway
|
||||
ws.shard_id = shard_id
|
||||
ws.shard_count = self.shard_count
|
||||
ws._max_heartbeat_timeout = self._connection.heartbeat_timeout
|
||||
|
||||
try:
|
||||
# OP HELLO
|
||||
await asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0)
|
||||
await asyncio.wait_for(ws.identify(), loop=self.loop, timeout=180.0)
|
||||
except asyncio.TimeoutError:
|
||||
log.info("Timed out when connecting for shard_id: %s. Retrying...", shard_id)
|
||||
await asyncio.sleep(5.0, loop=self.loop)
|
||||
return await self.launch_shard(gateway, shard_id)
|
||||
|
||||
# keep reading the shard while others connect
|
||||
self.shards[shard_id] = ret = Shard(ws, self)
|
||||
ret.launch_pending_reads()
|
||||
await asyncio.sleep(5.0, loop=self.loop)
|
||||
|
||||
async def launch_shards(self):
|
||||
if self.shard_count is None:
|
||||
self.shard_count, gateway = await self.http.get_bot_gateway()
|
||||
else:
|
||||
gateway = await self.http.get_gateway()
|
||||
|
||||
self._connection.shard_count = self.shard_count
|
||||
|
||||
shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count)
|
||||
|
||||
for shard_id in shard_ids:
|
||||
await self.launch_shard(gateway, shard_id)
|
||||
|
||||
shards_to_wait_for = []
|
||||
for shard in self.shards.values():
|
||||
shard.complete_pending_reads()
|
||||
shards_to_wait_for.append(shard.wait())
|
||||
|
||||
# wait for all pending tasks to finish
|
||||
await utils.sane_wait_for(shards_to_wait_for, timeout=300.0, loop=self.loop)
|
||||
|
||||
async def _connect(self):
|
||||
await self.launch_shards()
|
||||
|
||||
while True:
|
||||
pollers = [shard.get_future() for shard in self.shards.values()]
|
||||
done, _ = await asyncio.wait(
|
||||
pollers, loop=self.loop, return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for f in done:
|
||||
# we wanna re-raise to the main Client.connect handler if applicable
|
||||
f.result()
|
||||
|
||||
async def close(self):
|
||||
"""|coro|
|
||||
|
||||
Closes the connection to discord.
|
||||
"""
|
||||
if self.is_closed():
|
||||
return
|
||||
|
||||
self._closed.set()
|
||||
|
||||
for vc in self.voice_clients:
|
||||
try:
|
||||
await vc.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
to_close = [shard.ws.close() for shard in self.shards.values()]
|
||||
if to_close:
|
||||
await asyncio.wait(to_close, loop=self.loop)
|
||||
|
||||
await self.http.close()
|
||||
|
||||
async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
|
||||
"""|coro|
|
||||
|
||||
Changes the client's presence.
|
||||
|
||||
The activity parameter is a :class:`Activity` object (not a string) that represents
|
||||
the activity being done currently. This could also be the slimmed down versions,
|
||||
:class:`Game` and :class:`Streaming`.
|
||||
|
||||
Example: ::
|
||||
|
||||
game = discord.Game("with the API")
|
||||
await client.change_presence(status=discord.Status.idle, activity=game)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
activity: Optional[Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]]
|
||||
The activity being done. ``None`` if no currently active activity is done.
|
||||
status: Optional[:class:`Status`]
|
||||
Indicates what status to change to. If None, then
|
||||
:attr:`Status.online` is used.
|
||||
afk: 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[int]
|
||||
The shard_id to change the presence to. If not specified
|
||||
or ``None``, then it will change the presence of every
|
||||
shard the bot can see.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
If the ``activity`` parameter is not of proper type.
|
||||
"""
|
||||
|
||||
if status is None:
|
||||
status = "online"
|
||||
status_enum = Status.online
|
||||
elif status is Status.offline:
|
||||
status = "invisible"
|
||||
status_enum = Status.offline
|
||||
else:
|
||||
status_enum = status
|
||||
status = str(status)
|
||||
|
||||
if shard_id is None:
|
||||
for shard in self.shards.values():
|
||||
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
|
||||
|
||||
guilds = self._connection.guilds
|
||||
else:
|
||||
shard = self.shards[shard_id]
|
||||
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
|
||||
guilds = [g for g in self._connection.guilds if g.shard_id == shard_id]
|
||||
|
||||
for guild in guilds:
|
||||
me = guild.me
|
||||
if me is None:
|
||||
continue
|
||||
|
||||
me.activities = (activity,)
|
||||
me.status = status_enum
|
||||
1048
discord/state.py
1048
discord/state.py
File diff suppressed because it is too large
Load Diff
699
discord/user.py
699
discord/user.py
@@ -1,699 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 collections import namedtuple
|
||||
|
||||
import discord.abc
|
||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time, valid_icon_size
|
||||
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse
|
||||
from .errors import ClientException, InvalidArgument
|
||||
from .colour import Colour
|
||||
|
||||
VALID_STATIC_FORMATS = {"jpeg", "jpg", "webp", "png"}
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
|
||||
class Profile(namedtuple("Profile", "flags user mutual_guilds connected_accounts premium_since")):
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
def nitro(self):
|
||||
return self.premium_since is not None
|
||||
|
||||
premium = nitro
|
||||
|
||||
def _has_flag(self, o):
|
||||
v = o.value
|
||||
return (self.flags & v) == v
|
||||
|
||||
@property
|
||||
def staff(self):
|
||||
return self._has_flag(UserFlags.staff)
|
||||
|
||||
@property
|
||||
def partner(self):
|
||||
return self._has_flag(UserFlags.partner)
|
||||
|
||||
@property
|
||||
def bug_hunter(self):
|
||||
return self._has_flag(UserFlags.bug_hunter)
|
||||
|
||||
@property
|
||||
def early_supporter(self):
|
||||
return self._has_flag(UserFlags.early_supporter)
|
||||
|
||||
@property
|
||||
def hypesquad(self):
|
||||
return self._has_flag(UserFlags.hypesquad)
|
||||
|
||||
@property
|
||||
def hypesquad_houses(self):
|
||||
flags = (
|
||||
UserFlags.hypesquad_bravery,
|
||||
UserFlags.hypesquad_brilliance,
|
||||
UserFlags.hypesquad_balance,
|
||||
)
|
||||
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
|
||||
|
||||
|
||||
_BaseUser = discord.abc.User
|
||||
|
||||
|
||||
class BaseUser(_BaseUser):
|
||||
__slots__ = ("name", "id", "discriminator", "avatar", "bot", "_state")
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
self._state = state
|
||||
self.name = data["username"]
|
||||
self.id = int(data["id"])
|
||||
self.discriminator = data["discriminator"]
|
||||
self.avatar = data["avatar"]
|
||||
self.bot = data.get("bot", False)
|
||||
|
||||
def __str__(self):
|
||||
return "{0.name}#{0.discriminator}".format(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _BaseUser) and other.id == self.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id >> 22
|
||||
|
||||
@classmethod
|
||||
def _copy(cls, user):
|
||||
self = cls.__new__(cls) # bypass __init__
|
||||
|
||||
self.name = user.name
|
||||
self.id = user.id
|
||||
self.discriminator = user.discriminator
|
||||
self.avatar = user.avatar
|
||||
self.bot = user.bot
|
||||
self._state = user._state
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
"""Returns a friendly URL version of the avatar the user has.
|
||||
|
||||
If the user does not have a traditional avatar, their default
|
||||
avatar URL is returned instead.
|
||||
|
||||
This is equivalent to calling :meth:`avatar_url_as` with
|
||||
the default parameters (i.e. webp/gif detection and a size of 1024).
|
||||
"""
|
||||
return self.avatar_url_as(format=None, size=1024)
|
||||
|
||||
def is_avatar_animated(self):
|
||||
""":class:`bool`: Returns True if the user has an animated avatar."""
|
||||
return bool(self.avatar and self.avatar.startswith("a_"))
|
||||
|
||||
def avatar_url_as(self, *, format=None, static_format="webp", size=1024):
|
||||
"""Returns a friendly URL version of the avatar the user has.
|
||||
|
||||
If the user does not have a traditional avatar, their default
|
||||
avatar URL is returned instead.
|
||||
|
||||
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
|
||||
'gif' is only valid for animated avatars. The size must be a power of 2
|
||||
between 16 and 1024.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[str]
|
||||
The format to attempt to convert the avatar to.
|
||||
If the format is ``None``, then it is automatically
|
||||
detected into either 'gif' or static_format depending on the
|
||||
avatar being animated or not.
|
||||
static_format: 'str'
|
||||
Format to attempt to convert only non-animated avatars to.
|
||||
Defaults to 'webp'
|
||||
size: int
|
||||
The size of the image to display.
|
||||
|
||||
Returns
|
||||
--------
|
||||
str
|
||||
The resulting CDN URL.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or ``static_format``, or
|
||||
invalid ``size``.
|
||||
"""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
if format == "gif" and not self.is_avatar_animated():
|
||||
raise InvalidArgument("non animated avatars do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if self.avatar is None:
|
||||
return self.default_avatar_url
|
||||
|
||||
if format is None:
|
||||
if self.is_avatar_animated():
|
||||
format = "gif"
|
||||
else:
|
||||
format = static_format
|
||||
|
||||
return "https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}".format(
|
||||
self, format, size
|
||||
)
|
||||
|
||||
@property
|
||||
def default_avatar(self):
|
||||
"""Returns the default avatar for a given user. This is calculated by the user's discriminator"""
|
||||
return DefaultAvatar(int(self.discriminator) % len(DefaultAvatar))
|
||||
|
||||
@property
|
||||
def default_avatar_url(self):
|
||||
"""Returns a URL for a user's default avatar."""
|
||||
return "https://cdn.discordapp.com/embed/avatars/{}.png".format(self.default_avatar.value)
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
"""A property that returns a :class:`Colour` denoting the rendered colour
|
||||
for the user. This always returns :meth:`Colour.default`.
|
||||
|
||||
There is an alias for this under ``color``.
|
||||
"""
|
||||
return Colour.default()
|
||||
|
||||
color = colour
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
"""Returns a string that allows you to mention the given user."""
|
||||
return "<@{0.id}>".format(self)
|
||||
|
||||
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
|
||||
The channel to check your permissions for.
|
||||
"""
|
||||
return channel.permissions_for(self)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the user's creation time in UTC.
|
||||
|
||||
This is when the user's discord account was created."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Returns the user's display name.
|
||||
|
||||
For regular users this is just their username, but
|
||||
if they have a guild specific nickname then that
|
||||
is returned instead.
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def mentioned_in(self, message):
|
||||
"""Checks if the user is mentioned in the specified message.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message : :class:`Message`
|
||||
The message to check if you're mentioned in.
|
||||
"""
|
||||
|
||||
if message.mention_everyone:
|
||||
return True
|
||||
|
||||
for user in message.mentions:
|
||||
if user.id == self.id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ClientUser(BaseUser):
|
||||
"""Represents your Discord user.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two users are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two users are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the user's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the user's name with discriminator.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The user's username.
|
||||
id: :class:`int`
|
||||
The user's unique ID.
|
||||
discriminator: :class:`str`
|
||||
The user's discriminator. This is given when the username has conflicts.
|
||||
avatar: Optional[:class:`str`]
|
||||
The avatar hash the user has. Could be None.
|
||||
bot: :class:`bool`
|
||||
Specifies if the user is a bot account.
|
||||
verified: :class:`bool`
|
||||
Specifies if the user is a verified account.
|
||||
email: Optional[:class:`str`]
|
||||
The email the user used when registering.
|
||||
mfa_enabled: :class:`bool`
|
||||
Specifies if the user has MFA turned on and working.
|
||||
premium: :class:`bool`
|
||||
Specifies if the user is a premium user (e.g. has Discord Nitro).
|
||||
"""
|
||||
|
||||
__slots__ = ("email", "verified", "mfa_enabled", "premium", "_relationships")
|
||||
|
||||
def __init__(self, *, state, data):
|
||||
super().__init__(state=state, data=data)
|
||||
self.verified = data.get("verified", False)
|
||||
self.email = data.get("email")
|
||||
self.mfa_enabled = data.get("mfa_enabled", False)
|
||||
self.premium = data.get("premium", False)
|
||||
self._relationships = {}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}"
|
||||
" bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>".format(self)
|
||||
)
|
||||
|
||||
def get_relationship(self, user_id):
|
||||
"""Retrieves the :class:`Relationship` if applicable.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user_id: int
|
||||
The user ID to check if we have a relationship with them.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[:class:`Relationship`]
|
||||
The relationship if available or ``None``
|
||||
"""
|
||||
return self._relationships.get(user_id)
|
||||
|
||||
@property
|
||||
def relationships(self):
|
||||
"""Returns a :class:`list` of :class:`Relationship` that the user has."""
|
||||
return list(self._relationships.values())
|
||||
|
||||
@property
|
||||
def friends(self):
|
||||
r"""Returns a :class:`list` of :class:`User`\s that the user is friends with."""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend]
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
r"""Returns a :class:`list` of :class:`User`\s that the user has blocked."""
|
||||
return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked]
|
||||
|
||||
async def edit(self, **fields):
|
||||
"""|coro|
|
||||
|
||||
Edits the current profile of the client.
|
||||
|
||||
If a bot account is used then a password field is optional,
|
||||
otherwise it is required.
|
||||
|
||||
Note
|
||||
-----
|
||||
To upload an avatar, a :term:`py:bytes-like object` must be passed in that
|
||||
represents the image being uploaded. If this is done through a file
|
||||
then the file must be opened via ``open('some_filename', 'rb')`` and
|
||||
the :term:`py:bytes-like object` is given through the use of ``fp.read()``.
|
||||
|
||||
The only image formats supported for uploading is JPEG and PNG.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
password : str
|
||||
The current password for the client's account.
|
||||
Only applicable to user accounts.
|
||||
new_password: str
|
||||
The new password you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
email: str
|
||||
The new email you wish to change to.
|
||||
Only applicable to user accounts.
|
||||
house: Optional[:class:`HypeSquadHouse`]
|
||||
The hypesquad house you wish to change to.
|
||||
Could be ``None`` to leave the current house.
|
||||
Only applicable to user accounts.
|
||||
username :str
|
||||
The new username you wish to change to.
|
||||
avatar: bytes
|
||||
A :term:`py:bytes-like object` representing the image to upload.
|
||||
Could be ``None`` to denote no avatar.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPException
|
||||
Editing your profile failed.
|
||||
InvalidArgument
|
||||
Wrong image format passed for ``avatar``.
|
||||
ClientException
|
||||
Password is required for non-bot accounts.
|
||||
House field was not a HypeSquadHouse.
|
||||
"""
|
||||
|
||||
try:
|
||||
avatar_bytes = fields["avatar"]
|
||||
except KeyError:
|
||||
avatar = self.avatar
|
||||
else:
|
||||
if avatar_bytes is not None:
|
||||
avatar = _bytes_to_base64_data(avatar_bytes)
|
||||
else:
|
||||
avatar = None
|
||||
|
||||
not_bot_account = not self.bot
|
||||
password = fields.get("password")
|
||||
if not_bot_account and password is None:
|
||||
raise ClientException("Password is required for non-bot accounts.")
|
||||
|
||||
args = {
|
||||
"password": password,
|
||||
"username": fields.get("username", self.name),
|
||||
"avatar": avatar,
|
||||
}
|
||||
|
||||
if not_bot_account:
|
||||
args["email"] = fields.get("email", self.email)
|
||||
|
||||
if "new_password" in fields:
|
||||
args["new_password"] = fields["new_password"]
|
||||
|
||||
http = self._state.http
|
||||
|
||||
if "house" in fields:
|
||||
house = fields["house"]
|
||||
if house is None:
|
||||
await http.leave_hypesquad_house()
|
||||
elif not isinstance(house, HypeSquadHouse):
|
||||
raise ClientException("`house` parameter was not a HypeSquadHouse")
|
||||
else:
|
||||
value = house.value
|
||||
|
||||
await http.change_hypesquad_house(value)
|
||||
|
||||
data = await http.edit_profile(**args)
|
||||
if not_bot_account:
|
||||
self.email = data["email"]
|
||||
try:
|
||||
http._token(data["token"], bot=False)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# manually update data by calling __init__ explicitly.
|
||||
self.__init__(state=self._state, data=data)
|
||||
|
||||
async def create_group(self, *recipients):
|
||||
r"""|coro|
|
||||
|
||||
Creates a group direct message with the recipients
|
||||
provided. These recipients must be have a relationship
|
||||
of type :attr:`RelationshipType.friend`.
|
||||
|
||||
Bot accounts cannot create a group.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*recipients
|
||||
An argument :class:`list` of :class:`User` to have in
|
||||
your group.
|
||||
|
||||
Return
|
||||
-------
|
||||
:class:`GroupChannel`
|
||||
The new group channel.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Failed to create the group direct message.
|
||||
ClientException
|
||||
Attempted to create a group with only one recipient.
|
||||
This does not include yourself.
|
||||
"""
|
||||
|
||||
from .channel import GroupChannel
|
||||
|
||||
if len(recipients) < 2:
|
||||
raise ClientException("You must have two or more recipients to create a group.")
|
||||
|
||||
users = [str(u.id) for u in recipients]
|
||||
data = await self._state.http.start_group(self.id, users)
|
||||
return GroupChannel(me=self, data=data, state=self._state)
|
||||
|
||||
|
||||
class User(BaseUser, discord.abc.Messageable):
|
||||
"""Represents a Discord user.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if two users are equal.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two users are not equal.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the user's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the user's name with discriminator.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The user's username.
|
||||
id: :class:`int`
|
||||
The user's unique ID.
|
||||
discriminator: :class:`str`
|
||||
The user's discriminator. This is given when the username has conflicts.
|
||||
avatar: Optional[:class:`str`]
|
||||
The avatar hash the user has. Could be None.
|
||||
bot: :class:`bool`
|
||||
Specifies if the user is a bot account.
|
||||
"""
|
||||
|
||||
__slots__ = ("__weakref__",)
|
||||
|
||||
def __repr__(self):
|
||||
return "<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>".format(
|
||||
self
|
||||
)
|
||||
|
||||
async def _get_channel(self):
|
||||
ch = await self.create_dm()
|
||||
return ch
|
||||
|
||||
@property
|
||||
def dm_channel(self):
|
||||
"""Returns the :class:`DMChannel` associated with this user if it exists.
|
||||
|
||||
If this returns ``None``, you can create a DM channel by calling the
|
||||
:meth:`create_dm` coroutine function.
|
||||
"""
|
||||
return self._state._get_private_channel_by_user(self.id)
|
||||
|
||||
async def create_dm(self):
|
||||
"""Creates a :class:`DMChannel` with this user.
|
||||
|
||||
This should be rarely called, as this is done transparently for most
|
||||
people.
|
||||
"""
|
||||
found = self.dm_channel
|
||||
if found is not None:
|
||||
return found
|
||||
|
||||
state = self._state
|
||||
data = await state.http.start_private_message(self.id)
|
||||
return state.add_dm_channel(data)
|
||||
|
||||
@property
|
||||
def relationship(self):
|
||||
"""Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise."""
|
||||
return self._state.user.get_relationship(self.id)
|
||||
|
||||
async def mutual_friends(self):
|
||||
"""|coro|
|
||||
|
||||
Gets all mutual friends of this user. This can only be used by non-bot accounts
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[:class:`User`]
|
||||
The users that are mutual friends.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to get mutual friends of this user.
|
||||
HTTPException
|
||||
Getting mutual friends failed.
|
||||
"""
|
||||
state = self._state
|
||||
mutuals = await state.http.get_mutual_friends(self.id)
|
||||
return [User(state=state, data=friend) for friend in mutuals]
|
||||
|
||||
def is_friend(self):
|
||||
""":class:`bool`: Checks if the user is your friend."""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.friend
|
||||
|
||||
def is_blocked(self):
|
||||
""":class:`bool`: Checks if the user is blocked."""
|
||||
r = self.relationship
|
||||
if r is None:
|
||||
return False
|
||||
return r.type is RelationshipType.blocked
|
||||
|
||||
async def block(self):
|
||||
"""|coro|
|
||||
|
||||
Blocks the user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to block this user.
|
||||
HTTPException
|
||||
Blocking the user failed.
|
||||
"""
|
||||
|
||||
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
|
||||
|
||||
async def unblock(self):
|
||||
"""|coro|
|
||||
|
||||
Unblocks the user.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to unblock this user.
|
||||
HTTPException
|
||||
Unblocking the user failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def remove_friend(self):
|
||||
"""|coro|
|
||||
|
||||
Removes the user as a friend.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to remove this user as a friend.
|
||||
HTTPException
|
||||
Removing the user as a friend failed.
|
||||
"""
|
||||
await self._state.http.remove_relationship(self.id)
|
||||
|
||||
async def send_friend_request(self):
|
||||
"""|coro|
|
||||
|
||||
Sends the user a friend request.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to send a friend request to the user.
|
||||
HTTPException
|
||||
Sending the friend request failed.
|
||||
"""
|
||||
await self._state.http.send_friend_request(
|
||||
username=self.name, discriminator=self.discriminator
|
||||
)
|
||||
|
||||
async def profile(self):
|
||||
"""|coro|
|
||||
|
||||
Gets the user's profile. This can only be used by non-bot accounts.
|
||||
|
||||
Raises
|
||||
-------
|
||||
Forbidden
|
||||
Not allowed to fetch profiles.
|
||||
HTTPException
|
||||
Fetching the profile failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Profile`
|
||||
The profile of the user.
|
||||
"""
|
||||
|
||||
state = self._state
|
||||
data = await state.http.get_user_profile(self.id)
|
||||
|
||||
def transform(d):
|
||||
return state._get_guild(int(d["id"]))
|
||||
|
||||
since = data.get("premium_since")
|
||||
mutual_guilds = list(filter(None, map(transform, data.get("mutual_guilds", []))))
|
||||
return Profile(
|
||||
flags=data["user"].get("flags", 0),
|
||||
premium_since=parse_time(since),
|
||||
mutual_guilds=mutual_guilds,
|
||||
user=self,
|
||||
connected_accounts=data["connected_accounts"],
|
||||
)
|
||||
353
discord/utils.py
353
discord/utils.py
@@ -1,353 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import array
|
||||
import asyncio
|
||||
from base64 import b64encode
|
||||
from bisect import bisect_left
|
||||
import datetime
|
||||
from email.utils import parsedate_to_datetime
|
||||
import functools
|
||||
from inspect import isawaitable as _isawaitable
|
||||
import json
|
||||
from re import split as re_split
|
||||
import warnings
|
||||
|
||||
from .errors import InvalidArgument
|
||||
|
||||
DISCORD_EPOCH = 1420070400000
|
||||
|
||||
|
||||
class cached_property:
|
||||
def __init__(self, function):
|
||||
self.function = function
|
||||
self.__doc__ = getattr(function, "__doc__")
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
value = self.function(instance)
|
||||
setattr(instance, self.function.__name__, value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class CachedSlotProperty:
|
||||
def __init__(self, name, function):
|
||||
self.name = name
|
||||
self.function = function
|
||||
self.__doc__ = getattr(function, "__doc__")
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
try:
|
||||
return getattr(instance, self.name)
|
||||
except AttributeError:
|
||||
value = self.function(instance)
|
||||
setattr(instance, self.name, value)
|
||||
return value
|
||||
|
||||
|
||||
def cached_slot_property(name):
|
||||
def decorator(func):
|
||||
return CachedSlotProperty(name, func)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def parse_time(timestamp):
|
||||
if timestamp:
|
||||
return datetime.datetime(*map(int, re_split(r"[^\d]", timestamp.replace("+00:00", ""))))
|
||||
return None
|
||||
|
||||
|
||||
def deprecated(instead=None):
|
||||
def actual_decorator(func):
|
||||
@functools.wraps(func)
|
||||
def decorated(*args, **kwargs):
|
||||
warnings.simplefilter("always", DeprecationWarning) # turn off filter
|
||||
if instead:
|
||||
fmt = "{0.__name__} is deprecated, use {1} instead."
|
||||
else:
|
||||
fmt = "{0.__name__} is deprecated."
|
||||
|
||||
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
|
||||
warnings.simplefilter("default", DeprecationWarning) # reset filter
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return actual_decorator
|
||||
|
||||
|
||||
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
|
||||
"""A helper function that returns the OAuth2 URL for inviting the bot
|
||||
into guilds.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
client_id : str
|
||||
The client ID for your bot.
|
||||
permissions : :class:`Permissions`
|
||||
The permissions you're requesting. If not given then you won't be requesting any
|
||||
permissions.
|
||||
guild : :class:`Guild`
|
||||
The guild to pre-select in the authorization screen, if available.
|
||||
redirect_uri : str
|
||||
An optional valid redirect URI.
|
||||
"""
|
||||
url = "https://discordapp.com/oauth2/authorize?client_id={}&scope=bot".format(client_id)
|
||||
if permissions is not None:
|
||||
url = url + "&permissions=" + str(permissions.value)
|
||||
if guild is not None:
|
||||
url = url + "&guild_id=" + str(guild.id)
|
||||
if redirect_uri is not None:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
url = url + "&response_type=code&" + urlencode({"redirect_uri": redirect_uri})
|
||||
return url
|
||||
|
||||
|
||||
def snowflake_time(id):
|
||||
"""Returns the creation date in UTC of a discord id."""
|
||||
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000)
|
||||
|
||||
|
||||
def time_snowflake(datetime_obj, high=False):
|
||||
"""Returns a numeric snowflake pretending to be created at the given date.
|
||||
|
||||
When using as the lower end of a range, use time_snowflake(high=False) - 1 to be inclusive, high=True to be exclusive
|
||||
When using as the higher end of a range, use time_snowflake(high=True) + 1 to be inclusive, high=False to be exclusive
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
datetime_obj
|
||||
A timezone-naive datetime object representing UTC time.
|
||||
high
|
||||
Whether or not to set the lower 22 bit to high or low.
|
||||
"""
|
||||
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
|
||||
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
|
||||
|
||||
return (discord_millis << 22) + (2 ** 22 - 1 if high else 0)
|
||||
|
||||
|
||||
def find(predicate, seq):
|
||||
"""A helper to return the first element found in the sequence
|
||||
that meets the predicate. For example: ::
|
||||
|
||||
member = find(lambda m: m.name == 'Mighty', channel.guild.members)
|
||||
|
||||
would find the first :class:`Member` whose name is 'Mighty' and return it.
|
||||
If an entry is not found, then ``None`` is returned.
|
||||
|
||||
This is different from `filter`_ due to the fact it stops the moment it finds
|
||||
a valid entry.
|
||||
|
||||
|
||||
.. _filter: https://docs.python.org/3.6/library/functions.html#filter
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
predicate
|
||||
A function that returns a boolean-like result.
|
||||
seq : iterable
|
||||
The iterable to search through.
|
||||
"""
|
||||
|
||||
for element in seq:
|
||||
if predicate(element):
|
||||
return element
|
||||
return None
|
||||
|
||||
|
||||
def get(iterable, **attrs):
|
||||
r"""A helper that returns the first element in the iterable that meets
|
||||
all the traits passed in ``attrs``. This is an alternative for
|
||||
:func:`discord.utils.find`.
|
||||
|
||||
When multiple attributes are specified, they are checked using
|
||||
logical AND, not logical OR. Meaning they have to meet every
|
||||
attribute passed in and not one of them.
|
||||
|
||||
To have a nested attribute search (i.e. search by ``x.y``) then
|
||||
pass in ``x__y`` as the keyword argument.
|
||||
|
||||
If nothing is found that matches the attributes passed, then
|
||||
``None`` is returned.
|
||||
|
||||
Examples
|
||||
---------
|
||||
|
||||
Basic usage:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
member = discord.utils.get(message.guild.members, name='Foo')
|
||||
|
||||
Multiple attribute matching:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
channel = discord.utils.get(guild.voice_channels, name='Foo', bitrate=64000)
|
||||
|
||||
Nested attribute matching:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
channel = discord.utils.get(client.get_all_channels(), guild__name='Cool', name='general')
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
iterable
|
||||
An iterable to search through.
|
||||
\*\*attrs
|
||||
Keyword arguments that denote attributes to search with.
|
||||
"""
|
||||
|
||||
def predicate(elem):
|
||||
for attr, val in attrs.items():
|
||||
nested = attr.split("__")
|
||||
obj = elem
|
||||
for attribute in nested:
|
||||
obj = getattr(obj, attribute)
|
||||
|
||||
if obj != val:
|
||||
return False
|
||||
return True
|
||||
|
||||
return find(predicate, iterable)
|
||||
|
||||
|
||||
def _unique(iterable):
|
||||
seen = set()
|
||||
adder = seen.add
|
||||
return [x for x in iterable if not (x in seen or adder(x))]
|
||||
|
||||
|
||||
def _get_as_snowflake(data, key):
|
||||
try:
|
||||
value = data[key]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return value and int(value)
|
||||
|
||||
|
||||
def _get_mime_type_for_image(data):
|
||||
if data.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"):
|
||||
return "image/png"
|
||||
elif data.startswith(b"\xFF\xD8") and data.rstrip(b"\0").endswith(b"\xFF\xD9"):
|
||||
return "image/jpeg"
|
||||
elif data.startswith(b"\x47\x49\x46\x38\x37\x61") or data.startswith(
|
||||
b"\x47\x49\x46\x38\x39\x61"
|
||||
):
|
||||
return "image/gif"
|
||||
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
||||
return "image/webp"
|
||||
else:
|
||||
raise InvalidArgument("Unsupported image type given")
|
||||
|
||||
|
||||
def _bytes_to_base64_data(data):
|
||||
fmt = "data:{mime};base64,{data}"
|
||||
mime = _get_mime_type_for_image(data)
|
||||
b64 = b64encode(data).decode("ascii")
|
||||
return fmt.format(mime=mime, data=b64)
|
||||
|
||||
|
||||
def to_json(obj):
|
||||
return json.dumps(obj, separators=(",", ":"), ensure_ascii=True)
|
||||
|
||||
|
||||
def _parse_ratelimit_header(request):
|
||||
now = parsedate_to_datetime(request.headers["Date"])
|
||||
reset = datetime.datetime.fromtimestamp(
|
||||
int(request.headers["X-Ratelimit-Reset"]), datetime.timezone.utc
|
||||
)
|
||||
return (reset - now).total_seconds()
|
||||
|
||||
|
||||
async def maybe_coroutine(f, *args, **kwargs):
|
||||
value = f(*args, **kwargs)
|
||||
if _isawaitable(value):
|
||||
return await value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
async def async_all(gen, *, check=_isawaitable):
|
||||
for elem in gen:
|
||||
if check(elem):
|
||||
elem = await elem
|
||||
if not elem:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def sane_wait_for(futures, *, timeout, loop):
|
||||
_, pending = await asyncio.wait(futures, timeout=timeout, loop=loop)
|
||||
|
||||
if len(pending) != 0:
|
||||
raise asyncio.TimeoutError()
|
||||
|
||||
|
||||
def valid_icon_size(size):
|
||||
"""Icons must be power of 2 within [16, 2048]."""
|
||||
return not size & (size - 1) and size in range(16, 2049)
|
||||
|
||||
|
||||
class SnowflakeList(array.array):
|
||||
"""Internal data storage class to efficiently store a list of snowflakes.
|
||||
|
||||
This should have the following characteristics:
|
||||
|
||||
- Low memory usage
|
||||
- O(n) iteration (obviously)
|
||||
- O(n log n) initial creation if data is unsorted
|
||||
- O(log n) search and indexing
|
||||
- O(n) insertion
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, data, *, is_sorted=False):
|
||||
return array.array.__new__(cls, "Q", data if is_sorted else sorted(data))
|
||||
|
||||
def add(self, element):
|
||||
i = bisect_left(self, element)
|
||||
self.insert(i, element)
|
||||
|
||||
def get(self, element):
|
||||
i = bisect_left(self, element)
|
||||
return self[i] if i != len(self) and self[i] == element else None
|
||||
|
||||
def has(self, element):
|
||||
i = bisect_left(self, element)
|
||||
return i != len(self) and self[i] == element
|
||||
@@ -1,438 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
"""Some documentation to refer to:
|
||||
|
||||
- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID.
|
||||
- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE.
|
||||
- We pull the session_id from VOICE_STATE_UPDATE.
|
||||
- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE.
|
||||
- Then we initiate the voice web socket (vWS) pointing to the endpoint.
|
||||
- We send opcode 0 with the user_id, server_id, session_id and token using the vWS.
|
||||
- The vWS sends back opcode 2 with an ssrc, port, modes(array) and hearbeat_interval.
|
||||
- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE.
|
||||
- Then we send our IP and port via vWS with opcode 1.
|
||||
- When that's all done, we receive opcode 4 from the vWS.
|
||||
- Finally we can transmit data to endpoint:port.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import logging
|
||||
import struct
|
||||
import threading
|
||||
|
||||
from . import opus
|
||||
from .backoff import ExponentialBackoff
|
||||
from .gateway import *
|
||||
from .errors import ClientException, ConnectionClosed
|
||||
from .player import AudioPlayer, AudioSource
|
||||
|
||||
try:
|
||||
import nacl.secret
|
||||
|
||||
has_nacl = True
|
||||
except ImportError:
|
||||
has_nacl = False
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VoiceClient:
|
||||
"""Represents a Discord voice connection.
|
||||
|
||||
You do not create these, you typically get them from
|
||||
e.g. :meth:`VoiceChannel.connect`.
|
||||
|
||||
Warning
|
||||
--------
|
||||
In order to play audio, you must have loaded the opus library
|
||||
through :func:`opus.load_opus`.
|
||||
|
||||
If you don't do this then the library will not be able to
|
||||
transmit audio.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
session_id: :class:`str`
|
||||
The voice connection session ID.
|
||||
token: :class:`str`
|
||||
The voice connection token.
|
||||
endpoint: :class:`str`
|
||||
The endpoint we are connecting to.
|
||||
channel: :class:`abc.Connectable`
|
||||
The voice channel connected to.
|
||||
loop
|
||||
The event loop that the voice client is running on.
|
||||
"""
|
||||
|
||||
def __init__(self, state, timeout, channel):
|
||||
if not has_nacl:
|
||||
raise RuntimeError("PyNaCl library needed in order to use voice")
|
||||
|
||||
self.channel = channel
|
||||
self.main_ws = None
|
||||
self.timeout = timeout
|
||||
self.ws = None
|
||||
self.socket = None
|
||||
self.loop = state.loop
|
||||
self._state = state
|
||||
# this will be used in the AudioPlayer thread
|
||||
self._connected = threading.Event()
|
||||
self._handshake_complete = asyncio.Event(loop=self.loop)
|
||||
|
||||
self._connections = 0
|
||||
self.sequence = 0
|
||||
self.timestamp = 0
|
||||
self._runner = None
|
||||
self._player = None
|
||||
self.encoder = opus.Encoder()
|
||||
|
||||
warn_nacl = not has_nacl
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
"""Optional[:class:`Guild`]: The guild we're connected to, if applicable."""
|
||||
return getattr(self.channel, "guild", None)
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
""":class:`ClientUser`: The user connected to voice (i.e. ourselves)."""
|
||||
return self._state.user
|
||||
|
||||
def checked_add(self, attr, value, limit):
|
||||
val = getattr(self, attr)
|
||||
if val + value > limit:
|
||||
setattr(self, attr, 0)
|
||||
else:
|
||||
setattr(self, attr, val + value)
|
||||
|
||||
# connection related
|
||||
|
||||
async def start_handshake(self):
|
||||
log.info("Starting voice handshake...")
|
||||
|
||||
guild_id, channel_id = self.channel._get_voice_state_pair()
|
||||
state = self._state
|
||||
self.main_ws = ws = state._get_websocket(guild_id)
|
||||
self._connections += 1
|
||||
|
||||
# request joining
|
||||
await ws.voice_state(guild_id, channel_id)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await self.terminate_handshake(remove=True)
|
||||
raise
|
||||
|
||||
log.info(
|
||||
"Voice handshake complete. Endpoint found %s (IP: %s)", self.endpoint, self.endpoint_ip
|
||||
)
|
||||
|
||||
async def terminate_handshake(self, *, remove=False):
|
||||
guild_id, channel_id = self.channel._get_voice_state_pair()
|
||||
self._handshake_complete.clear()
|
||||
await self.main_ws.voice_state(guild_id, None, self_mute=True)
|
||||
|
||||
log.info(
|
||||
"The voice handshake is being terminated for Channel ID %s (Guild ID %s)",
|
||||
channel_id,
|
||||
guild_id,
|
||||
)
|
||||
if remove:
|
||||
log.info(
|
||||
"The voice client has been removed for Channel ID %s (Guild ID %s)",
|
||||
channel_id,
|
||||
guild_id,
|
||||
)
|
||||
key_id, _ = self.channel._get_voice_client_key()
|
||||
self._state._remove_voice_client(key_id)
|
||||
|
||||
async def _create_socket(self, server_id, data):
|
||||
self._connected.clear()
|
||||
self.session_id = self.main_ws.session_id
|
||||
self.server_id = server_id
|
||||
self.token = data.get("token")
|
||||
endpoint = data.get("endpoint")
|
||||
|
||||
if endpoint is None or self.token is None:
|
||||
log.warning(
|
||||
"Awaiting endpoint... This requires waiting. "
|
||||
"If timeout occurred considering raising the timeout and reconnecting."
|
||||
)
|
||||
return
|
||||
|
||||
self.endpoint = endpoint.replace(":80", "")
|
||||
self.endpoint_ip = socket.gethostbyname(self.endpoint)
|
||||
|
||||
if self.socket:
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
if self._handshake_complete.is_set():
|
||||
# terminate the websocket and handle the reconnect loop if necessary.
|
||||
self._handshake_complete.clear()
|
||||
await self.ws.close(4000)
|
||||
return
|
||||
|
||||
self._handshake_complete.set()
|
||||
|
||||
async def connect(self, *, reconnect=True, _tries=0, do_handshake=True):
|
||||
log.info("Connecting to voice...")
|
||||
try:
|
||||
del self.secret_key
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if do_handshake:
|
||||
await self.start_handshake()
|
||||
|
||||
try:
|
||||
self.ws = await DiscordVoiceWebSocket.from_client(self)
|
||||
self._connected.clear()
|
||||
while not hasattr(self, "secret_key"):
|
||||
await self.ws.poll_event()
|
||||
self._connected.set()
|
||||
except (ConnectionClosed, asyncio.TimeoutError):
|
||||
if reconnect and _tries < 5:
|
||||
log.exception("Failed to connect to voice... Retrying...")
|
||||
await asyncio.sleep(1 + _tries * 2.0, loop=self.loop)
|
||||
await self.terminate_handshake()
|
||||
await self.connect(reconnect=reconnect, _tries=_tries + 1)
|
||||
else:
|
||||
raise
|
||||
|
||||
if self._runner is None:
|
||||
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
|
||||
|
||||
async def poll_voice_ws(self, reconnect):
|
||||
backoff = ExponentialBackoff()
|
||||
while True:
|
||||
try:
|
||||
await self.ws.poll_event()
|
||||
except (ConnectionClosed, asyncio.TimeoutError) as exc:
|
||||
if isinstance(exc, ConnectionClosed):
|
||||
if exc.code == 1000:
|
||||
await self.disconnect()
|
||||
break
|
||||
|
||||
if not reconnect:
|
||||
await self.disconnect()
|
||||
raise
|
||||
|
||||
retry = backoff.delay()
|
||||
log.exception("Disconnected from voice... Reconnecting in %.2fs.", retry)
|
||||
self._connected.clear()
|
||||
await asyncio.sleep(retry, loop=self.loop)
|
||||
await self.terminate_handshake()
|
||||
try:
|
||||
await self.connect(reconnect=True)
|
||||
except asyncio.TimeoutError:
|
||||
# at this point we've retried 5 times... let's continue the loop.
|
||||
log.warning("Could not connect to voice... Retrying...")
|
||||
continue
|
||||
|
||||
async def disconnect(self, *, force=False):
|
||||
"""|coro|
|
||||
|
||||
Disconnects this voice client from voice.
|
||||
"""
|
||||
if not force and not self._connected.is_set():
|
||||
return
|
||||
|
||||
self.stop()
|
||||
self._connected.clear()
|
||||
|
||||
try:
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
|
||||
await self.terminate_handshake(remove=True)
|
||||
finally:
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
|
||||
async def move_to(self, channel):
|
||||
"""|coro|
|
||||
|
||||
Moves you to a different voice channel.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
channel: :class:`abc.Snowflake`
|
||||
The channel to move to. Must be a voice channel.
|
||||
"""
|
||||
guild_id, _ = self.channel._get_voice_state_pair()
|
||||
await self.main_ws.voice_state(guild_id, channel.id)
|
||||
|
||||
def is_connected(self):
|
||||
""":class:`bool`: Indicates if the voice client is connected to voice."""
|
||||
return self._connected.is_set()
|
||||
|
||||
# audio related
|
||||
|
||||
def _get_voice_packet(self, data):
|
||||
header = bytearray(12)
|
||||
nonce = bytearray(24)
|
||||
box = nacl.secret.SecretBox(bytes(self.secret_key))
|
||||
|
||||
# Formulate header
|
||||
header[0] = 0x80
|
||||
header[1] = 0x78
|
||||
struct.pack_into(">H", header, 2, self.sequence)
|
||||
struct.pack_into(">I", header, 4, self.timestamp)
|
||||
struct.pack_into(">I", header, 8, self.ssrc)
|
||||
|
||||
# Copy header to nonce's first 12 bytes
|
||||
nonce[:12] = header
|
||||
|
||||
# Encrypt and return the data
|
||||
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
|
||||
|
||||
def play(self, source, *, after=None):
|
||||
"""Plays an :class:`AudioSource`.
|
||||
|
||||
The finalizer, ``after`` is called after the source has been exhausted
|
||||
or an error occurred.
|
||||
|
||||
If an error happens while the audio player is running, the exception is
|
||||
caught and the audio player is then stopped.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
source: :class:`AudioSource`
|
||||
The audio source we're reading from.
|
||||
after
|
||||
The finalizer that is called after the stream is exhausted.
|
||||
All exceptions it throws are silently discarded. This function
|
||||
must have a single parameter, ``error``, that denotes an
|
||||
optional exception that was raised during playing.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ClientException
|
||||
Already playing audio or not connected.
|
||||
TypeError
|
||||
source is not a :class:`AudioSource` or after is not a callable.
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ClientException("Not connected to voice.")
|
||||
|
||||
if self.is_playing():
|
||||
raise ClientException("Already playing audio.")
|
||||
|
||||
if not isinstance(source, AudioSource):
|
||||
raise TypeError("source must an AudioSource not {0.__class__.__name__}".format(source))
|
||||
|
||||
self._player = AudioPlayer(source, self, after=after)
|
||||
self._player.start()
|
||||
|
||||
def is_playing(self):
|
||||
"""Indicates if we're currently playing audio."""
|
||||
return self._player is not None and self._player.is_playing()
|
||||
|
||||
def is_paused(self):
|
||||
"""Indicates if we're playing audio, but if we're paused."""
|
||||
return self._player is not None and self._player.is_paused()
|
||||
|
||||
def stop(self):
|
||||
"""Stops playing audio."""
|
||||
if self._player:
|
||||
self._player.stop()
|
||||
self._player = None
|
||||
|
||||
def pause(self):
|
||||
"""Pauses the audio playing."""
|
||||
if self._player:
|
||||
self._player.pause()
|
||||
|
||||
def resume(self):
|
||||
"""Resumes the audio playing."""
|
||||
if self._player:
|
||||
self._player.resume()
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Optional[:class:`AudioSource`]: The audio source being played, if playing.
|
||||
|
||||
This property can also be used to change the audio source currently being played.
|
||||
"""
|
||||
return self._player.source if self._player else None
|
||||
|
||||
@source.setter
|
||||
def source(self, value):
|
||||
if not isinstance(value, AudioSource):
|
||||
raise TypeError("expected AudioSource not {0.__class__.__name__}.".format(value))
|
||||
|
||||
if self._player is None:
|
||||
raise ValueError("Not playing anything.")
|
||||
|
||||
self._player._set_source(value)
|
||||
|
||||
def send_audio_packet(self, data, *, encode=True):
|
||||
"""Sends an audio packet composed of the data.
|
||||
|
||||
You must be connected to play audio.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: bytes
|
||||
The :term:`py:bytes-like object` denoting PCM or Opus voice data.
|
||||
encode: bool
|
||||
Indicates if ``data`` should be encoded into Opus.
|
||||
|
||||
Raises
|
||||
-------
|
||||
ClientException
|
||||
You are not connected.
|
||||
OpusError
|
||||
Encoding the data failed.
|
||||
"""
|
||||
|
||||
self.checked_add("sequence", 1, 65535)
|
||||
if encode:
|
||||
encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME)
|
||||
else:
|
||||
encoded_data = data
|
||||
packet = self._get_voice_packet(encoded_data)
|
||||
try:
|
||||
self.socket.sendto(packet, (self.endpoint_ip, self.voice_port))
|
||||
except BlockingIOError:
|
||||
log.warning(
|
||||
"A packet has been dropped (seq: %s, timestamp: %s)", self.sequence, self.timestamp
|
||||
)
|
||||
|
||||
self.checked_add("timestamp", self.encoder.SAMPLES_PER_FRAME, 4294967295)
|
||||
@@ -1,703 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
|
||||
from . import utils
|
||||
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound
|
||||
from .user import BaseUser, User
|
||||
|
||||
__all__ = ["WebhookAdapter", "AsyncWebhookAdapter", "RequestsWebhookAdapter", "Webhook"]
|
||||
|
||||
|
||||
class WebhookAdapter:
|
||||
"""Base class for all webhook adapters.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
webhook: :class:`Webhook`
|
||||
The webhook that owns this adapter.
|
||||
"""
|
||||
|
||||
BASE = "https://discordapp.com/api/v7"
|
||||
|
||||
def _prepare(self, webhook):
|
||||
self._webhook_id = webhook.id
|
||||
self._webhook_token = webhook.token
|
||||
self._request_url = "{0.BASE}/webhooks/{1}/{2}".format(self, webhook.id, webhook.token)
|
||||
self.webhook = webhook
|
||||
|
||||
def request(self, verb, url, payload=None, multipart=None):
|
||||
"""Actually does the request.
|
||||
|
||||
Subclasses must implement this.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
verb: str
|
||||
The HTTP verb to use for the request.
|
||||
url: str
|
||||
The URL to send the request to. This will have
|
||||
the query parameters already added to it, if any.
|
||||
multipart: Optional[dict]
|
||||
A dict containing multipart form data to send with
|
||||
the request. If a filename is being uploaded, then it will
|
||||
be under a ``file`` key which will have a 3-element :class:`tuple`
|
||||
denoting ``(filename, file, content_type)``.
|
||||
payload: Optional[dict]
|
||||
The JSON to send with the request, if any.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_webhook(self):
|
||||
return self.request("DELETE", self._request_url)
|
||||
|
||||
def edit_webhook(self, **payload):
|
||||
return self.request("PATCH", self._request_url, payload=payload)
|
||||
|
||||
def handle_execution_response(self, data, *, wait):
|
||||
"""Transforms the webhook execution response into something
|
||||
more meaningful.
|
||||
|
||||
This is mainly used to convert the data into a :class:`Message`
|
||||
if necessary.
|
||||
|
||||
Subclasses must implement this.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
data
|
||||
The data that was returned from the request.
|
||||
wait: bool
|
||||
Whether the webhook execution was asked to wait or not.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def store_user(self, data):
|
||||
# mocks a ConnectionState for appropriate use for Message
|
||||
return BaseUser(state=self, data=data)
|
||||
|
||||
def execute_webhook(self, *, payload, wait=False, file=None, files=None):
|
||||
if file is not None:
|
||||
multipart = {"file": file, "payload_json": utils.to_json(payload)}
|
||||
data = None
|
||||
elif files is not None:
|
||||
multipart = {"payload_json": utils.to_json(payload)}
|
||||
for i, file in enumerate(files, start=1):
|
||||
multipart["file%i" % i] = file
|
||||
data = None
|
||||
else:
|
||||
data = payload
|
||||
multipart = None
|
||||
|
||||
url = "%s?wait=%d" % (self._request_url, wait)
|
||||
maybe_coro = self.request("POST", url, multipart=multipart, payload=data)
|
||||
return self.handle_execution_response(maybe_coro, wait=wait)
|
||||
|
||||
|
||||
class AsyncWebhookAdapter(WebhookAdapter):
|
||||
"""A webhook adapter suited for use with aiohttp.
|
||||
|
||||
.. note::
|
||||
|
||||
You are responsible for cleaning up the client session.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
session: aiohttp.ClientSession
|
||||
The session to use to send requests.
|
||||
"""
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.loop = session.loop
|
||||
|
||||
async def request(self, verb, url, payload=None, multipart=None):
|
||||
headers = {}
|
||||
data = None
|
||||
if payload:
|
||||
headers["Content-Type"] = "application/json"
|
||||
data = utils.to_json(payload)
|
||||
|
||||
if multipart:
|
||||
data = aiohttp.FormData()
|
||||
for key, value in multipart.items():
|
||||
if key.startswith("file"):
|
||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
||||
else:
|
||||
data.add_field(key, value)
|
||||
|
||||
for tries in range(5):
|
||||
async with self.session.request(verb, url, headers=headers, data=data) as r:
|
||||
data = await r.text(encoding="utf-8")
|
||||
if r.headers["Content-Type"] == "application/json":
|
||||
data = json.loads(data)
|
||||
|
||||
# check if we have rate limit header information
|
||||
remaining = r.headers.get("X-Ratelimit-Remaining")
|
||||
if remaining == "0" and r.status != 429:
|
||||
delta = utils._parse_ratelimit_header(r)
|
||||
await asyncio.sleep(delta, loop=self.loop)
|
||||
|
||||
if 300 > r.status >= 200:
|
||||
return data
|
||||
|
||||
# we are being rate limited
|
||||
if r.status == 429:
|
||||
retry_after = data["retry_after"] / 1000.0
|
||||
await asyncio.sleep(retry_after, loop=self.loop)
|
||||
continue
|
||||
|
||||
if r.status in (500, 502):
|
||||
await asyncio.sleep(1 + tries * 2, loop=self.loop)
|
||||
continue
|
||||
|
||||
if r.status == 403:
|
||||
raise Forbidden(r, data)
|
||||
elif r.status == 404:
|
||||
raise NotFound(r, data)
|
||||
else:
|
||||
raise HTTPException(r, data)
|
||||
|
||||
async def handle_execution_response(self, response, *, wait):
|
||||
data = await response
|
||||
if not wait:
|
||||
return data
|
||||
|
||||
# transform into Message object
|
||||
from .message import Message
|
||||
|
||||
return Message(data=data, state=self, channel=self.webhook.channel)
|
||||
|
||||
|
||||
class RequestsWebhookAdapter(WebhookAdapter):
|
||||
"""A webhook adapter suited for use with ``requests``.
|
||||
|
||||
Only versions of requests higher than 2.13.0 are supported.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
session: Optional[`requests.Session <http://docs.python-requests.org/en/latest/api/#requests.Session>`_]
|
||||
The requests session to use for sending requests. If not given then
|
||||
each request will create a new session. Note if a session is given,
|
||||
the webhook adapter **will not** clean it up for you. You must close
|
||||
the session yourself.
|
||||
sleep: bool
|
||||
Whether to sleep the thread when encountering a 429 or pre-emptive
|
||||
rate limit or a 5xx status code. Defaults to ``True``. If set to
|
||||
``False`` then this will raise an :exc:`HTTPException` instead.
|
||||
"""
|
||||
|
||||
def __init__(self, session=None, *, sleep=True):
|
||||
import requests
|
||||
|
||||
self.session = session or requests
|
||||
self.sleep = sleep
|
||||
|
||||
def request(self, verb, url, payload=None, multipart=None):
|
||||
headers = {}
|
||||
data = None
|
||||
if payload:
|
||||
headers["Content-Type"] = "application/json"
|
||||
data = utils.to_json(payload)
|
||||
|
||||
if multipart is not None:
|
||||
data = {"payload_json": multipart.pop("payload_json")}
|
||||
|
||||
for tries in range(5):
|
||||
r = self.session.request(verb, url, headers=headers, data=data, files=multipart)
|
||||
r.encoding = "utf-8"
|
||||
data = r.text
|
||||
|
||||
# compatibility with aiohttp
|
||||
r.status = r.status_code
|
||||
|
||||
if r.headers["Content-Type"] == "application/json":
|
||||
data = json.loads(data)
|
||||
|
||||
# check if we have rate limit header information
|
||||
remaining = r.headers.get("X-Ratelimit-Remaining")
|
||||
if remaining == "0" and r.status != 429 and self.sleep:
|
||||
delta = utils._parse_ratelimit_header(r)
|
||||
time.sleep(delta)
|
||||
|
||||
if 300 > r.status >= 200:
|
||||
return data
|
||||
|
||||
# we are being rate limited
|
||||
if r.status == 429:
|
||||
if self.sleep:
|
||||
retry_after = data["retry_after"] / 1000.0
|
||||
time.sleep(retry_after)
|
||||
continue
|
||||
else:
|
||||
raise HTTPException(r, data)
|
||||
|
||||
if self.sleep and r.status in (500, 502):
|
||||
time.sleep(1 + tries * 2)
|
||||
continue
|
||||
|
||||
if r.status == 403:
|
||||
raise Forbidden(r, data)
|
||||
elif r.status == 404:
|
||||
raise NotFound(r, data)
|
||||
else:
|
||||
raise HTTPException(r, data)
|
||||
|
||||
def handle_execution_response(self, response, *, wait):
|
||||
if not wait:
|
||||
return response
|
||||
|
||||
# transform into Message object
|
||||
from .message import Message
|
||||
|
||||
return Message(data=response, state=self, channel=self.webhook.channel)
|
||||
|
||||
|
||||
class Webhook:
|
||||
"""Represents a Discord webhook.
|
||||
|
||||
Webhooks are a form to send messages to channels in Discord without a
|
||||
bot user or authentication.
|
||||
|
||||
There are two main ways to use Webhooks. The first is through the ones
|
||||
received by the library such as :meth:`.Guild.webhooks` and
|
||||
:meth:`.TextChannel.webhooks`. The ones received by the library will
|
||||
automatically have an adapter bound using the library's HTTP session.
|
||||
Those webhooks will have :meth:`~.Webhook.send`, :meth:`~.Webhook.delete` and
|
||||
:meth:`~.Webhook.edit` as coroutines.
|
||||
|
||||
The second form involves creating a webhook object manually without having
|
||||
it bound to a websocket connection using the :meth:`~.Webhook.from_url` or
|
||||
:meth:`~.Webhook.partial` classmethods. This form allows finer grained control
|
||||
over how requests are done, allowing you to mix async and sync code using either
|
||||
``aiohttp`` or ``requests``.
|
||||
|
||||
For example, creating a webhook from a URL and using ``aiohttp``:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from discord import Webhook, AsyncWebhookAdapter
|
||||
import aiohttp
|
||||
|
||||
async def foo():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
webhook = Webhook.from_url('url-here', adapter=AsyncWebhookAdapter(session))
|
||||
await webhook.send('Hello World', username='Foo')
|
||||
|
||||
Or creating a webhook from an ID and token and using ``requests``:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
import requests
|
||||
from discord import Webhook, RequestsWebhookAdapter
|
||||
|
||||
webhook = Webhook.partial(123456, 'abcdefg', adapter=RequestsWebhookAdapter())
|
||||
webhook.send('Hello World', username='Foo')
|
||||
|
||||
Attributes
|
||||
------------
|
||||
id: :class:`int`
|
||||
The webhook's ID
|
||||
token: :class:`str`
|
||||
The authentication token of the webhook.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild ID this webhook is for.
|
||||
channel_id: Optional[:class:`int`]
|
||||
The channel ID this webhook is for.
|
||||
user: Optional[:class:`abc.User`]
|
||||
The user this webhook was created by. If the webhook was
|
||||
received without authentication then this will be ``None``.
|
||||
name: Optional[:class:`str`]
|
||||
The default name of the webhook.
|
||||
avatar: Optional[:class:`str`]
|
||||
The default avatar of the webhook.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"id",
|
||||
"guild_id",
|
||||
"channel_id",
|
||||
"user",
|
||||
"name",
|
||||
"avatar",
|
||||
"token",
|
||||
"_state",
|
||||
"_adapter",
|
||||
)
|
||||
|
||||
def __init__(self, data, *, adapter, state=None):
|
||||
self.id = int(data["id"])
|
||||
self.channel_id = utils._get_as_snowflake(data, "channel_id")
|
||||
self.guild_id = utils._get_as_snowflake(data, "guild_id")
|
||||
self.name = data.get("name")
|
||||
self.avatar = data.get("avatar")
|
||||
self.token = data["token"]
|
||||
self._state = state
|
||||
self._adapter = adapter
|
||||
self._adapter._prepare(self)
|
||||
|
||||
user = data.get("user")
|
||||
if user is None:
|
||||
self.user = None
|
||||
elif state is None:
|
||||
self.user = BaseUser(state=None, data=user)
|
||||
else:
|
||||
self.user = User(state=state, data=user)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Webhook id=%r>" % self.id
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Returns the webhook's url."""
|
||||
return "https://discordapp.com/api/webhooks/{}/{}".format(self.id, self.token)
|
||||
|
||||
@classmethod
|
||||
def partial(cls, id, token, *, adapter):
|
||||
"""Creates a partial :class:`Webhook`.
|
||||
|
||||
A partial webhook is just a webhook object with an ID and a token.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
id: int
|
||||
The ID of the webhook.
|
||||
token: str
|
||||
The authentication token of the webhook.
|
||||
adapter: :class:`WebhookAdapter`
|
||||
The webhook adapter to use when sending requests. This is
|
||||
typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or
|
||||
:class:`RequestsWebhookAdapter` for ``requests``.
|
||||
"""
|
||||
|
||||
if not isinstance(adapter, WebhookAdapter):
|
||||
raise TypeError("adapter must be a subclass of WebhookAdapter")
|
||||
|
||||
data = {"id": id, "token": token}
|
||||
|
||||
return cls(data, adapter=adapter)
|
||||
|
||||
@classmethod
|
||||
def from_url(cls, url, *, adapter):
|
||||
"""Creates a partial :class:`Webhook` from a webhook URL.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
url: str
|
||||
The URL of the webhook.
|
||||
adapter: :class:`WebhookAdapter`
|
||||
The webhook adapter to use when sending requests. This is
|
||||
typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or
|
||||
:class:`RequestsWebhookAdapter` for ``requests``.
|
||||
|
||||
Raises
|
||||
-------
|
||||
InvalidArgument
|
||||
The URL is invalid.
|
||||
"""
|
||||
|
||||
m = re.search(
|
||||
r"discordapp.com/api/webhooks/(?P<id>[0-9]{17,21})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})",
|
||||
url,
|
||||
)
|
||||
if m is None:
|
||||
raise InvalidArgument("Invalid webhook URL given.")
|
||||
return cls(m.groupdict(), adapter=adapter)
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, data, state):
|
||||
return cls(data, adapter=AsyncWebhookAdapter(session=state.http._session), state=state)
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
"""Optional[:class:`Guild`]: The guild this webhook belongs to.
|
||||
|
||||
If this is a partial webhook, then this will always return ``None``.
|
||||
"""
|
||||
return self._state and self._state._get_guild(self.guild_id)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""Optional[:class:`TextChannel`]: The text channel this webhook belongs to.
|
||||
|
||||
If this is a partial webhook, then this will always return ``None``.
|
||||
"""
|
||||
guild = self.guild
|
||||
return guild and guild.get_channel(self.channel_id)
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
"""Returns the webhook's creation time in UTC."""
|
||||
return utils.snowflake_time(self.id)
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
"""Returns a friendly URL version of the avatar the webhook has.
|
||||
|
||||
If the webhook does not have a traditional avatar, their default
|
||||
avatar URL is returned instead.
|
||||
|
||||
This is equivalent to calling :meth:`avatar_url_as` with the
|
||||
default parameters.
|
||||
"""
|
||||
return self.avatar_url_as()
|
||||
|
||||
def avatar_url_as(self, *, format=None, size=1024):
|
||||
"""Returns a friendly URL version of the avatar the webhook has.
|
||||
|
||||
If the webhook does not have a traditional avatar, their default
|
||||
avatar URL is returned instead.
|
||||
|
||||
The format must be one of 'jpeg', 'jpg', or 'png'.
|
||||
The size must be a power of 2 between 16 and 1024.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
format: Optional[str]
|
||||
The format to attempt to convert the avatar to.
|
||||
If the format is ``None``, then it is equivalent to png.
|
||||
size: int
|
||||
The size of the image to display.
|
||||
|
||||
Returns
|
||||
--------
|
||||
str
|
||||
The resulting CDN URL.
|
||||
|
||||
Raises
|
||||
------
|
||||
InvalidArgument
|
||||
Bad image format passed to ``format`` or invalid ``size``.
|
||||
"""
|
||||
if self.avatar is None:
|
||||
# Default is always blurple apparently
|
||||
return "https://cdn.discordapp.com/embed/avatars/0.png"
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
|
||||
|
||||
format = format or "png"
|
||||
|
||||
if format not in ("png", "jpg", "jpeg"):
|
||||
raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.")
|
||||
|
||||
return "https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}".format(
|
||||
self, format, size
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
"""|maybecoro|
|
||||
|
||||
Deletes this Webhook.
|
||||
|
||||
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
|
||||
not a coroutine.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Deleting the webhook failed.
|
||||
NotFound
|
||||
This webhook does not exist.
|
||||
Forbidden
|
||||
You do not have permissions to delete this webhook.
|
||||
"""
|
||||
return self._adapter.delete_webhook()
|
||||
|
||||
def edit(self, **kwargs):
|
||||
"""|maybecoro|
|
||||
|
||||
Edits this Webhook.
|
||||
|
||||
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
|
||||
not a coroutine.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
name: Optional[str]
|
||||
The webhook's new default name.
|
||||
avatar: Optional[bytes]
|
||||
A :term:`py:bytes-like object` representing the webhook's new default avatar.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Editing the webhook failed.
|
||||
NotFound
|
||||
This webhook does not exist.
|
||||
Forbidden
|
||||
You do not have permissions to edit this webhook.
|
||||
"""
|
||||
payload = {}
|
||||
|
||||
try:
|
||||
name = kwargs["name"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if name is not None:
|
||||
payload["name"] = str(name)
|
||||
else:
|
||||
payload["name"] = None
|
||||
|
||||
try:
|
||||
avatar = kwargs["avatar"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if avatar is not None:
|
||||
payload["avatar"] = utils._bytes_to_base64_data(avatar)
|
||||
else:
|
||||
payload["avatar"] = None
|
||||
|
||||
return self._adapter.edit_webhook(**payload)
|
||||
|
||||
def send(
|
||||
self,
|
||||
content=None,
|
||||
*,
|
||||
wait=False,
|
||||
username=None,
|
||||
avatar_url=None,
|
||||
tts=False,
|
||||
file=None,
|
||||
files=None,
|
||||
embed=None,
|
||||
embeds=None
|
||||
):
|
||||
"""|maybecoro|
|
||||
|
||||
Sends a message using the webhook.
|
||||
|
||||
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
|
||||
not a coroutine.
|
||||
|
||||
The content must be a type that can convert to a string through ``str(content)``.
|
||||
|
||||
To upload a single file, the ``file`` parameter should be used with a
|
||||
single :class:`File` object.
|
||||
|
||||
If the ``embed`` parameter is provided, it must be of type :class:`Embed` and
|
||||
it must be a rich embed type. You cannot mix the ``embed`` parameter with the
|
||||
``embeds`` parameter, which must be a :class:`list` of :class:`Embed` objects to send.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
content
|
||||
The content of the message to send.
|
||||
wait: bool
|
||||
Whether the server should wait before sending a response. This essentially
|
||||
means that the return type of this function changes from ``None`` to
|
||||
a :class:`Message` if set to ``True``.
|
||||
username: str
|
||||
The username to send with this message. If no username is provided
|
||||
then the default username for the webhook is used.
|
||||
avatar_url: str
|
||||
The avatar URL to send with this message. If no avatar URL is provided
|
||||
then the default avatar for the webhook is used.
|
||||
tts: bool
|
||||
Indicates if the message should be sent using text-to-speech.
|
||||
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.
|
||||
embed: :class:`Embed`
|
||||
The rich embed for the content to send. This cannot be mixed with
|
||||
``embeds`` parameter.
|
||||
embeds: List[:class:`Embed`]
|
||||
A list of embeds to send with the content. Maximum of 10. This cannot
|
||||
be mixed with the ``embed`` parameter.
|
||||
|
||||
Raises
|
||||
--------
|
||||
HTTPException
|
||||
Sending the message failed.
|
||||
NotFound
|
||||
This webhook was not found.
|
||||
Forbidden
|
||||
The authorization token for the webhook is incorrect.
|
||||
InvalidArgument
|
||||
You specified both ``embed`` and ``embeds`` or the length of
|
||||
``embeds`` was invalid.
|
||||
|
||||
Returns
|
||||
---------
|
||||
Optional[:class:`Message`]
|
||||
The message that was sent.
|
||||
"""
|
||||
|
||||
payload = {}
|
||||
|
||||
if files is not None and file is not None:
|
||||
raise InvalidArgument("Cannot mix file and files keyword arguments.")
|
||||
if embeds is not None and embed is not None:
|
||||
raise InvalidArgument("Cannot mix embed and embeds keyword arguments.")
|
||||
|
||||
if embeds is not None:
|
||||
if len(embeds) > 10:
|
||||
raise InvalidArgument("embeds has a maximum of 10 elements.")
|
||||
payload["embeds"] = [e.to_dict() for e in embeds]
|
||||
|
||||
if embed is not None:
|
||||
payload["embeds"] = [embed.to_dict()]
|
||||
|
||||
if content is not None:
|
||||
payload["content"] = str(content)
|
||||
|
||||
payload["tts"] = tts
|
||||
if avatar_url:
|
||||
payload["avatar_url"] = avatar_url
|
||||
if username:
|
||||
payload["username"] = username
|
||||
|
||||
if file is not None:
|
||||
try:
|
||||
to_pass = (file.filename, file.open_file(), "application/octet-stream")
|
||||
return self._adapter.execute_webhook(wait=wait, file=to_pass, payload=payload)
|
||||
finally:
|
||||
file.close()
|
||||
elif files is not None:
|
||||
try:
|
||||
to_pass = [
|
||||
(file.filename, file.open_file(), "application/octet-stream") for file in files
|
||||
]
|
||||
return self._adapter.execute_webhook(wait=wait, files=to_pass, payload=payload)
|
||||
finally:
|
||||
for file in files:
|
||||
file.close()
|
||||
else:
|
||||
return self._adapter.execute_webhook(wait=wait, payload=payload)
|
||||
|
||||
def execute(self, *args, **kwargs):
|
||||
"""An alias for :meth:`~.Webhook.send`."""
|
||||
return self.send(*args, **kwargs)
|
||||
BIN
docs/.resources/code-grant.png
Normal file
BIN
docs/.resources/code-grant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
BIN
docs/.resources/instances-ssh-button.png
Normal file
BIN
docs/.resources/instances-ssh-button.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/.resources/red-console.png
Normal file
BIN
docs/.resources/red-console.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user