Compare commits
703 commits
713eeb1e3a
...
fcd5d5f702
Author | SHA1 | Date | |
---|---|---|---|
fcd5d5f702 | |||
13e20f73ad | |||
04c029b368 | |||
86546dec16 | |||
a2cd241265 | |||
33842a145f | |||
3587154281 | |||
84c0ea6414 | |||
64bf95a872 | |||
fbff5c1fff | |||
0ee278546a | |||
1c1eade166 | |||
1c0c7fa053 | |||
2770d49b07 | |||
1fd5362177 | |||
0fc3ccc518 | |||
99f8d19cef | |||
a612d4231b | |||
11ff68f7e7 | |||
4f46455fa3 | |||
b6f2b74360 | |||
dd95bade31 | |||
ad41accdc3 | |||
0179f3913c | |||
8bff707037 | |||
d37ab8d91c | |||
c8da4839a8 | |||
edb0f8ac2c | |||
b963fad7be | |||
326ec78004 | |||
443c1a2801 | |||
82fa6e9984 | |||
b0c4496f88 | |||
f7e2bc673f | |||
b75b4f7776 | |||
5f568b37de | |||
2035b963eb | |||
867fea273b | |||
e561e821ca | |||
b2f2120529 | |||
f836c146df | |||
54e3463a51 | |||
04f0b80db8 | |||
2e6903dfc9 | |||
be971dda94 | |||
a211fe3048 | |||
f318731882 | |||
f99e419a2b | |||
6e2ab01cbf | |||
73bb685d1c | |||
cae455675b | |||
54b0ac2538 | |||
9944673b1e | |||
aabe972056 | |||
420aeaac95 | |||
4d0bb27451 | |||
66148d90f2 | |||
2ce001784f | |||
d7c37dd959 | |||
d0943ff1dd | |||
e239194636 | |||
d950c458ef | |||
a95d32fbcc | |||
2788083257 | |||
b64eedba6b | |||
77de5d1778 | |||
6668030cd3 | |||
ec0fa5509d | |||
6269510bf9 | |||
527d4b19be | |||
59e42d8182 | |||
e13b74ce38 | |||
943b46d4e9 | |||
110720f278 | |||
d031459349 | |||
a2bbe427be | |||
f8a3b64928 | |||
b11f6ea869 | |||
ef912ef7b8 | |||
0fe4d06b59 | |||
bd0fa7a049 | |||
b1733a9673 | |||
75af7dcd6f | |||
68ccd02fba | |||
29a14d2e3b | |||
74f474b9a7 | |||
a12d9c6dd7 | |||
1625422a5f | |||
433aaee7be | |||
528546664e | |||
d00b878ecd | |||
dc9f49f568 | |||
2d03bbc672 | |||
883565fbf3 | |||
925beb8b2e | |||
f0cbb39d25 | |||
f0ddede3c4 | |||
9b24998c17 | |||
8388cfa840 | |||
fda6b3aae2 | |||
d19696e0ec | |||
087114033c | |||
c63b7a176f | |||
764759b30a | |||
2395aba4f7 | |||
565a7361e6 | |||
d1d16390ef | |||
6cb45a50d5 | |||
ff4eb301c6 | |||
c2650ace52 | |||
4dc82c7b48 | |||
7cfe02cc56 | |||
bc63ed810c | |||
7f05307b78 | |||
757b49a3eb | |||
a173451164 | |||
297aeb0b6e | |||
182eede186 | |||
6f41b1f500 | |||
1cf766820d | |||
26e9e53c80 | |||
a96835326c | |||
ad0b4a6362 | |||
501e5b9452 | |||
e5b5d30553 | |||
e7c2cdceb7 | |||
b897740e76 | |||
c7742a8023 | |||
259da83950 | |||
4f8f606609 | |||
fbb5873884 | |||
d88a34f1a5 | |||
09d8f0535a | |||
ff672d1f81 | |||
9be23219c5 | |||
cb292ec3a7 | |||
9b0e13b744 | |||
b9eb3eddac | |||
9e39888589 | |||
6145ab3df3 | |||
750b12f7d9 | |||
9e46f4c37c | |||
fc2c5b5e5f | |||
8560f29e17 | |||
e32c398290 | |||
011b550ea5 | |||
bf522d33bd | |||
279a3e7116 | |||
40633e9744 | |||
ad91fa4030 | |||
c933457c72 | |||
1c320b5e6e | |||
604e427af4 | |||
715274e99e | |||
d7b9e04742 | |||
e24c8e3a93 | |||
09969ada7b | |||
465c9fa316 | |||
3f7eaaa248 | |||
76a8896773 | |||
d15397dad9 | |||
d66bee2e38 | |||
f36c340f11 | |||
86f9984998 | |||
122461b6e0 | |||
536853b00e | |||
f942852813 | |||
58539d85fa | |||
8877f6f2be | |||
6858a22a11 | |||
37270d6b6c | |||
936040a6c8 | |||
0d3ec6f46f | |||
11b6366360 | |||
5fe0e65cb3 | |||
9019426302 | |||
7ee3725c7e | |||
c4d0a3b2a9 | |||
e97c50edfe | |||
a6d0d507d9 | |||
fb26b7ffcf | |||
164d62a179 | |||
0ff6704206 | |||
4065f63e70 | |||
85e34b32ca | |||
b19d080232 | |||
4312b5f5ef | |||
5a28951eb7 | |||
8adde44fb1 | |||
3f13f6af6d | |||
60cf454122 | |||
549f72d660 | |||
2b4e098d67 | |||
09e6f17a11 | |||
88b8c04117 | |||
8a6ec4e681 | |||
b8153756d4 | |||
9b4fe182e0 | |||
af9eeabe5e | |||
da67359bb3 | |||
ff60827bdf | |||
7fd4cb5abe | |||
f221a0eee5 | |||
78afa83970 | |||
f4ee4e81ae | |||
151b3c4ebf | |||
0a90b8c05a | |||
6facd5794b | |||
acc402297b | |||
43fe8c3a67 | |||
fba57a0747 | |||
d7f7f326b8 | |||
a6be874cb4 | |||
5affddcd46 | |||
aa6f8350db | |||
7b2af300c0 | |||
85d0b8ba74 | |||
0fcd51958a | |||
48ac555131 | |||
fa68cba89a | |||
d0cacd7334 | |||
a8d6c0ff31 | |||
afcbca3e64 | |||
d60ce6122c | |||
29bcea3b21 | |||
c1b13be7e0 | |||
63a7788030 | |||
cbbec95196 | |||
7c4c7facaa | |||
af8b3bcf29 | |||
54967acfbd | |||
c3e8e0fac6 | |||
762bc7c3dc | |||
446990f7e5 | |||
0c53100e7b | |||
d3670b8982 | |||
8f98b4ea95 | |||
6e4c4703c2 | |||
755d7cde06 | |||
c782ad8727 | |||
7964be7adb | |||
fb975f34b0 | |||
ae60ef5eed | |||
9ed107b997 | |||
6ab976681c | |||
f513b92cc7 | |||
55ce47f383 | |||
d3c5332483 | |||
a7a8444e4d | |||
041fd8628e | |||
dec0875e10 | |||
aa2a2aa4ee | |||
caa3854dc2 | |||
8ff72f07bd | |||
b13d6510a4 | |||
45d80bf6b9 | |||
048af76400 | |||
fab533c07a | |||
368914a7c7 | |||
5f571567a3 | |||
c33b16136f | |||
7f261a81fa | |||
e307a5262c | |||
9c25e032b5 | |||
00b4d82c4e | |||
46a1280256 | |||
28e912728b | |||
e42381b5b5 | |||
76a6af2383 | |||
187c6d3ba7 | |||
eaef475410 | |||
a4a47cded3 | |||
a93432d0b7 | |||
29625cb9dc | |||
2c47ab881b | |||
4f816216f8 | |||
13ce1d861e | |||
9fd3891125 | |||
042b2fcaea | |||
ffa1126f01 | |||
0be369fb79 | |||
56ca4dabf2 | |||
086b7f660b | |||
60326c7053 | |||
08a31d284b | |||
a752d3a3f7 | |||
61b6c5d0be | |||
29d6eb4e4c | |||
b0b6400747 | |||
120d95096f | |||
3494066ae7 | |||
ea53c2dbd0 | |||
9ca6f8996f | |||
049973f026 | |||
899c02fd77 | |||
8f078ea0ce | |||
393dc9d6a7 | |||
773e69e481 | |||
21d0dc275f | |||
3d43659ae9 | |||
08bfed110d | |||
575ff60590 | |||
bca36e095b | |||
94955be495 | |||
cc35fa377b | |||
fe7f57e9bb | |||
f4f4fb881e | |||
fd8dfb92bc | |||
1c3cfc7443 | |||
e794c95c50 | |||
39353ea6e7 | |||
fe25f93e61 | |||
3c49b5ecd8 | |||
ebc4b0730f | |||
c859377c32 | |||
75dff5d273 | |||
d1a1d5974d | |||
ab93c3f436 | |||
da114970fb | |||
5f6615d370 | |||
49ac1ac6f3 | |||
ea62d899bf | |||
9f97f107bc | |||
a404763c41 | |||
a8b48bc718 | |||
9b8e3f3875 | |||
0e5e9ab3d4 | |||
90930e547f | |||
7983c07a12 | |||
7d66f3702f | |||
3c42bc4545 | |||
c1fb35a9d6 | |||
345ad7a63c | |||
467cdb57d5 | |||
601561fc94 | |||
01ebe5bf21 | |||
b769c64e01 | |||
b9efeefa89 | |||
fd1fddc450 | |||
d1662e1a82 | |||
d5c49b2347 | |||
0a7cfb42b7 | |||
43a289750d | |||
7525bd1927 | |||
ce22d44a86 | |||
6ab89c1ba5 | |||
e6efcf9042 | |||
df37b78718 | |||
d5a5820983 | |||
a09452247e | |||
dc08406739 | |||
9604d5a4ea | |||
e6bdf6c8f3 | |||
05e6d0404f | |||
ec1df2dd3d | |||
e98f1504cc | |||
51f3696308 | |||
6b805faf0f | |||
0f2f9b628a | |||
14d6cc5e20 | |||
1956d6e5c1 | |||
8c1f23e59b | |||
8abe27e85c | |||
eedcd9572a | |||
60e9348896 | |||
3fb4d3f337 | |||
25c6a89e20 | |||
a65ffeb221 | |||
534dd29f4f | |||
ef531bd2d2 | |||
b0a965031f | |||
dbe5cf6110 | |||
7f2c542b3a | |||
898fd8b7f4 | |||
6779dfca03 | |||
e43808f40c | |||
efc1a3cf6d | |||
893673aa94 | |||
1cc88ce6ab | |||
7c30ed595a | |||
83e68a4c7f | |||
bf5f65b0b7 | |||
d5b1f0a64d | |||
74896d921e | |||
e7a4f58229 | |||
9544ccc425 | |||
2e159cf7c4 | |||
ce6b831b93 | |||
4178298065 | |||
aebaa9b282 | |||
942e1b0a09 | |||
159efa6dee | |||
19507d06ba | |||
0f17624b58 | |||
13643bd1a2 | |||
32be71d4ec | |||
38018674bc | |||
11fae59047 | |||
6f4a5ee0ba | |||
fcf88680ce | |||
6d18799dd8 | |||
47f0cfebfa | |||
dae3e4761e | |||
4db5e57a98 | |||
a5daa026d5 | |||
917ca70901 | |||
8860cd90a7 | |||
ccc70f0c4b | |||
38792b1a0b | |||
184256c905 | |||
c519896715 | |||
84c096df4b | |||
e9354febc4 | |||
97ae123c71 | |||
dd9b3a4f1b | |||
29680724b0 | |||
2a5e6b96b1 | |||
2e1fd109f1 | |||
ca8bcd8adf | |||
a15e2862ee | |||
37e493b101 | |||
d0842037f3 | |||
7abcbe45d0 | |||
d792f1d648 | |||
3e14ddfb89 | |||
79ed786bb6 | |||
f1d588688a | |||
c6b534da0b | |||
6277b5d78b | |||
6d2917c31b | |||
645bfef0b6 | |||
0549f8134d | |||
86de528733 | |||
e3e2a312d0 | |||
aef72178a8 | |||
3b469fbe73 | |||
913ca3ce68 | |||
812988f00d | |||
f0ce98e731 | |||
622be1e2ae | |||
bfa859ac1e | |||
75b0d2705f | |||
9fda74775c | |||
d61f2fa472 | |||
80c90f8d80 | |||
8f9e514260 | |||
1e451942be | |||
4cb478eb8e | |||
4ebfcf6613 | |||
064e182dde | |||
e282e1e073 | |||
60b1510c5c | |||
5451395e0a | |||
92514b6987 | |||
97f1da001b | |||
24a7a64b07 | |||
77848aef01 | |||
0c30aca9d9 | |||
005ac4179b | |||
1d859e0995 | |||
cbd522991a | |||
bd7b5a5ca5 | |||
6dcaf2ef99 | |||
bfb744bb29 | |||
483698ea29 | |||
a793378093 | |||
aa610bd1d8 | |||
00b49f7445 | |||
67dd73c0af | |||
7658241913 | |||
e4ef3c075b | |||
84746e23bb | |||
38cc78c09e | |||
85d1cdbe2d | |||
ab5d73a742 | |||
3754fbc565 | |||
6aab8267f9 | |||
c295265e0b | |||
89129a5d88 | |||
baa16d6e18 | |||
d7f3307e62 | |||
86a8d57666 | |||
43b32badc4 | |||
0d66448338 | |||
d516551de6 | |||
a12a18db14 | |||
e574e2d532 | |||
cc6c867306 | |||
863903c765 | |||
5aa87ce653 | |||
32f195b2f1 | |||
71772a4e19 | |||
ca8c3625a4 | |||
6704af7508 | |||
e7133bc25f | |||
ead82dc4aa | |||
90598f26bf | |||
99ac6fdd6c | |||
96e17e9cb6 | |||
a30dae0735 | |||
c821adf87a | |||
7e1ca280de | |||
543c82a5c4 | |||
ccee1dc046 | |||
43de52fae7 | |||
6be0c4c93d | |||
50cca0c7ff | |||
6199d867dc | |||
62c9c764d6 | |||
cc470c86db | |||
c34532cf09 | |||
5d487a7f78 | |||
694c848517 | |||
34fae41208 | |||
40b4977a32 | |||
cf4fc12831 | |||
173baaba14 | |||
e011b56873 | |||
92f62c6ce8 | |||
b59b57f716 | |||
60f2b0e2e5 | |||
d485f64f25 | |||
7f48f11586 | |||
53f9183ab6 | |||
5ac2d6f8ce | |||
bfd3f75836 | |||
a21fcdeec5 | |||
ba24b24933 | |||
8181eca8dc | |||
58b8a0d125 | |||
46e28b81a1 | |||
ac8689881d | |||
38133ad8bb | |||
e531d0ab4d | |||
71a9512888 | |||
f69b947cfb | |||
78b511c867 | |||
79994c6cf4 | |||
74d22f2d8d | |||
6029797f1c | |||
3f2f7e77a3 | |||
f99afec44f | |||
c4b29224a6 | |||
a45f37324e | |||
e0136bce2f | |||
66bafe381a | |||
063d64e86c | |||
d0f96db9b5 | |||
f31dc9c24b | |||
503f710a4b | |||
66cba8a0a7 | |||
5631913021 | |||
18a36ad9a0 | |||
08980f4cbb | |||
24a23f1277 | |||
7f2db7b3a6 | |||
e8b725484a | |||
26bd3931a6 | |||
dd3742a318 | |||
bf7326dccb | |||
bc0aac4009 | |||
a77d22effc | |||
1296ab6d39 | |||
989d909c2f | |||
37a5fb3701 | |||
20734739a8 | |||
f6d143dd79 | |||
586e0e45f8 | |||
75029b9abb | |||
86169750db | |||
69c7562c58 | |||
6af06cfa30 | |||
b94a2438b1 | |||
5e0a4a763a | |||
e1b8bdcbe9 | |||
5cf232edf6 | |||
5f9283fff9 | |||
29242efdf0 | |||
2a82a7d78a | |||
0968284ede | |||
ea8535a647 | |||
93a245ffa5 | |||
3edd9b64de | |||
0806cdadb2 | |||
30df3be94a | |||
a5d8e849aa | |||
4a7eaaf4da | |||
8e2c943d24 | |||
e985eae89a | |||
078f1bbdf4 | |||
11142f9fa8 | |||
a5b21eaa68 | |||
13f2b89e81 | |||
788743de25 | |||
66099fe6b1 | |||
d0bd554b04 | |||
e9a4d5aee8 | |||
9cccbdb8de | |||
11cee7e7a4 | |||
6de148a1c7 | |||
748bc4f374 | |||
cbc29c226f | |||
67d6c43e96 | |||
c0c135fb3d | |||
5c90d3e7fa | |||
f2ad84e0c5 | |||
8f4421e6a1 | |||
c2b20a9a29 | |||
e158d18c4f | |||
ae46593ec9 | |||
7d54882549 | |||
209e1522f1 | |||
0c25f4ab6f | |||
8f488654c5 | |||
efcd7ad251 | |||
2c5c69cfce | |||
94c5ef1873 | |||
a88ee2972b | |||
e0534d1d5b | |||
c1c1a7c20f | |||
9caf781289 | |||
582e33d907 | |||
22c4b57991 | |||
dcfbc0a3f0 | |||
630a11c1cf | |||
1473dc9e67 | |||
1e535a3842 | |||
7a41730f81 | |||
6007237f10 | |||
9311ac78a6 | |||
e3e1474d38 | |||
a61f79004e | |||
ca7967057d | |||
2a966124c3 | |||
7459ac1955 | |||
45ab645e1a | |||
78a46b2482 | |||
9e86472b64 | |||
8699b0e303 | |||
67d717cff7 | |||
c49bae11a8 | |||
00c8f2509f | |||
191a48ce9f | |||
6a6ae6683d | |||
e5e1ef7638 | |||
6928c13cfd | |||
d6b1b8bc49 | |||
5c20112ff3 | |||
2b1d34707a | |||
606f9c1d27 | |||
b93f104965 | |||
78352bea85 | |||
9e057dbc07 | |||
fd5d3f1538 | |||
993ad7ce43 | |||
fa8d03c6c2 | |||
4e09b7a737 | |||
1a2a1f070c | |||
81b0500733 | |||
17c853caae | |||
d43c854584 | |||
16aaf3f01b | |||
286af4e356 | |||
1800c9ab5c | |||
b0a6328b57 | |||
1d3ab2fb20 | |||
a22cf3e414 | |||
ac23687696 | |||
38268f4886 | |||
d8a053eb67 | |||
85e70c9a11 | |||
ffadb4a634 | |||
6ed044c1cc | |||
18e5648b41 | |||
e482f671d8 | |||
17b9e0253b | |||
061562b60d | |||
ca5bcbdc80 | |||
58eb623a1a | |||
7a3e7afe33 | |||
b3976c9d8a | |||
69a92431d8 | |||
0823c19b94 | |||
1e2f44d494 | |||
bb08575b98 | |||
365501bef2 | |||
c6d6661204 | |||
78187197b3 | |||
f91e992d25 | |||
4018db2dac | |||
67d8fedef8 | |||
2b9d8a2dca | |||
d82127fa38 | |||
2852576250 | |||
d9c654f73d | |||
c6c7fbf73b | |||
b99318f51b | |||
16b9dc36c7 | |||
82a26df045 | |||
3543d205f2 | |||
a05b768274 | |||
23e69a01c6 | |||
4b4649b0de |
11
.gitignore
vendored
|
@ -1,24 +1,15 @@
|
|||
config.yml
|
||||
config.json
|
||||
.idea
|
||||
__pycache__
|
||||
nohup.out
|
||||
restartcron.sh
|
||||
GitAutoDeploy.conf.json
|
||||
.htaccess
|
||||
autoupdate.php
|
||||
error_log
|
||||
images/banner/tmp/*
|
||||
.ftpquota
|
||||
bonfire.log
|
||||
*.sublime-*
|
||||
cert.pem
|
||||
.bash*
|
||||
.cache
|
||||
.python_history
|
||||
.config/
|
||||
audio_tmp/*
|
||||
.viminfo
|
||||
docs/_build
|
||||
.ssh/*
|
||||
.gitconfig
|
||||
.*
|
||||
|
|
39
README.md
|
@ -1,47 +1,36 @@
|
|||
# Bonfire
|
||||
|
||||
This is for a Discord bot using the discord.py wrapper made for fun, used in a couple of my own servers.
|
||||
![fuyu approved](https://img.shields.io/badge/fuyu-approved-green.svg)
|
||||
|
||||
This is for a Discord bot using the discord.py wrapper made for fun, used in a couple of my own servers that somehow got popular I guess?
|
||||
|
||||
If you'd like to add this bot to one of your own servers, please visit the following URL:
|
||||
https://discordapp.com/oauth2/authorize?client_id=183748889814237186&scope=bot&permissions=0
|
||||
|
||||
This requires the discord.py library, as well as all of its dependencies.
|
||||
https://github.com/Rapptz/discord.py
|
||||
|
||||
To save the data for the bot, rethinkdb is what is used:
|
||||
https://www.rethinkdb.com/docs/install/
|
||||
|
||||
I also use a few libraries that aren't included by default, which can be installed using pip.
|
||||
I will not assist with, nor provide instructions on the setup for rethinkdb.
|
||||
|
||||
In order to install the requirements for Bonfire you will first need to install python3.5. Once that is installed, run the following (replacing python with the correct executable based on your installation):
|
||||
|
||||
|
||||
```
|
||||
python3.5 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline
|
||||
# Or on windows
|
||||
py -3 -m pip install discord.py[voice] BeautifulSoup4 youtube_dl rethinkdb ruamel.yaml pendulum Pillow==3.4.1 readline
|
||||
#NOTE: To use the requirements.txt file, you need to be in the installation directory for this bot.
|
||||
python -m pip install --upgrade -r requirements.txt
|
||||
```
|
||||
|
||||
Note: ATM of writing this, Pillow 3.4.2 (the stable version...good job Pillow?) is broken, do not use pip's default to install this. This is why we're using Pillow==3.4.1 above, and not just Pillow
|
||||
|
||||
The joke command requires the fortune-mod package, which are installable on Linux and other Unix-based distros.
|
||||
|
||||
Debian, Ubuntu, etc.:
|
||||
```
|
||||
sudo apt install fortune-mod
|
||||
```
|
||||
If you're on Red Hat, Fedora, etc., replace ``apt`` with ``yum``. If you're on another Linux distro, I trust you know what package manager to use. If you're on Windows, you might want to check out [this guide](http://superuser.com/questions/683162/bsd-fortune-for-windows-command-prompt-or-dos), but you're on your own.
|
||||
|
||||
The only required file to modify would be the config.yml.sample file. The entries are as follows:
|
||||
The only required file to modify would be the config.yml.sample file. Copy this file to config.yml and edit the entries as needed; the entries are as follows:
|
||||
|
||||
- bot_token: The token that can be retrieved from the [bot's application page](https://discordapp.com/developers/applications/me)
|
||||
- owner_id: This is your ID, which can be retrieved by right clicking your name in the discord application, when developer mode is on
|
||||
- description: Self explanotory, the description for the bot
|
||||
- description: Self explanatory, the description for the bot
|
||||
- command_prefix: A list of the prefixes you want the bot to respond to, if none is provided in the config file ! will be used
|
||||
- default_status: The default status to use when the bot is booted up, which will populate the "game" that the bot is playing
|
||||
- discord_bots_key: The key for the [bots.discord.pw site](https://bots.discord.pw/#g=1), if you don't have a key just leave it blank, it should fail and log the failure
|
||||
- carbon_key: The key used for the [carbonitex site](https://www.carbonitex.net/discord/bots)
|
||||
- twitch_key: The twitch token that is used for the API calls
|
||||
- youtube_dl_proxy: A URL that can be used to proxy the calls youtube_dl goes through. Useful if your server is in a location that has many youtube videos blocked.
|
||||
- youtube_key: The key used for youtube API calls
|
||||
- osu_key: The key used for Osu API calls
|
||||
- shard_count: This is the number of shards the bot is split over. 1 needs to be used if the bot is not being sharded
|
||||
- shard_id: This will be the ID of the shard in particular, 0 if sharding is not used
|
||||
- extensions: This is a list of the extensions loaded into the bot (check the cogs folder for the extensions available). The disabled playlist is a special entry....read that file for what its purpose is....most likely you will not need it. Entries in this list need to be separated by ", " like in the example.
|
||||
- db_*: This is the information for the rethinkdb database. The cert is the certificate used for driver connections
|
||||
|
||||
- db_*: This is the information for the rethinkdb database.
|
||||
|
|
211
bot.py
|
@ -1,4 +1,3 @@
|
|||
#!/usr/local/bin/python3.5
|
||||
import discord
|
||||
import traceback
|
||||
import logging
|
||||
|
@ -10,120 +9,124 @@ import aiohttp
|
|||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
from discord.ext import commands
|
||||
from cogs import utils
|
||||
import utils
|
||||
|
||||
opts = {'command_prefix': utils.command_prefix,
|
||||
'description': utils.bot_description,
|
||||
'pm_help': None,
|
||||
'shard_count': utils.shard_count,
|
||||
'shard_id': utils.shard_id,
|
||||
'command_not_found': ''}
|
||||
opts = {
|
||||
'command_prefix': utils.command_prefix,
|
||||
'description': utils.bot_description,
|
||||
'pm_help': None,
|
||||
'command_not_found': '',
|
||||
'activity': discord.Activity(name=utils.default_status, type=0)
|
||||
}
|
||||
|
||||
bot = commands.Bot(**opts)
|
||||
logging.basicConfig(level=logging.WARNING, filename='bonfire.log')
|
||||
bot = commands.AutoShardedBot(**opts)
|
||||
logging.basicConfig(level=logging.INFO, filename='bonfire.log')
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
# Change the status upon connection to the default status
|
||||
await bot.change_presence(game=discord.Game(name=utils.default_status, type=0))
|
||||
|
||||
if not hasattr(bot, 'uptime'):
|
||||
bot.uptime = pendulum.utcnow()
|
||||
await utils.db_check()
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
if message.author.bot:
|
||||
return
|
||||
await bot.process_commands(message)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_command_completion(command, ctx):
|
||||
# There's no reason to continue waiting for this to complete, so lets immediately launch this in a new future
|
||||
bot.loop.create_task(process_command(ctx))
|
||||
|
||||
|
||||
async def process_command(ctx):
|
||||
author = ctx.message.author
|
||||
server = ctx.message.server
|
||||
command = ctx.command
|
||||
|
||||
command_usage = await utils.get_content('command_usage', key=command.qualified_name)
|
||||
if command_usage is None:
|
||||
command_usage = {'command': command.qualified_name}
|
||||
|
||||
# Add one to the total usage for this command, basing it off 0 to start with (obviously)
|
||||
total_usage = command_usage.get('total_usage', 0) + 1
|
||||
command_usage['total_usage'] = total_usage
|
||||
|
||||
# Add one to the author's usage for this command
|
||||
total_member_usage = command_usage.get('member_usage', {})
|
||||
member_usage = total_member_usage.get(author.id, 0) + 1
|
||||
total_member_usage[author.id] = member_usage
|
||||
command_usage['member_usage'] = total_member_usage
|
||||
|
||||
# Add one to the server's usage for this command
|
||||
if ctx.message.server is not None:
|
||||
total_server_usage = command_usage.get('server_usage', {})
|
||||
server_usage = total_server_usage.get(server.id, 0) + 1
|
||||
total_server_usage[server.id] = server_usage
|
||||
command_usage['server_usage'] = total_server_usage
|
||||
|
||||
# Save all the changes
|
||||
if not await utils.update_content('command_usage', command_usage, command.qualified_name):
|
||||
await utils.add_content('command_usage', command_usage)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(error, ctx):
|
||||
if isinstance(error, commands.CommandNotFound):
|
||||
return
|
||||
if isinstance(error, commands.DisabledCommand):
|
||||
return
|
||||
@bot.before_invoke
|
||||
async def start_typing(ctx):
|
||||
try:
|
||||
if isinstance(error.original, discord.Forbidden):
|
||||
return
|
||||
elif isinstance(error.original, discord.HTTPException) and 'empty message' in str(error.original):
|
||||
return
|
||||
elif isinstance(error.original, aiohttp.ClientOSError):
|
||||
return
|
||||
except AttributeError:
|
||||
await ctx.trigger_typing()
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
|
||||
if isinstance(error, commands.BadArgument):
|
||||
fmt = "Please provide a valid argument to pass to the command: {}".format(error)
|
||||
await bot.send_message(ctx.message.channel, fmt)
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
fmt = "You can't tell me what to do!"
|
||||
await bot.send_message(ctx.message.channel, fmt)
|
||||
elif isinstance(error, commands.CommandOnCooldown):
|
||||
m, s = divmod(error.retry_after, 60)
|
||||
fmt = "This command is on cooldown! Hold your horses! >:c\nTry again in {} minutes and {} seconds" \
|
||||
.format(round(m), round(s))
|
||||
await bot.send_message(ctx.message.channel, fmt)
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
fmt = "This command cannot be used in a private message"
|
||||
await bot.send_message(ctx.message.channel, fmt)
|
||||
elif isinstance(error, commands.MissingRequiredArgument):
|
||||
await bot.send_message(ctx.message.channel, error)
|
||||
else:
|
||||
now = datetime.datetime.now()
|
||||
with open("error_log", 'a') as f:
|
||||
print("In server '{0.message.server}' at {1}\nFull command: `{0.message.content}`".format(ctx, str(now)),
|
||||
file=f)
|
||||
try:
|
||||
traceback.print_tb(error.original.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error.original), file=f)
|
||||
except:
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error), file=f)
|
||||
|
||||
@bot.event
|
||||
async def on_command_completion(ctx):
|
||||
author = ctx.author.id
|
||||
guild = ctx.guild.id if ctx.guild else None
|
||||
command = ctx.command.qualified_name
|
||||
|
||||
await bot.db.execute(
|
||||
"INSERT INTO command_usage(command, guild, author) VALUES ($1, $2, $3)",
|
||||
command,
|
||||
guild,
|
||||
author
|
||||
)
|
||||
|
||||
# Now add credits to a users amount
|
||||
# user_credits = bot.db.load('credits', key=ctx.author.id, pluck='credits') or 1000
|
||||
# user_credits = int(user_credits) + 5
|
||||
# update = {
|
||||
# 'member_id': str(ctx.author.id),
|
||||
# 'credits': user_credits
|
||||
# }
|
||||
# await bot.db.save('credits', update)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx, error):
|
||||
error = error.original if hasattr(error, "original") else error
|
||||
ignored_errors = (
|
||||
commands.CommandNotFound,
|
||||
commands.DisabledCommand,
|
||||
discord.Forbidden,
|
||||
aiohttp.ClientOSError,
|
||||
commands.CheckFailure,
|
||||
commands.CommandOnCooldown,
|
||||
)
|
||||
|
||||
if isinstance(error, ignored_errors):
|
||||
return
|
||||
elif isinstance(error, discord.HTTPException) and (
|
||||
'empty message' in str(error) or
|
||||
'INTERNAL SERVER ERROR' in str(error) or
|
||||
'REQUEST ENTITY TOO LARGE' in str(error) or
|
||||
'Unknown Message' in str(error) or
|
||||
'Origin Time-out' in str(error) or
|
||||
'Bad Gateway' in str(error) or
|
||||
'Gateway Time-out' in str(error) or
|
||||
'Explicit content' in str(error)):
|
||||
return
|
||||
elif isinstance(error, discord.NotFound) and 'Unknown Channel' in str(error):
|
||||
return
|
||||
|
||||
try:
|
||||
if isinstance(error, commands.BadArgument):
|
||||
fmt = "Please provide a valid argument to pass to the command: {}".format(error)
|
||||
await ctx.message.channel.send(fmt)
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
fmt = "This command cannot be used in a private message"
|
||||
await ctx.message.channel.send(fmt)
|
||||
elif isinstance(error, commands.MissingRequiredArgument):
|
||||
await ctx.message.channel.send(error)
|
||||
elif isinstance(error, (
|
||||
commands.InvalidEndOfQuotedStringError,
|
||||
commands.ExpectedClosingQuoteError,
|
||||
commands.UnexpectedQuoteError)
|
||||
):
|
||||
await ctx.message.channel.send("Quotes must go around the arguments you want to provide to the command,"
|
||||
" recheck where your quotes are")
|
||||
else:
|
||||
if isinstance(bot.error_channel, int):
|
||||
bot.error_channel = bot.get_channel(bot.error_channel)
|
||||
|
||||
if bot.error_channel is None:
|
||||
now = datetime.datetime.now()
|
||||
with open("error_log", 'a') as f:
|
||||
print("In server '{0.message.guild}' at {1}\n"
|
||||
"Full command: `{0.message.content}`".format(ctx, str(now)), file=f)
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error), file=f)
|
||||
else:
|
||||
await bot.error_channel.send(f"""```
|
||||
Command = {discord.utils.escape_markdown(ctx.message.clean_content).strip()}
|
||||
{''.join(traceback.format_tb(error.__traceback__)).strip()}
|
||||
{error.__class__.__name__}: {error}```""")
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.remove_command('help')
|
||||
|
||||
# Setup our bot vars, db and cache
|
||||
bot.db = utils.DB()
|
||||
bot.cache = utils.Cache(bot.db)
|
||||
bot.error_channel = utils.error_channel
|
||||
# Start our startup task (cache sets up the database, so just this)
|
||||
bot.loop.create_task(bot.cache.setup())
|
||||
for e in utils.extensions:
|
||||
bot.load_extension(e)
|
||||
|
||||
bot.uptime = pendulum.now(tz="UTC")
|
||||
bot.run(utils.bot_token)
|
||||
|
|
584
cogs/admin.py
Normal file
|
@ -0,0 +1,584 @@
|
|||
import discord
|
||||
import utils
|
||||
|
||||
from asyncpg import UniqueViolationError
|
||||
from discord.ext import commands
|
||||
|
||||
valid_perms = list(discord.Permissions.VALID_FLAGS.keys())
|
||||
|
||||
|
||||
class Admin(commands.Cog):
|
||||
"""These are commands that allow more intuitive configuration, that don't fit into the config command"""
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def disable(self, ctx, *, command):
|
||||
"""Disables the use of a command on this server"""
|
||||
if command == "disable" or command == "enable":
|
||||
return await ctx.send("You cannot disable `{}`".format(command))
|
||||
|
||||
cmd = ctx.bot.get_command(command)
|
||||
if cmd is None:
|
||||
return await ctx.send("No command called `{}`".format(command))
|
||||
|
||||
try:
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO restrictions (source, destination, from_to, guild) VALUES ($1, 'everyone', 'from', $2)",
|
||||
cmd.qualified_name,
|
||||
ctx.guild.id
|
||||
)
|
||||
except UniqueViolationError:
|
||||
await ctx.send(f"{cmd.qualified_name} is already disabled")
|
||||
else:
|
||||
await ctx.send(f"{cmd.qualified_name} is now disabled")
|
||||
ctx.bot.cache.add_restriction(
|
||||
ctx.guild,
|
||||
"from",
|
||||
{"source": cmd.qualified_name, "destination": "everyone"}
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def enable(self, ctx, *, command):
|
||||
"""Enables the use of a command on this server"""
|
||||
cmd = ctx.bot.get_command(command)
|
||||
if cmd is None:
|
||||
await ctx.send("No command called `{}`".format(command))
|
||||
return
|
||||
|
||||
query = f"""
|
||||
DELETE FROM restrictions WHERE
|
||||
source=$1 AND
|
||||
from_to='from' AND
|
||||
destination='everyone' AND
|
||||
guild=$2
|
||||
"""
|
||||
await ctx.bot.db.execute(query, cmd.qualified_name, ctx.guild.id)
|
||||
ctx.bot.cache.remove_restriction(
|
||||
ctx.guild,
|
||||
"from",
|
||||
{"source": cmd.qualified_name, "destination": "everyone"}
|
||||
)
|
||||
await ctx.send(f"{cmd.qualified_name} is no longer disabled")
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def notify(self, ctx, role: discord.Role, *, message):
|
||||
"""
|
||||
Notify everyone in "role" with "message"
|
||||
This sets the role to mentionable, mentions the role, then sets it back
|
||||
"""
|
||||
if not ctx.me.guild_permissions.manage_roles:
|
||||
await ctx.send("I do not have permissions to edit roles (this is required to complete this command)")
|
||||
return
|
||||
try:
|
||||
await role.edit(mentionable=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send("I do not have permissions to edit that role. "
|
||||
"(I either don't have manage roles permissions, or it is higher on the hierarchy)")
|
||||
else:
|
||||
fmt = f"{role.mention}\n{message}"
|
||||
await ctx.send(fmt)
|
||||
await role.edit(mentionable=False)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def restrictions(self, ctx):
|
||||
"""Used to list all the current restrictions set
|
||||
|
||||
EXAMPLE: !restrictions
|
||||
RESULT: All the current restrictions"""
|
||||
restrictions = await ctx.bot.db.fetch(
|
||||
"SELECT source, destination, from_to FROM restrictions WHERE guild=$1",
|
||||
ctx.guild.id
|
||||
)
|
||||
|
||||
entries = []
|
||||
for restriction in restrictions:
|
||||
# Check whether it's from or to to change what the format looks like
|
||||
dest = restriction["destination"]
|
||||
if dest != "everyone":
|
||||
dest = await utils.convert(ctx, restriction["destination"])
|
||||
# If it doesn't exist, don't add it
|
||||
if dest:
|
||||
entries.append(f"{restriction['source']} {'from' if restriction['from_to'] == 'from' else 'to'} {dest}")
|
||||
|
||||
if entries:
|
||||
# Then paginate
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
await ctx.send("There are no restrictions!")
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def restrict(self, ctx, *options):
|
||||
"""
|
||||
This is an intuitive command to restrict something to/from something
|
||||
The format is `!restrict what from/to who/where`
|
||||
|
||||
For example, `!restrict command to role` will require a user to have `role`
|
||||
to be able to run `command`
|
||||
`!restrict command to channel` will only allow `command` to be ran in `channel`
|
||||
|
||||
EXAMPLE: !restrict boop from @user
|
||||
RESULT: This user can no longer use the boop command
|
||||
"""
|
||||
# First make sure we're given three options
|
||||
if len(options) != 3:
|
||||
await ctx.send("You need to provide 3 options! Such as `command from @User`")
|
||||
return
|
||||
elif ctx.message.mention_everyone:
|
||||
await ctx.send("Please do not use this command to 'disable from everyone'. Use the `disable` command")
|
||||
return
|
||||
else:
|
||||
# Get the three arguments from this list, then make sure the 2nd is either from or to
|
||||
arg1, arg2, arg3 = options
|
||||
if arg2.lower() not in ['from', 'to']:
|
||||
await ctx.send("The 2nd option needs to be either \"to\" or \"from\". Such as: `command from @user` "
|
||||
"or `command to Role`")
|
||||
return
|
||||
else:
|
||||
# Try to convert the other arguments
|
||||
arg2 = arg2.lower()
|
||||
option1 = await utils.convert(ctx, arg1)
|
||||
option2 = await utils.convert(ctx, arg3)
|
||||
if option1 is None or option2 is None:
|
||||
await ctx.send("Sorry, but I don't know how to restrict {} {} {}".format(arg1, arg2, arg3))
|
||||
return
|
||||
|
||||
from_to = arg2
|
||||
source = None
|
||||
destination = None
|
||||
overwrites = None
|
||||
|
||||
# The possible options:
|
||||
# Member
|
||||
# Role
|
||||
# Command
|
||||
# Text/Voice Channel
|
||||
|
||||
if isinstance(option1, (commands.core.Command, commands.core.Group)):
|
||||
# From:
|
||||
# Users - Command can't be run by this person
|
||||
# Channels - Command can't be ran in this channel
|
||||
# Roles - Command can't be ran by anyone in this role (least likely, but still possible uses)
|
||||
if arg2 == "from":
|
||||
if isinstance(option2, (discord.Member, discord.Role, discord.TextChannel)):
|
||||
source = option1.qualified_name
|
||||
destination = str(option2.id)
|
||||
# To:
|
||||
# Channels - Command can only be run in this channel
|
||||
# Roles - This role is required in order to run this command
|
||||
else:
|
||||
if isinstance(option2, (discord.Role, discord.TextChannel)):
|
||||
source = option1.qualified_name
|
||||
destination = str(option2.id)
|
||||
elif isinstance(option1, discord.Member):
|
||||
# From:
|
||||
# Channels - Setup an overwrite for this channel so that they cannot read it
|
||||
# Command - Command cannot be used by this user
|
||||
if arg2 == "from":
|
||||
if isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
|
||||
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=False)
|
||||
else:
|
||||
ov = discord.PermissionOverwrite(read_messages=False)
|
||||
overwrites = {
|
||||
'channel': option2,
|
||||
option1: ov
|
||||
}
|
||||
elif isinstance(option2, (commands.core.Command, commands.core.Group)):
|
||||
source = option2.qualified_name
|
||||
destination = str(option1.id)
|
||||
elif isinstance(option1, (discord.TextChannel, discord.VoiceChannel)):
|
||||
# From:
|
||||
# Command - Command cannot be used in this channel
|
||||
# Member - Setup an overwrite for this channel so that they cannot read it
|
||||
# Role - Setup an overwrite for this channel so that this Role cannot read it
|
||||
if arg2 == "from":
|
||||
if isinstance(option2, (discord.Member, discord.Role)):
|
||||
ov = discord.utils.find(lambda t: t[0] == option2, option1.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=False)
|
||||
else:
|
||||
ov = discord.PermissionOverwrite(read_messages=False)
|
||||
overwrites = {
|
||||
'channel': option1,
|
||||
option2: ov
|
||||
}
|
||||
elif isinstance(option2, (commands.core.Command, commands.core.Group)) \
|
||||
and isinstance(option1, discord.TextChannel):
|
||||
source = option2.qualified_name
|
||||
destination = str(option1.id)
|
||||
# To:
|
||||
# Command - Command can only be used in this channel
|
||||
# Role - Setup an overwrite so only this role can read this channel
|
||||
else:
|
||||
if isinstance(option2, (commands.core.Command, commands.core.Group)) \
|
||||
and isinstance(option1, discord.TextChannel):
|
||||
source = option2.qualified_name
|
||||
destination = str(option1.id)
|
||||
elif isinstance(option2, (discord.Member, discord.Role)):
|
||||
ov = discord.utils.find(lambda t: t[0] == option2, option1.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=True)
|
||||
else:
|
||||
ov = discord.PermissionOverwrite(read_messages=True)
|
||||
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role,
|
||||
option1.overwrites)
|
||||
if ov2:
|
||||
ov2 = ov2[1]
|
||||
ov2.update(read_messages=False)
|
||||
else:
|
||||
ov2 = discord.PermissionOverwrite(read_messages=False)
|
||||
overwrites = {
|
||||
'channel': option1,
|
||||
option2: ov,
|
||||
ctx.message.guild.default_role: ov2
|
||||
}
|
||||
elif isinstance(option1, discord.Role):
|
||||
# From:
|
||||
# Command - No one with this role can run this command
|
||||
# Channel - Setup an overwrite for this channel so that this Role cannot read it
|
||||
if arg2 == "from":
|
||||
if isinstance(option2, (commands.core.Command, commands.core.Group)):
|
||||
source = option2.qualified_name
|
||||
destination = option1.id
|
||||
elif isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
|
||||
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=False)
|
||||
else:
|
||||
ov = discord.PermissionOverwrite(read_messages=False)
|
||||
overwrites = {
|
||||
'channel': option2,
|
||||
option1: ov
|
||||
}
|
||||
# To:
|
||||
# Command - You have to have this role to run this command
|
||||
# Channel - Setup an overwrite so you have to have this role to read this channel
|
||||
else:
|
||||
if isinstance(option2, (discord.TextChannel, discord.VoiceChannel)):
|
||||
ov = discord.utils.find(lambda t: t[0] == option1, option2.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=True)
|
||||
else:
|
||||
ov = discord.PermissionOverwrite(read_messages=True)
|
||||
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role,
|
||||
option2.overwrites)
|
||||
if ov2:
|
||||
ov2 = ov2[1]
|
||||
ov2.update(read_messages=False)
|
||||
else:
|
||||
ov2 = discord.PermissionOverwrite(read_messages=False)
|
||||
overwrites = {
|
||||
'channel': option2,
|
||||
option1: ov,
|
||||
ctx.message.guild.default_role: ov2
|
||||
}
|
||||
elif isinstance(option2, (commands.core.Command, commands.core.Group)):
|
||||
source = option2.qualified_name
|
||||
destination = str(option1.id)
|
||||
|
||||
if source is not None and destination is not None:
|
||||
try:
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO restrictions (guild, source, destination, from_to) VALUES ($1, $2, $3, $4)",
|
||||
ctx.guild.id,
|
||||
source,
|
||||
destination,
|
||||
from_to
|
||||
)
|
||||
except UniqueViolationError:
|
||||
# If it's already inserted, then nothing needs to be updated
|
||||
# It just means this particular restriction is already set
|
||||
pass
|
||||
else:
|
||||
ctx.bot.cache.add_restriction(ctx.guild, from_to, {"source": source, "destination": destination})
|
||||
elif overwrites:
|
||||
channel = overwrites.pop('channel')
|
||||
for target, setting in overwrites.items():
|
||||
await channel.set_permissions(target, overwrite=setting)
|
||||
else:
|
||||
await ctx.send("Sorry but I don't know how to restrict {} {} {}".format(arg1, arg2, arg3))
|
||||
return
|
||||
|
||||
await ctx.send("I have just restricted {} {} {}".format(arg1, arg2, arg3))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def unrestrict(self, ctx, *options):
|
||||
"""
|
||||
This is an intuitive command to unrestrict something to/from something
|
||||
The format is `!restrict what from/to who/where`
|
||||
|
||||
For example, `!unrestrict command to role` will remove the restriction on this command, requiring role
|
||||
|
||||
EXAMPLE: !unrestrict boop from @user
|
||||
RESULT: The restriction on this user to use boop has been lifted
|
||||
"""
|
||||
# First make sure we're given three options
|
||||
if len(options) != 3:
|
||||
await ctx.send("You need to provide 3 options! Such as `command from @User`")
|
||||
return
|
||||
else:
|
||||
# Get the three arguments from this list, then make sure the 2nd is either from or to
|
||||
arg1, arg2, arg3 = options
|
||||
if arg2.lower() not in ['from', 'to']:
|
||||
await ctx.send("The 2nd option needs to be either \"to\" or \"from\". Such as: `command from @user` "
|
||||
"or `command to Role`")
|
||||
return
|
||||
else:
|
||||
# Try to convert the other arguments
|
||||
arg2 = arg2.lower()
|
||||
option1 = await utils.convert(ctx, arg1)
|
||||
option2 = await utils.convert(ctx, arg3)
|
||||
if option1 is None or option2 is None:
|
||||
await ctx.send("Sorry, but I don't know how to unrestrict {} {} {}".format(arg1, arg2, arg3))
|
||||
return
|
||||
|
||||
# First check if this is a blacklist/whitelist (by checking if we are unrestricting commands)
|
||||
if any(isinstance(x, (commands.core.Command, commands.core.Group)) for x in [option1, option2]):
|
||||
# The source should always be the command, so just set this based on which order is given (either is
|
||||
# allowed)
|
||||
if isinstance(option1, (commands.core.Command, commands.core.Group)):
|
||||
source = option1.qualified_name
|
||||
destination = str(option2.id)
|
||||
else:
|
||||
source = option2.qualified_name
|
||||
destination = str(option1.id)
|
||||
|
||||
# Now just try to remove it
|
||||
await ctx.bot.db.execute("""
|
||||
DELETE FROM
|
||||
restrictions
|
||||
WHERE
|
||||
source=$1 AND
|
||||
destination=$2 AND
|
||||
from_to=$3 AND
|
||||
guild=$4""", source, destination, arg2, ctx.guild.id)
|
||||
ctx.bot.cache.remove_restriction(ctx.guild, arg2, {"source": source, "destination": destination})
|
||||
|
||||
# If this isn't a blacklist/whitelist, then we are attempting to remove an overwrite
|
||||
else:
|
||||
# Get the source and destination based on whatever order is provided
|
||||
if isinstance(option1, (discord.TextChannel, discord.VoiceChannel)):
|
||||
source = option2
|
||||
destination = option1
|
||||
else:
|
||||
source = option1
|
||||
destination = option2
|
||||
|
||||
# See if it's the blacklist that we're removing from
|
||||
if arg2 == "from":
|
||||
# Get overwrites if they exist
|
||||
# If it doesn't, there's nothing to do here
|
||||
ov = discord.utils.find(lambda t: t[0] == source, destination.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=True)
|
||||
await destination.set_permissions(source, overwrite=ov)
|
||||
else:
|
||||
ov = discord.utils.find(lambda t: t[0] == source, destination.overwrites)
|
||||
ov2 = discord.utils.find(lambda t: t[0] == ctx.message.guild.default_role, destination.overwrites)
|
||||
if ov:
|
||||
ov = ov[1]
|
||||
ov.update(read_messages=None)
|
||||
await destination.set_permissions(source, overwrite=ov)
|
||||
if ov2:
|
||||
ov2 = ov2[1]
|
||||
ov2.update(read_messages=True)
|
||||
await destination.set_permissions(source, overwrite=ov2)
|
||||
|
||||
await ctx.send("I have just unrestricted {} {} {}".format(arg1, arg2, arg3))
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def perms(self, ctx, *, command: str):
|
||||
"""This command can be used to print the current allowed permissions on a specific command
|
||||
This supports groups as well as subcommands; pass no argument to print a list of available permissions
|
||||
|
||||
EXAMPLE: !perms help
|
||||
RESULT: Hopefully a result saying you just need send_messages permissions; otherwise lol
|
||||
this server's admin doesn't like me """
|
||||
cmd = ctx.bot.get_command(command)
|
||||
|
||||
if cmd is None:
|
||||
# If a command wasn't provided, see if a user was
|
||||
converter = commands.converter.MemberConverter()
|
||||
try:
|
||||
member = await converter.convert(ctx, command)
|
||||
# If we failed to convert, just mention that an invalid command was provided
|
||||
except commands.converter.BadArgument:
|
||||
await ctx.send("That is not a valid command!")
|
||||
return
|
||||
else:
|
||||
# Otherwise iterate through the permissions and their values, only including ones that are on
|
||||
perms = [p for p, value in member.guild_permissions if value]
|
||||
# Create an embed with their colour
|
||||
embed = discord.Embed(colour=member.colour)
|
||||
# Set the author to this user
|
||||
embed.set_author(name=str(member), icon_url=member.avatar_url)
|
||||
# Then add their permissions in one field
|
||||
embed.add_field(name="Allowed permissions", value="\n".join(perms))
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
result = await ctx.bot.db.fetchrow(
|
||||
"SELECT permission FROM custom_permissions WHERE guild = $1 AND command = $2",
|
||||
ctx.guild.id,
|
||||
command
|
||||
)
|
||||
perms_value = result["permission"] if result else None
|
||||
|
||||
if perms_value is None:
|
||||
# If we don't find custom permissions, get the required permission for a command
|
||||
# based on what we set in utils.can_run, if can_run isn't found, we'll get an IndexError
|
||||
try:
|
||||
can_run = [func for func in cmd.checks if "can_run" in func.__qualname__][0]
|
||||
except IndexError:
|
||||
# Loop through and check if there is a check called is_owner
|
||||
# If we loop through and don't find one, this means that the only other choice is to be
|
||||
# Able to manage the server (for the utils on perm commands)
|
||||
for func in cmd.checks:
|
||||
if "is_owner" in func.__qualname__:
|
||||
await ctx.send("You need to own the bot to run this command")
|
||||
return
|
||||
await ctx.send("You are required to have `manage_guild` permissions to run `{}`".format(
|
||||
cmd.qualified_name
|
||||
))
|
||||
return
|
||||
|
||||
# Perms will be an attribute if can_run is found no matter what, so no need to check this
|
||||
perms = "\n".join(attribute for attribute, setting in can_run.perms.items() if setting)
|
||||
await ctx.send(
|
||||
"You are required to have `{}` permissions to run `{}`".format(perms, cmd.qualified_name))
|
||||
else:
|
||||
# Permissions are saved as bit values, so create an object based on that value
|
||||
# Then check which permission is true, that is our required permission
|
||||
# There's no need to check for errors here, as we ensure a permission is valid when adding it
|
||||
permissions = discord.Permissions(perms_value)
|
||||
needed_perm = [perm[0] for perm in permissions if perm[1]][0]
|
||||
await ctx.send("You need to have the permission `{}` "
|
||||
"to use the command `{}` in this server".format(needed_perm, command))
|
||||
|
||||
@perms.command(name="add", aliases=["setup,create"])
|
||||
@commands.guild_only()
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
async def add_perms(self, ctx, *, msg: str):
|
||||
"""Sets up custom permissions on the provided command
|
||||
Format must be 'perms add <command> <permission>'
|
||||
If you want to open the command to everyone, provide 'none' as the permission
|
||||
|
||||
EXAMPLE: !perms add skip ban_members
|
||||
RESULT: No more random people voting to skip a song"""
|
||||
|
||||
# Since subcommands exist, base the last word in the list as the permission, and the rest of it as the command
|
||||
command, _, permission = msg.rpartition(" ")
|
||||
if command == "":
|
||||
await ctx.send("Please provide the permissions you want to setup, the format for this must be in:\n"
|
||||
"`perms add <command> <permission>`")
|
||||
return
|
||||
|
||||
cmd = ctx.bot.get_command(command)
|
||||
|
||||
if cmd is None:
|
||||
await ctx.send(
|
||||
"That command does not exist! You can't have custom permissions on a non-existant command....")
|
||||
return
|
||||
|
||||
# If a user can run a command, they have to have send_messages permissions; so use this as the base
|
||||
if permission.lower() == "none":
|
||||
permission = "send_messages"
|
||||
|
||||
# Convert the string to an int value of the permissions object, based on the required permission
|
||||
# If we hit an attribute error, that means the permission given was not correct
|
||||
perm_obj = discord.Permissions.none()
|
||||
try:
|
||||
setattr(perm_obj, permission, True)
|
||||
except AttributeError:
|
||||
await ctx.send("{} does not appear to be a valid permission! Valid permissions are: ```\n{}```"
|
||||
.format(permission, "\n".join(valid_perms)))
|
||||
return
|
||||
perm_value = perm_obj.value
|
||||
|
||||
# Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons
|
||||
# The other case is if I'm using the default has_permissions case
|
||||
# Which means I do not want to check custom permissions at all
|
||||
# Currently the second case is only on adding and removing permissions, to avoid abuse on these
|
||||
for check in cmd.checks:
|
||||
if "is_owner" == check.__name__ or "has_permissions" in str(check):
|
||||
await ctx.send("This command cannot have custom permissions setup!")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO custom_permissions (guild, command, permission) VALUES ($1, $2, $3)",
|
||||
ctx.guild.id,
|
||||
cmd.qualified_name,
|
||||
perm_value
|
||||
)
|
||||
|
||||
ctx.bot.cache.update_custom_permission(ctx.guild, cmd.qualified_name, perm_value)
|
||||
|
||||
await ctx.send("I have just added your custom permissions; "
|
||||
"you now need to have `{}` permissions to use the command `{}`".format(permission, command))
|
||||
|
||||
@perms.command(name="remove", aliases=["delete"])
|
||||
@commands.guild_only()
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
async def remove_perms(self, ctx, *, command: str):
|
||||
"""Removes the custom permissions setup on the command specified
|
||||
|
||||
EXAMPLE: !perms remove play
|
||||
RESULT: Freedom!"""
|
||||
|
||||
cmd = ctx.bot.get_command(command)
|
||||
|
||||
if cmd is None:
|
||||
await ctx.send(
|
||||
"That command does not exist! You can't have custom permissions on a non-existant command....")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute(
|
||||
"DELETE FROM custom_permissions WHERE guild=$1 AND command=$2", ctx.guild.id, cmd.qualified_name
|
||||
)
|
||||
|
||||
ctx.bot.cache.update_custom_permission(ctx.guild, cmd.qualified_name, None)
|
||||
|
||||
await ctx.send("I have just removed the custom permissions for {}!".format(cmd))
|
||||
|
||||
@commands.command(aliases=['nick'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def nickname(self, ctx, *, name=None):
|
||||
"""Used to set the nickname for Bonfire (provide no nickname and it will reset)
|
||||
|
||||
EXAMPLE: !nick Music Bot
|
||||
RESULT: My nickname is now Music Bot"""
|
||||
try:
|
||||
await ctx.message.guild.me.edit(nick=name)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Sorry but I can't change my nickname to {}".format(name))
|
||||
else:
|
||||
await ctx.send("\N{OK HAND SIGN}")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Admin())
|
226
cogs/birthday.py
Normal file
|
@ -0,0 +1,226 @@
|
|||
import discord
|
||||
import datetime
|
||||
import asyncio
|
||||
import traceback
|
||||
import re
|
||||
import calendar
|
||||
|
||||
from discord.ext import commands
|
||||
from asyncpg import UniqueViolationError
|
||||
import utils
|
||||
|
||||
|
||||
def parse_string(date):
|
||||
today = datetime.date.today()
|
||||
month = None
|
||||
day = None
|
||||
month_map = {
|
||||
"january": 1,
|
||||
"jan": 1,
|
||||
"february": 2,
|
||||
"feb": 2,
|
||||
"march": 3,
|
||||
"mar": 3,
|
||||
"april": 4,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"june": 6,
|
||||
"jun": 6,
|
||||
"july": 7,
|
||||
"jul": 7,
|
||||
"august": 8,
|
||||
"aug": 8,
|
||||
"september": 9,
|
||||
"sep": 9,
|
||||
"october": 10,
|
||||
"oct": 10,
|
||||
"november": 11,
|
||||
"nov": 11,
|
||||
"december": 12,
|
||||
"dec": 12,
|
||||
}
|
||||
|
||||
num_re = re.compile("^(\d+)[a-z]*$")
|
||||
|
||||
for part in [x.lower() for x in date.split()]:
|
||||
match = num_re.match(part)
|
||||
if match:
|
||||
day = int(match.group(1))
|
||||
elif part in month_map:
|
||||
month = month_map.get(part)
|
||||
if month and day:
|
||||
year = today.year
|
||||
if month < today.month:
|
||||
year += 1
|
||||
elif month == today.month and day <= today.day:
|
||||
year += 1
|
||||
return datetime.date(year, month, day)
|
||||
|
||||
|
||||
class Birthday(commands.Cog):
|
||||
"""Track and announce birthdays"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.task = self.bot.loop.create_task(self.birthday_task())
|
||||
|
||||
async def get_birthdays_for_server(self, server, today=False):
|
||||
members = ", ".join(f"{m.id}" for m in server.members)
|
||||
query = f"""
|
||||
SELECT
|
||||
id, birthday
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id IN ({members})
|
||||
"""
|
||||
if today:
|
||||
query += """
|
||||
AND
|
||||
birthday = CURRENT_DATE
|
||||
"""
|
||||
query += """
|
||||
ORDER BY
|
||||
birthday
|
||||
"""
|
||||
|
||||
return await self.bot.db.fetch(query)
|
||||
|
||||
async def birthday_task(self):
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
while not self.bot.is_closed():
|
||||
try:
|
||||
await self.notify_birthdays()
|
||||
except Exception as error:
|
||||
with open("error_log", 'a') as f:
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print(f"{error.__class__.__name__}: {error}", file=f)
|
||||
finally:
|
||||
# Every day
|
||||
await asyncio.sleep(60 * 60 * 24)
|
||||
|
||||
async def notify_birthdays(self):
|
||||
query = """
|
||||
SELECT
|
||||
id, COALESCE(birthday_alerts, default_alerts) AS channel
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
birthday_notifications=True
|
||||
AND
|
||||
COALESCE(birthday_alerts, default_alerts) IS NOT NULL
|
||||
"""
|
||||
servers = await self.bot.db.fetch(query)
|
||||
update_bds = []
|
||||
if not servers:
|
||||
return
|
||||
|
||||
for s in servers:
|
||||
# Get the channel based on the birthday alerts, or default alerts channel
|
||||
channel = self.bot.get_channel(s['channel'])
|
||||
if not channel:
|
||||
continue
|
||||
|
||||
bds = await self.get_birthdays_for_server(channel.guild, today=True)
|
||||
|
||||
# A list of the id's that will get updated
|
||||
for bd in bds:
|
||||
try:
|
||||
member = channel.guild.get_member(bd["id"])
|
||||
await channel.send(f"It is {member.mention}'s birthday today! "
|
||||
"Wish them a happy birthday! \N{SHORTCAKE}")
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
finally:
|
||||
update_bds.append(bd['id'])
|
||||
|
||||
if not update_bds:
|
||||
return
|
||||
|
||||
query = f"""
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
birthday = birthday + interval '1 year'
|
||||
WHERE
|
||||
id IN ({", ".join(f"'{bd}'" for bd in update_bds)})
|
||||
"""
|
||||
await self.bot.db.execute(query)
|
||||
|
||||
@commands.group(aliases=['birthdays'], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def birthday(self, ctx, *, member: discord.Member = None):
|
||||
"""A command used to view the birthdays on this server; or a specific member's birthday
|
||||
|
||||
EXAMPLE: !birthdays
|
||||
RESULT: A printout of the birthdays from everyone on this server"""
|
||||
if member:
|
||||
date = await ctx.bot.db.fetchrow("SELECT birthday FROM users WHERE id=$1", member.id)
|
||||
if date is None or date["birthday"] is None:
|
||||
await ctx.send(f"I do not have {member.display_name}'s birthday saved!")
|
||||
else:
|
||||
date = date['birthday']
|
||||
await ctx.send(f"{member.display_name}'s birthday is {calendar.month_name[date.month]} {date.day}")
|
||||
else:
|
||||
# Get this server's birthdays
|
||||
bds = await self.get_birthdays_for_server(ctx.guild)
|
||||
# Create entries based on the user's display name and their birthday
|
||||
entries = [
|
||||
f"{ctx.guild.get_member(bd['id']).display_name} ({bd['birthday'].strftime('%B %-d')})"
|
||||
for bd in bds
|
||||
if bd['birthday']
|
||||
]
|
||||
if not entries:
|
||||
await ctx.send("I don't know anyone's birthday in this server!")
|
||||
return
|
||||
|
||||
# Create our pages object
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=entries, per_page=5)
|
||||
pages.title = f"Birthdays for {ctx.guild.name}"
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
|
||||
@birthday.command(name='add')
|
||||
@utils.can_run(send_messages=True)
|
||||
async def _add_bday(self, ctx, *, date):
|
||||
"""Used to link your birthday to your account
|
||||
|
||||
EXAMPLE: !birthday add December 1st
|
||||
RESULT: I now know your birthday is December 1st"""
|
||||
if len(date.split()) != 2:
|
||||
await ctx.send("Please provide date in a valid format, such as December 1st!")
|
||||
return
|
||||
|
||||
try:
|
||||
date = parse_string(date)
|
||||
except ValueError:
|
||||
await ctx.send("Please provide date in a valid format, such as December 1st!")
|
||||
return
|
||||
|
||||
if date is None:
|
||||
await ctx.send("Please provide date in a valid format, such as December 1st!")
|
||||
return
|
||||
|
||||
await ctx.send(f"I have just saved your birthday as {date}")
|
||||
try:
|
||||
await ctx.bot.db.execute("INSERT INTO users (id, birthday) VALUES ($1, $2)", ctx.author.id, date)
|
||||
except UniqueViolationError:
|
||||
await ctx.bot.db.execute("UPDATE users SET birthday = $1 WHERE id = $2", date, ctx.author.id)
|
||||
|
||||
@birthday.command(name='remove')
|
||||
@utils.can_run(send_messages=True)
|
||||
async def _remove_bday(self, ctx):
|
||||
"""Used to unlink your birthday to your account
|
||||
|
||||
EXAMPLE: !birthday remove
|
||||
RESULT: I have magically forgotten your birthday"""
|
||||
await ctx.send("I don't know your birthday anymore :(")
|
||||
await ctx.bot.db.execute("UPDATE users SET birthday=NULL WHERE id=$1", ctx.author.id)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Birthday(bot))
|
|
@ -1,31 +1,19 @@
|
|||
from . import utils
|
||||
import utils
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
|
||||
face_map = {
|
||||
'S': 'spades',
|
||||
'D': 'diamonds',
|
||||
'C': 'clubs',
|
||||
'H': 'hearts'
|
||||
}
|
||||
|
||||
card_map = {
|
||||
'A': 'Ace',
|
||||
'K': 'King',
|
||||
'Q': 'Queen',
|
||||
'J': 'Jack'
|
||||
}
|
||||
class Blackjack(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
|
||||
|
||||
class Blackjack:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.games = {}
|
||||
|
||||
def __unload(self):
|
||||
def cog_unload(self):
|
||||
# Simply cancel every task
|
||||
for game in self.games.values():
|
||||
game.task.cancel()
|
||||
|
@ -34,17 +22,18 @@ class Blackjack:
|
|||
# When we're done with the game, we need to delete the game itself and remove it's instance from games
|
||||
# To do this, it needs to be able to access this instance of Blackjack
|
||||
game = Game(self.bot, message, self)
|
||||
self.games[message.server.id] = game
|
||||
self.games[message.guild.id] = game
|
||||
|
||||
@commands.group(pass_context=True, no_pm=True, aliases=['bj'], invoke_without_command=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.group(aliases=['bj'], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def blackjack(self, ctx):
|
||||
"""Creates a game/joins the current running game of blackjack
|
||||
|
||||
EXAMPLE: !blackjack
|
||||
RESULT: A new game of blackjack!"""
|
||||
# Get this server's game if it exists
|
||||
game = self.games.get(ctx.message.server.id)
|
||||
# Get this guild's game if it exists
|
||||
game = self.games.get(ctx.message.guild.id)
|
||||
# If it doesn't, start one
|
||||
if game is None:
|
||||
self.create_game(ctx.message)
|
||||
|
@ -54,53 +43,55 @@ class Blackjack:
|
|||
# If it worked, they're ready to play
|
||||
if status:
|
||||
fmt = "{} has joined the game of blackjack, and will be able to play next round!"
|
||||
await self.bot.say(fmt.format(ctx.message.author.display_name))
|
||||
await ctx.send(fmt.format(ctx.message.author.display_name))
|
||||
else:
|
||||
# Otherwise, lets check *why* they couldn't join
|
||||
if game.playing(ctx.message.author):
|
||||
await self.bot.say("You are already playing! Wait for your turn!")
|
||||
await ctx.send("You are already playing! Wait for your turn!")
|
||||
else:
|
||||
await self.bot.say("There are already a max number of players playing/waiting to play!")
|
||||
await ctx.send("There are already a max number of players playing/waiting to play!")
|
||||
|
||||
@blackjack.command(pass_context=True, no_pm=True, name='leave', aliases=['quit'])
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@blackjack.command(name='leave', aliases=['quit'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def blackjack_leave(self, ctx):
|
||||
"""Leaves the current game of blackjack
|
||||
|
||||
EXAMPLE: !blackjack leave
|
||||
RESULT: You stop losing money in blackjack"""
|
||||
|
||||
# Get this server's game if it exists
|
||||
game = self.games.get(ctx.message.server.id)
|
||||
# Get this guild's game if it exists
|
||||
game = self.games.get(ctx.message.guild.id)
|
||||
|
||||
if game is None:
|
||||
await self.bot.say("There are currently no games of Blackjack running!")
|
||||
await ctx.send("There are currently no games of Blackjack running!")
|
||||
return
|
||||
|
||||
status = game.leave(ctx.message.author)
|
||||
if status:
|
||||
await self.bot.say("You have left the game, and will be removed at the end of this round")
|
||||
await ctx.send("You have left the game, and will be removed at the end of this round")
|
||||
else:
|
||||
await self.bot.say("Either you have already bet, or you are not even playing right now!")
|
||||
await ctx.send("Either you have already bet, or you are not even playing right now!")
|
||||
|
||||
@blackjack.command(pass_context=True, no_pm=True, name='forcestop', aliases=['stop'])
|
||||
@utils.custom_perms(manage_server=True)
|
||||
@blackjack.command(name='forcestop', aliases=['stop'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def blackjack_stop(self, ctx):
|
||||
"""Forces the game to stop, mostly for use if someone has gone afk
|
||||
|
||||
EXAMPLE: !blackjack forcestop
|
||||
RESULT: No more blackjack spam"""
|
||||
|
||||
# Get this server's game if it exists
|
||||
game = self.games.get(ctx.message.server.id)
|
||||
# Get this guild's game if it exists
|
||||
game = self.games.get(ctx.message.guild.id)
|
||||
|
||||
if game is None:
|
||||
await self.bot.say("There are currently no games of Blackjack running!")
|
||||
await ctx.send("There are currently no games of Blackjack running!")
|
||||
return
|
||||
|
||||
game.task.cancel()
|
||||
del self.games[ctx.message.server.id]
|
||||
await self.bot.say("The blackjack game running here has just ended")
|
||||
del self.games[ctx.message.guild.id]
|
||||
await ctx.send("The blackjack game running here has just ended")
|
||||
|
||||
|
||||
def FOIL(a, b):
|
||||
|
@ -146,16 +137,17 @@ class Player:
|
|||
|
||||
for card in self.hand:
|
||||
# Order is suit, face...so we want the second value
|
||||
face = card[1]
|
||||
value = card.value.value
|
||||
face = card.value.name
|
||||
|
||||
if face in ['Q', 'K', 'J']:
|
||||
if face in ['queen', 'king', 'jack']:
|
||||
for index, t in enumerate(total):
|
||||
total[index] += 10
|
||||
elif face == 'A':
|
||||
elif face == 'ace':
|
||||
total = FOIL(total, [1, 11])
|
||||
else:
|
||||
for index, t in enumerate(total):
|
||||
total[index] += int(face)
|
||||
total[index] += int(value)
|
||||
|
||||
# If we have more than one possible total (there is at least one ace) then we do not care about one if it is
|
||||
# over 21
|
||||
|
@ -177,12 +169,7 @@ class Player:
|
|||
def __str__(self):
|
||||
# We only care about our hand, for printing wise
|
||||
fmt = "Hand:\n"
|
||||
fmt += "\n".join(
|
||||
"{} of {}".format(
|
||||
card_map.get(card[1], card[1]),
|
||||
face_map.get(card[0], card[0]))
|
||||
for card in self.hand
|
||||
)
|
||||
fmt += "\n".join(str(card) for card in self.hand)
|
||||
fmt += "\n(Total: {})".format(self.count)
|
||||
return fmt
|
||||
|
||||
|
@ -226,7 +213,7 @@ class Game:
|
|||
async def game_task(self):
|
||||
"""The task to handle the entire game"""
|
||||
while len(self.players) > 0:
|
||||
await self.bot.send_message(self.channel, "A new round has started!!")
|
||||
await self.channel.send("A new round has started!!")
|
||||
|
||||
# First wait for bets
|
||||
await self.bet_task()
|
||||
|
@ -253,25 +240,25 @@ class Game:
|
|||
await self.cleanup()
|
||||
|
||||
# If we reach the end of this loop, that means there are no more players
|
||||
del self.bj.games[self.channel.server.id]
|
||||
del self.bj.games[self.channel.guild.id]
|
||||
|
||||
async def dealer_task(self):
|
||||
"""The task handling the dealer's play after all players have stood"""
|
||||
fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer)
|
||||
msg = await self.bot.send_message(self.channel, fmt)
|
||||
msg = await self.channel.send(fmt)
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
if self.dealer.bust:
|
||||
fmt = msg.content + "\n\nDealer has busted!!"
|
||||
await self.bot.edit_message(msg, fmt)
|
||||
await msg.edit(content=fmt)
|
||||
return
|
||||
for num in self.dealer.count:
|
||||
if num > 16:
|
||||
return
|
||||
self.hit(self.dealer)
|
||||
fmt = "It is the dealer's turn to play\n\n{}".format(self.dealer)
|
||||
msg = await self.bot.edit_message(msg, fmt)
|
||||
await msg.edit(content=fmt)
|
||||
|
||||
async def round_task(self):
|
||||
"""The task handling the round itself, asking each person to hit or stand"""
|
||||
|
@ -280,59 +267,60 @@ class Game:
|
|||
# A differen task will handle if a player hit blackjack to start (so they would not be 'playing')
|
||||
|
||||
# Our check to make sure a valid 'command' was provided
|
||||
check = lambda m: m.content.lower() in ['hit', 'stand', 'double']
|
||||
def check(m):
|
||||
if m.channel == self.channel and m.author == player.member:
|
||||
return m.content.lower() in ['hit', 'stand', 'double']
|
||||
else:
|
||||
return False
|
||||
|
||||
# First lets handle the blackjacks
|
||||
for entry in [p for p in self.players if p['status'] == 'blackjack']:
|
||||
player = entry['player']
|
||||
fmt = "You got a blackjack {0.member.mention}!\n\n{0}".format(player)
|
||||
|
||||
await self.bot.send_message(self.channel, fmt)
|
||||
await self.channel.send(fmt)
|
||||
# Loop through each player (as long as their status is playing) and they have bet chips
|
||||
for entry in [p for p in self.players if p['status'] == 'playing' and hasattr(p['player'], 'bet')]:
|
||||
player = entry['player']
|
||||
|
||||
# Let them know it's their turn to play
|
||||
fmt = "It is your turn to play {0.member.mention}\n\n{0}".format(player)
|
||||
await self.bot.send_message(self.channel, fmt)
|
||||
await self.channel.send(fmt)
|
||||
first = True
|
||||
|
||||
# If they're not playing anymore (i.e. they busted, are standing, etc.) then we don't want to keep asking
|
||||
# them to hit or stand
|
||||
while entry['status'] not in ['stand', 'bust']:
|
||||
|
||||
# Ask if they want to hit or stand
|
||||
fmt = "Hit, stand, or double?"
|
||||
await self.bot.send_message(self.channel, fmt)
|
||||
msg = await self.bot.wait_for_message(timeout=60, author=player.member, channel=self.channel,
|
||||
check=check)
|
||||
if first:
|
||||
fmt = "Hit, stand, or double?"
|
||||
else:
|
||||
fmt = "Hit or stand?"
|
||||
await self.channel.send(fmt)
|
||||
|
||||
# If they took to long, make them stand so the next person can play
|
||||
if msg is None:
|
||||
await self.bot.send_message(self.channel, "Took to long! You're standing!")
|
||||
try:
|
||||
msg = await self.bot.wait_for('message', timeout=60, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
await self.channel.send("Took to long! You're standing!")
|
||||
entry['status'] = 'stand'
|
||||
# If they want to hit
|
||||
elif 'hit' in msg.content.lower():
|
||||
self.hit(player)
|
||||
await self.bot.send_message(self.channel, player)
|
||||
# If they want to stand
|
||||
elif 'stand' in msg.content.lower():
|
||||
self.stand(player)
|
||||
elif 'double' in msg.content.lower():
|
||||
self.double(player)
|
||||
await self.bot.send_message(self.channel, player)
|
||||
# TODO: Handle double, split
|
||||
else:
|
||||
# If they want to hit
|
||||
if 'hit' in msg.content.lower():
|
||||
self.hit(player)
|
||||
await self.channel.send(player)
|
||||
# If they want to stand
|
||||
elif 'stand' in msg.content.lower():
|
||||
self.stand(player)
|
||||
elif 'double' in msg.content.lower() and first:
|
||||
self.double(player)
|
||||
await self.channel.send(player)
|
||||
# TODO: Handle double, split
|
||||
|
||||
first = False
|
||||
|
||||
async def bet_task(self):
|
||||
"""Performs the task of betting"""
|
||||
|
||||
def check(_msg):
|
||||
"""Makes sure the message provided is within the min and max bets"""
|
||||
try:
|
||||
msg_length = int(_msg.content)
|
||||
return self.min_bet <= msg_length <= self.max_bet
|
||||
except ValueError:
|
||||
return _msg.content.lower() == 'skip'
|
||||
|
||||
# There is one situation that we want to allow that means we cannot loop through players like normally would
|
||||
# be the case: Betting has started; while one person is betting, another joins This means our list has
|
||||
# changed, and neither based on the length or looping through the list itself will handle this To handle
|
||||
|
@ -348,28 +336,43 @@ class Game:
|
|||
entry = players[0]
|
||||
player = entry['player']
|
||||
|
||||
def check(_msg):
|
||||
"""Makes sure the message provided is within the min and max bets"""
|
||||
if _msg.channel == self.channel and _msg.author == player.member:
|
||||
try:
|
||||
msg_length = int(_msg.content)
|
||||
return self.min_bet <= msg_length <= self.max_bet
|
||||
except ValueError:
|
||||
return _msg.content.lower() == 'skip'
|
||||
else:
|
||||
return False
|
||||
|
||||
fmt = "Your turn to bet {0.member.mention}, your current chips are: {0.chips}\n" \
|
||||
"Current min bet is {1}, current max bet is {2}\n" \
|
||||
"Place your bet now (please provide only the number;" \
|
||||
"'skip' if you would like to leave this game)".format(player, self.min_bet, self.max_bet)
|
||||
await self.bot.send_message(self.channel, fmt)
|
||||
msg = await self.bot.wait_for_message(timeout=60, author=player.member, channel=self.channel, check=check)
|
||||
|
||||
if msg is None:
|
||||
await self.bot.send_message(self.channel, "You took too long! You're sitting this round out")
|
||||
await self.channel.send(fmt)
|
||||
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", timeout=60, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
await self.channel.send("You took too long! You're sitting this round out")
|
||||
entry['status'] = 'stand'
|
||||
elif msg.content.lower() == 'skip':
|
||||
await self.bot.send_message(self.channel, "Alright, you've been removed from the game")
|
||||
self.leave(player.member)
|
||||
else:
|
||||
num = int(msg.content)
|
||||
# Set the new bet, and remove it from this players chip total
|
||||
if num <= player.chips:
|
||||
player.bet = num
|
||||
player.chips -= num
|
||||
entry['status'] = 'bet'
|
||||
if msg.content.lower() == 'skip':
|
||||
await self.channel.send("Alright, you've been removed from the game")
|
||||
self.leave(player.member)
|
||||
else:
|
||||
await self.bot.send_message(self.channel, "You can't bet more than you have!!")
|
||||
num = int(msg.content)
|
||||
# Set the new bet, and remove it from this players chip total
|
||||
if num <= player.chips:
|
||||
player.bet = num
|
||||
player.chips -= num
|
||||
entry['status'] = 'bet'
|
||||
else:
|
||||
await self.channel.send("You can't bet more than you have!!")
|
||||
|
||||
# Call this so that we can correct the list, if someone has left or join
|
||||
self.player_cleanup()
|
||||
|
||||
|
@ -464,7 +467,7 @@ class Game:
|
|||
# Add that to the main string
|
||||
fmt += "Blackjacks: {}\n".format(_fmt)
|
||||
|
||||
await self.bot.send_message(self.channel, fmt)
|
||||
await self.channel.send(fmt)
|
||||
|
||||
# Do the same for the dealers hand
|
||||
cards = list(self.dealer.hand.draw(self.dealer.hand.count))
|
||||
|
@ -481,10 +484,10 @@ class Game:
|
|||
self.players.extend(self._added_players)
|
||||
self._added_players.clear()
|
||||
|
||||
# What we want to do is remove any players that are in the game and have left the server
|
||||
# What we want to do is remove any players that are in the game and have left the guild
|
||||
for entry in self.players:
|
||||
m = entry['player'].member
|
||||
if m not in self.channel.server.members:
|
||||
if m not in self.channel.guild.members:
|
||||
self._removed_players.append(entry['player'])
|
||||
|
||||
# Remove the players who left
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from .utils import checks
|
||||
from utils import checks
|
||||
|
||||
import random
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Chess:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Our format for games is going to be a little different, because we do want to allow multiple games per server
|
||||
# Format should be {'server_id': [Game, Game, Game]}
|
||||
self.games = {}
|
||||
class Chess(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
|
||||
# Our format for games is going to be a little different, because we do want to allow multiple games per guild
|
||||
# Format should be {'server_id': [Game, Game, Game]}
|
||||
games = {}
|
||||
|
||||
def play(self, player, notation):
|
||||
"""Our task to handle a player making their actual move"""
|
||||
|
@ -91,18 +91,18 @@ class Chess:
|
|||
return MoveStatus.invalid
|
||||
|
||||
def get_game(self, player):
|
||||
"""Provides the game this player is playing, in this server"""
|
||||
server_games = self.games.get(player.server.id, [])
|
||||
for game in server_games:
|
||||
"""Provides the game this player is playing, in this guild"""
|
||||
guild_games = self.games.get(player.guild.id, [])
|
||||
for game in guild_games:
|
||||
if player in game.challengers.values():
|
||||
return game
|
||||
|
||||
return None
|
||||
|
||||
def in_game(self, player):
|
||||
"""Checks to see if a player is playing in a game right now, in this server"""
|
||||
server_games = self.games.get(player.server.id, [])
|
||||
for game in server_games:
|
||||
"""Checks to see if a player is playing in a game right now, in this guild"""
|
||||
guild_games = self.games.get(player.guild.id, [])
|
||||
for game in guild_games:
|
||||
if player in game.challengers.values():
|
||||
return True
|
||||
|
||||
|
@ -111,14 +111,14 @@ class Chess:
|
|||
def start_game(self, player1, player2):
|
||||
game = Game(player1, player2)
|
||||
try:
|
||||
self.games[player1.server.id].append(game)
|
||||
self.games[player1.guild.id].append(game)
|
||||
except KeyError:
|
||||
self.games[player1.server.id] = [game]
|
||||
self.games[player1.guild.id] = [game]
|
||||
|
||||
return game
|
||||
|
||||
@commands.group(pass_contxt=True, invoke_without_command=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@commands.group(invoke_without_command=True)
|
||||
@checks.can_run(send_messages=True)
|
||||
async def chess(self, ctx, *, move):
|
||||
"""Moves a piece based on the notation provided
|
||||
Notation for normal moves are {piece} to {position} based on the algebraic notation of the board (This is on the picture)
|
||||
|
@ -142,47 +142,48 @@ class Chess:
|
|||
EXAMPLE: !rook to d4"""
|
||||
result = self.play(ctx.message.author, move)
|
||||
if result is MoveStatus.invalid:
|
||||
await self.bot.say("That was an invalid move!")
|
||||
await ctx.send("That was an invalid move!")
|
||||
elif result is MoveStatus.wrong_turn:
|
||||
await self.bot.say("It is not your turn to play on your game in this server!")
|
||||
await ctx.send("It is not your turn to play on your game in this guild!")
|
||||
elif result is MoveStatus.no_game:
|
||||
await self.bot.say("You are not currently playing a game on this server!")
|
||||
await ctx.send("You are not currently playing a game on this guild!")
|
||||
elif result is MoveStatus.valid:
|
||||
game = self.get_game(ctx.message.author)
|
||||
link = game.draw_board()
|
||||
await self.bot.upload(link)
|
||||
await ctx.bot.upload(link)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@checks.can_run(send_messages=True)
|
||||
async def chess_start(self, ctx, player2: discord.Member):
|
||||
"""Starts a chess game with another player
|
||||
You can play one game on a single server at a time
|
||||
You can play one game on a single guild at a time
|
||||
|
||||
EXAMPLE: !chess start @Victim
|
||||
RESULT: A new chess game! Good luck~"""
|
||||
|
||||
# Lets first check our permissions; we're not going to create a text based board
|
||||
# So we need to be able to attach files in order to send the board
|
||||
if not ctx.message.channel.permissions_for(ctx.message.server.me).attach_files:
|
||||
await self.bot.say(
|
||||
if not ctx.message.channel.permissions_for(ctx.message.guild.me).attach_files:
|
||||
await ctx.send(
|
||||
"I need to be able to send pictures to provide the board! Please ask someone with mange roles permission, to grant me attach files permission if you want to play this")
|
||||
return
|
||||
|
||||
# Make sure the author and player 2 are not in a game already
|
||||
if self.in_game(ctx.message.author):
|
||||
await self.bot.say("Sorry, but you can only be in one game per server at a time")
|
||||
await ctx.send("Sorry, but you can only be in one game per guild at a time")
|
||||
return
|
||||
|
||||
if self.in_game(player2):
|
||||
await self.bot.say("Sorry, but {} is already in a game on this server!".format(player2.display_name))
|
||||
await ctx.send("Sorry, but {} is already in a game on this guild!".format(player2.display_name))
|
||||
return
|
||||
|
||||
# Start the game
|
||||
game = self.start_game(ctx.message.author, player2)
|
||||
player1 = game.challengers.get('white')
|
||||
await self.bot.say(
|
||||
"{} you have started a chess game with {}\n\n{} will be white this game, and is going first.\nIf you need information about the notation used to play, run {}help chess".format(
|
||||
ctx.message.author.display_name, player2.display_name, ctx.prefix))
|
||||
await ctx.send(
|
||||
"{} you have started a chess game with {}\n\n{} will be white this game, and is going first.\nIf you need "
|
||||
"information about the notation used to play, run {}help chess".format(
|
||||
ctx.message.author.display_name, player2.display_name, player1, ctx.prefix))
|
||||
|
||||
|
||||
class MoveStatus(Enum):
|
||||
|
@ -209,7 +210,7 @@ class Game:
|
|||
self.reset_board()
|
||||
|
||||
# The point of chess revolves around the king's position
|
||||
# Due to this, we're going to use the king's position a lot, so lets save this variable
|
||||
# Due to this, we're going to use the king's position a lot, so lets save this variable
|
||||
self.w_king_pos = (0, 4)
|
||||
self.b_king_pos = (7, 4)
|
||||
|
||||
|
@ -262,7 +263,7 @@ class Game:
|
|||
colour = 'black'
|
||||
king_pos = self.b_king_pos
|
||||
|
||||
if not can_castle[colour][pos]:
|
||||
if not self.can_castle[colour][pos]:
|
||||
return False
|
||||
|
||||
# During castling, the row should never change
|
||||
|
@ -388,7 +389,7 @@ class Game:
|
|||
def check(self):
|
||||
"""Checks our current board, and checks (based on whose turn it is) if we're in a 'check' state"""
|
||||
# To check for a check, what we should do is loop through the board
|
||||
# Then check if it's the the current players turn's piece, and compare to moving to the king's position
|
||||
# Then check if it's the the current players turn's piece, and compare to moving to the king's position
|
||||
for x, row in enumerate(self.board):
|
||||
for y, piece in enumerate(row):
|
||||
if self.white_turn and re.search('B.', piece) and self.valid_move((x, y), self.b_king_pos):
|
||||
|
|
771
cogs/config.py
Normal file
|
@ -0,0 +1,771 @@
|
|||
from discord.ext import commands
|
||||
from asyncpg import UniqueViolationError
|
||||
|
||||
import utils
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class ConfigException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WrongSettingType(ConfigException):
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
|
||||
class MessageFormatError(ConfigException):
|
||||
|
||||
def __init__(self, original, keys):
|
||||
self.original = original
|
||||
self.keys = keys
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
class GuildConfiguration(commands.Cog):
|
||||
"""Handles configuring the different settings that can be used on the bot"""
|
||||
|
||||
keys = {
|
||||
|
||||
}
|
||||
|
||||
def _str_to_bool(self, opt, setting):
|
||||
setting = setting.title()
|
||||
if setting.title() not in ["True", "False"]:
|
||||
raise WrongSettingType(
|
||||
f"The {opt} setting requires either 'True' or 'False', not {setting}"
|
||||
)
|
||||
|
||||
return setting.title() == "True"
|
||||
|
||||
async def _get_channel(self, ctx, setting):
|
||||
converter = commands.converter.TextChannelConverter()
|
||||
return await converter.convert(ctx, setting)
|
||||
|
||||
async def _set_db_guild_opt(self, opt, setting, ctx):
|
||||
if opt == "prefix":
|
||||
ctx.bot.cache.update_prefix(ctx.guild, setting)
|
||||
try:
|
||||
return await ctx.bot.db.execute(f"INSERT INTO guilds (id, \"{opt}\") VALUES ($1, $2)", ctx.guild.id, setting)
|
||||
except UniqueViolationError:
|
||||
return await ctx.bot.db.execute(f"UPDATE guilds SET {opt} = $1 WHERE id = $2", setting, ctx.guild.id)
|
||||
|
||||
async def _show_bool_options(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
return f"`{opt}` are currently {'enabled' if result is not None and result[opt] else 'disabled'}"
|
||||
|
||||
async def _show_channel_options(self, ctx, opt):
|
||||
"""For showing options that rely on a certain channel"""
|
||||
result = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
if result is None:
|
||||
return f"You do not have a channel set for {opt}"
|
||||
channel_id = result[opt]
|
||||
if channel_id:
|
||||
channel = ctx.guild.get_channel(channel_id)
|
||||
if channel:
|
||||
return f"Your {opt} alerts channel is currently set to {channel.mention}"
|
||||
else:
|
||||
return "It looks like you used to have a channel set for this," \
|
||||
"however the channel has since been deleted"
|
||||
else:
|
||||
return f"You do not have a channel set for {opt}"
|
||||
|
||||
# These are handles for each setting type
|
||||
# Just the bool ones, they're all handled the exact same way
|
||||
_handle_show_birthday_notifications = _show_bool_options
|
||||
_handle_show_welcome_notifications = _show_bool_options
|
||||
_handle_show_goodbye_notifications = _show_bool_options
|
||||
_handle_show_colour_roles = _show_bool_options
|
||||
_handle_show_include_default_battles = _show_bool_options
|
||||
_handle_show_include_default_hugs = _show_bool_options
|
||||
# The channel ones
|
||||
_handle_show_default_alerts = _show_channel_options
|
||||
_handle_show_welcome_alerts = _show_channel_options
|
||||
_handle_show_goodbye_alerts = _show_channel_options
|
||||
_handle_show_picarto_alerts = _show_channel_options
|
||||
_handle_show_birthday_alerts = _show_channel_options
|
||||
_handle_show_raffle_alerts = _show_channel_options
|
||||
|
||||
async def _handle_show_welcome_msg(self, ctx, setting):
|
||||
result = await ctx.bot.db.fetchrow("SELECT welcome_msg FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
try:
|
||||
msg = result["welcome_msg"].format(server=ctx.guild.name, member=ctx.author.mention)
|
||||
return f"Your current welcome message will appear like this:\n\n"
|
||||
except (AttributeError, TypeError):
|
||||
return "You currently have no welcome message setup"
|
||||
|
||||
async def _handle_show_goodbye_msg(self, ctx, setting):
|
||||
result = await ctx.bot.db.fetchrow("SELECT goodbye_msg FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
try:
|
||||
msg = result["goodbye_msg"].format(server=ctx.guild.name, member=ctx.author.mention)
|
||||
return f"Your current goodbye message will appear like this:\n\n"
|
||||
except (AttributeError, TypeError):
|
||||
return "You currently have no goodbye message setup"
|
||||
|
||||
async def _handle_show_prefix(self, ctx, setting):
|
||||
result = await ctx.bot.db.fetchrow("SELECT prefix FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result is not None:
|
||||
prefix = result["prefix"]
|
||||
if prefix is not None:
|
||||
return f"Your current prefix is `{prefix}`"
|
||||
|
||||
return "You do not have a custom prefix set, you are using the default prefix"
|
||||
|
||||
async def _handle_show_followed_picarto_channels(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT followed_picarto_channels FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["followed_picarto_channels"]:
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=result["followed_picarto_channels"])
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server is not following any picarto channels"
|
||||
|
||||
async def _handle_show_ignored_channels(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT ignored_channels FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["ignored_channels"]:
|
||||
try:
|
||||
entries = [
|
||||
ctx.guild.get_channel(ch).mention
|
||||
for ch in result["ignored_members"]
|
||||
if ctx.guild.get_channel(ch) is not None
|
||||
]
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server is not ignoring any channels"
|
||||
|
||||
async def _handle_show_ignored_members(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT ignored_members FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["ignored_members"]:
|
||||
try:
|
||||
entries = [
|
||||
ctx.guild.get_member(m).display_name
|
||||
for m in result["ignored_members"]
|
||||
if ctx.guild.get_member(m) is not None
|
||||
]
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server is not ignoring any members"
|
||||
|
||||
async def _handle_show_rules(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT rules FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["rules"]:
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=result["rules"])
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server has no rules"
|
||||
|
||||
async def _handle_show_assignable_roles(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["assignable_roles"]:
|
||||
try:
|
||||
entries = [
|
||||
ctx.guild.get_role(r).name
|
||||
for r in result["assignable_roles"]
|
||||
if ctx.guild.get_role(r) is not None
|
||||
]
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server has no assignable roles"
|
||||
|
||||
async def _handle_show_custom_battles(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT custom_battles FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["custom_battles"]:
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=result["custom_battles"])
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server has no custom battles"
|
||||
|
||||
async def _handle_show_custom_hugs(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT custom_hugs FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result["custom_hugs"]:
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=result["custom_hugs"])
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
return "This server has no custom hugs"
|
||||
|
||||
async def _handle_show_join_role(self, ctx, opt):
|
||||
result = await ctx.bot.db.fetchrow("SELECT join_role FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result and result['join_role']:
|
||||
role = ctx.guild.get_role(result['join_role'])
|
||||
if role is None:
|
||||
return "You had a role set, but I can't find it...it's most likely been deleted afterwords!"
|
||||
else:
|
||||
return f"When people join I will give them the role {role.name}"
|
||||
else:
|
||||
return "You have no join_role setting for when people join this server"
|
||||
|
||||
async def _handle_set_birthday_notifications(self, ctx, setting):
|
||||
opt = "birthday_notifications"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_welcome_notifications(self, ctx, setting):
|
||||
opt = "welcome_notifications"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_goodbye_notifications(self, ctx, setting):
|
||||
opt = "goodbye_notifications"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_colour_roles(self, ctx, setting):
|
||||
opt = "colour_roles"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_include_default_battles(self, ctx, setting):
|
||||
opt = "include_default_battles"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_include_default_hugs(self, ctx, setting):
|
||||
opt = "include_default_hugs"
|
||||
setting = self._str_to_bool(opt, setting)
|
||||
return await self._set_db_guild_opt(opt, setting, ctx)
|
||||
|
||||
async def _handle_set_welcome_msg(self, ctx, setting):
|
||||
try:
|
||||
setting.format(member='test', server='test')
|
||||
except KeyError as e:
|
||||
raise MessageFormatError(e, ["member", "server"])
|
||||
else:
|
||||
return await self._set_db_guild_opt("welcome_msg", setting, ctx)
|
||||
|
||||
async def _handle_set_goodbye_msg(self, ctx, setting):
|
||||
try:
|
||||
setting.format(member='test', server='test')
|
||||
except KeyError as e:
|
||||
raise MessageFormatError(e, ["member", "server"])
|
||||
else:
|
||||
return await self._set_db_guild_opt("goodbye_msg", setting, ctx)
|
||||
|
||||
async def _handle_set_prefix(self, ctx, setting):
|
||||
if len(setting) > 20:
|
||||
raise WrongSettingType("Please keep the prefix under 20 characters")
|
||||
if setting.lower().strip() == "none":
|
||||
setting = None
|
||||
|
||||
return await self._set_db_guild_opt("prefix", setting, ctx)
|
||||
|
||||
async def _handle_set_default_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("default_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_welcome_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("welcome_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_goodbye_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("goodbye_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_picarto_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("picarto_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_birthday_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("birthday_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_raffle_alerts(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
return await self._set_db_guild_opt("raffle_alerts", channel.id, ctx)
|
||||
|
||||
async def _handle_set_followed_picarto_channels(self, ctx, setting):
|
||||
user = await utils.request(f"http://api.picarto.tv/v1/channel/name/{setting}")
|
||||
if user is None:
|
||||
raise WrongSettingType(f"Could not find a picarto user with the username {setting}")
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
followed_picarto_channels = array_append(followed_picarto_channels, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(followed_picarto_channels);
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_set_ignored_channels(self, ctx, setting):
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
ignored_channels = array_append(ignored_channels, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(ignored_channels);
|
||||
"""
|
||||
ctx.bot.loop.create_task(ctx.bot.cache.load_ignored())
|
||||
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
|
||||
|
||||
async def _handle_set_ignored_members(self, ctx, setting):
|
||||
# We want to make it possible to have members that aren't in the server ignored
|
||||
# So first check if it's a digit (the id)
|
||||
if not setting.isdigit():
|
||||
converter = commands.converter.MemberConverter()
|
||||
member = await converter.convert(ctx, setting)
|
||||
setting = member.id
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
ignored_members = array_append(ignored_members, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(ignored_members);
|
||||
"""
|
||||
ctx.bot.loop.create_task(ctx.bot.cache.load_ignored())
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_set_rules(self, ctx, setting):
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
rules = array_append(rules, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(rules);
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_set_assignable_roles(self, ctx, setting):
|
||||
converter = commands.converter.RoleConverter()
|
||||
role = await converter.convert(ctx, setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
assignable_roles = array_append(assignable_roles, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(assignable_roles);
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, role.id, ctx.guild.id)
|
||||
|
||||
async def _handle_set_custom_battles(self, ctx, setting):
|
||||
try:
|
||||
setting.format(loser="player1", winner="player2")
|
||||
except (KeyError, ValueError) as e:
|
||||
raise MessageFormatError(e, ["loser", "winner"])
|
||||
else:
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
custom_battles = array_append(custom_battles, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(custom_battles);
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_set_custom_hugs(self, ctx, setting):
|
||||
try:
|
||||
setting.format(user="user")
|
||||
except (KeyError, ValueError)as e:
|
||||
raise MessageFormatError(e, ["user"])
|
||||
else:
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
custom_hugs = array_append(custom_hugs, $1)
|
||||
WHERE
|
||||
id=$2 AND
|
||||
NOT $1 = ANY(custom_hugs);
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_set_join_role(self, ctx, setting):
|
||||
converter = commands.converter.RoleConverter()
|
||||
role = await converter.convert(ctx, setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
join_role = $1
|
||||
where
|
||||
ID=$2
|
||||
"""
|
||||
|
||||
return await ctx.bot.db.execute(query, role.id, ctx.guild.id)
|
||||
|
||||
async def _handle_remove_birthday_notifications(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("birthday_notifications", False, ctx)
|
||||
|
||||
async def _handle_remove_welcome_notifications(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("welcome_notifications", False, ctx)
|
||||
|
||||
async def _handle_remove_goodbye_notifications(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("goodbye_notifications", False, ctx)
|
||||
|
||||
async def _handle_remove_colour_roles(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("colour_roles", False, ctx)
|
||||
|
||||
async def _handle_remove_include_default_battles(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("include_default_battles", False, ctx)
|
||||
|
||||
async def _handle_remove_include_default_hugs(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("include_default_hugs", False, ctx)
|
||||
|
||||
async def _handle_remove_welcome_msg(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("welcome_msg", None, ctx)
|
||||
|
||||
async def _handle_remove_goodbye_msg(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("goodbye_msg", None, ctx)
|
||||
|
||||
async def _handle_remove_prefix(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("prefix", None, ctx)
|
||||
|
||||
async def _handle_remove_default_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("default_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_welcome_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("welcome_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_goodbye_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("goodbye_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_picarto_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("picarto_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_birthday_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("birthday_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_raffle_alerts(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("raffle_alerts", None, ctx)
|
||||
|
||||
async def _handle_remove_join_role(self, ctx, setting=None):
|
||||
return await self._set_db_guild_opt("join_role", None, ctx)
|
||||
|
||||
async def _handle_remove_followed_picarto_channels(self, ctx, setting=None):
|
||||
if setting is None:
|
||||
raise WrongSettingType("Specifying which channel you want to remove is required")
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
followed_picarto_channels = array_remove(followed_picarto_channels, $1)
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_remove_ignored_channels(self, ctx, setting=None):
|
||||
if setting is None:
|
||||
raise WrongSettingType("Specifying which channel you want to remove is required")
|
||||
|
||||
channel = await self._get_channel(ctx, setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
ignored_channels = array_remove(ignored_channels, $1)
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
|
||||
|
||||
async def _handle_remove_ignored_members(self, ctx, setting=None):
|
||||
if setting is None:
|
||||
raise WrongSettingType("Specifying which channel you want to remove is required")
|
||||
# We want to make it possible to have members that aren't in the server ignored
|
||||
# So first check if it's a digit (the id)
|
||||
if not setting.isdigit():
|
||||
converter = commands.converter.MemberConverter()
|
||||
member = await converter.convert(ctx, setting)
|
||||
setting = member.id
|
||||
else:
|
||||
setting = int(setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
ignored_members = array_remove(ignored_members, $1)
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_remove_rules(self, ctx, setting=None):
|
||||
if setting is None or not setting.isdigit():
|
||||
raise WrongSettingType("Please provide the number of the rule you want to remove")
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
rules = array_remove(rules, rules[$1])
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
|
||||
|
||||
async def _handle_remove_assignable_roles(self, ctx, setting=None):
|
||||
if setting is None:
|
||||
raise WrongSettingType("Specifying which channel you want to remove is required")
|
||||
if not setting.isdigit():
|
||||
converter = commands.converter.RoleConverter()
|
||||
role = await converter.convert(ctx, setting)
|
||||
setting = role.id
|
||||
else:
|
||||
setting = int(setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
assignable_roles = array_remove(assignable_roles, $1)
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
|
||||
|
||||
async def _handle_remove_custom_battles(self, ctx, setting=None):
|
||||
if setting is None or not setting.isdigit():
|
||||
raise WrongSettingType("Please provide the number of the custom battle you want to remove")
|
||||
else:
|
||||
setting = int(setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
custom_battles = array_remove(custom_battles, custom_battles[$1])
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
|
||||
|
||||
async def _handle_remove_custom_hugs(self, ctx, setting=None):
|
||||
if setting is None or not setting.isdigit():
|
||||
raise WrongSettingType("Please provide the number of the custom hug you want to remove")
|
||||
else:
|
||||
setting = int(setting)
|
||||
|
||||
query = """
|
||||
UPDATE
|
||||
guilds
|
||||
SET
|
||||
custom_hugs = array_remove(custom_hugs, custom_hugs[$1])
|
||||
WHERE
|
||||
id=$2
|
||||
"""
|
||||
return await ctx.bot.db.execute(query, int(setting), ctx.guild.id)
|
||||
|
||||
async def cog_after_invoke(self, ctx):
|
||||
"""Here we will facilitate cleaning up settings, will remove channels/roles that no longer exist, etc."""
|
||||
pass
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def config(self, ctx, *, opt=None):
|
||||
"""Handles the configuration of the bot for this server"""
|
||||
if opt:
|
||||
try:
|
||||
coro = getattr(self, f"_handle_show_{opt}")
|
||||
except AttributeError:
|
||||
await ctx.send(f"{opt} is not a valid config option. Use {ctx.prefix}config to list all config options")
|
||||
else:
|
||||
try:
|
||||
msg = await coro(ctx, opt)
|
||||
except WrongSettingType as exc:
|
||||
await ctx.send(exc.message)
|
||||
except commands.BadArgument:
|
||||
pass
|
||||
else:
|
||||
return await ctx.send(msg)
|
||||
|
||||
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
|
||||
|
||||
# For convenience, if it's None, just create it and return the default values
|
||||
if settings is None:
|
||||
await ctx.bot.db.execute("INSERT INTO guilds (id) VALUES ($1)", ctx.guild.id)
|
||||
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
|
||||
|
||||
alerts = {}
|
||||
# This is dirty I know, but oh well...
|
||||
for alert_type in ["default", "welcome", "goodbye", "picarto", "birthday", "raffle"]:
|
||||
channel = ctx.guild.get_channel(settings.get(f"{alert_type}_alerts"))
|
||||
name = channel.name if channel else None
|
||||
alerts[alert_type] = name
|
||||
|
||||
fmt = f"""
|
||||
**Notification Settings**
|
||||
birthday_notifications
|
||||
*Notify on the birthday that users in this guild have saved*
|
||||
**{settings.get("birthday_notifications")}**
|
||||
|
||||
welcome_notifications
|
||||
*Notify when someone has joined this guild*
|
||||
**{settings.get("welcome_notifications")}**
|
||||
|
||||
goodbye_notifications
|
||||
*Notify when someone has left this guild
|
||||
**{settings.get("goodbye_notifications")}**
|
||||
|
||||
welcome_msg
|
||||
*A message that can be customized and used when someone joins the server*
|
||||
**{"Set" if settings.get("welcome_msg") is not None else "Not set"}**
|
||||
|
||||
goodbye_msg
|
||||
*A message that can be customized and used when someone leaves the server*
|
||||
**{"Set" if settings.get("goodbye_msg") is not None else "Not set"}**
|
||||
|
||||
**Alert Channels**
|
||||
default_alerts
|
||||
*The channel to default alert messages to*
|
||||
**{alerts.get("default_alerts")}**
|
||||
|
||||
welcome_alerts
|
||||
*The channel to send welcome alerts to (when someone joins the server)*
|
||||
**{alerts.get("welcome_alerts")}**
|
||||
|
||||
goodbye_alerts
|
||||
*The channel to send goodbye alerts to (when someone leaves the server)*
|
||||
**{alerts.get("goodbye_alerts")}**
|
||||
|
||||
picarto_alerts
|
||||
*The channel to send Picarto alerts to (when a channel the server follows goes on/offline)*
|
||||
**{alerts.get("picarto_alerts")}**
|
||||
|
||||
birthday_alerts
|
||||
*The channel to send birthday alerts to (on the day of someone's birthday)*
|
||||
**{alerts.get("birthday_alerts")}**
|
||||
|
||||
raffle_alerts
|
||||
*The channel to send alerts for server raffles to*
|
||||
**{alerts.get("raffle_alerts")}**
|
||||
|
||||
|
||||
**Misc Settings**
|
||||
followed_picarto_channels
|
||||
*Channels for the bot to "follow" and notify this server when they go live*
|
||||
**{len(settings.get("followed_picarto_channels"))}**
|
||||
|
||||
ignored_channels
|
||||
*Channels that the bot ignores*
|
||||
**{len(settings.get("ignored_channels"))}**
|
||||
|
||||
ignored_members
|
||||
*Members that the bot ignores*
|
||||
**{len(settings.get("ignored_members"))}**
|
||||
|
||||
rules
|
||||
*Rules for this server*
|
||||
**{len(settings.get("rules"))}**
|
||||
|
||||
assignable_roles
|
||||
*Roles that can be self-assigned by users*
|
||||
**{len(settings.get("assignable_roles"))}**
|
||||
|
||||
custom_battles
|
||||
*Possible outcomes to battles that can be received on this server*
|
||||
**{len(settings.get("custom_battles"))}**
|
||||
|
||||
custom_hugs
|
||||
*Possible outcomes to hugs that can be received on this server*
|
||||
**{len(settings.get("custom_hugs"))}**
|
||||
""".strip()
|
||||
|
||||
embed = discord.Embed(title=f"Configuration for {ctx.guild.name}", description=fmt)
|
||||
embed.set_image(url=ctx.guild.icon_url)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@config.command(name="set", aliases=["add"])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def _set_setting(self, ctx, option, *, setting):
|
||||
"""Sets one of the configuration settings for this server"""
|
||||
try:
|
||||
coro = getattr(self, f"_handle_set_{option}")
|
||||
except AttributeError:
|
||||
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
|
||||
else:
|
||||
# First make sure there's an entry for this guild before doing anything
|
||||
try:
|
||||
await ctx.bot.db.execute("INSERT INTO guilds(id) VALUES ($1)", ctx.guild.id)
|
||||
except UniqueViolationError:
|
||||
pass
|
||||
|
||||
try:
|
||||
await coro(ctx, setting=setting)
|
||||
except WrongSettingType as exc:
|
||||
await ctx.send(exc.message)
|
||||
except MessageFormatError as exc:
|
||||
fmt = f"""
|
||||
Failed to parse the format string provided, possible keys are: {', '.join(k for k in exc.keys)}
|
||||
Extraneous args provided: {', '.join(k for k in exc.original.args)}
|
||||
"""
|
||||
await ctx.send(fmt)
|
||||
except commands.BadArgument as exc:
|
||||
await ctx.send(exc)
|
||||
else:
|
||||
await ctx.invoke(ctx.bot.get_command("config"), opt=option)
|
||||
|
||||
@config.command(name="unset", aliases=["remove"])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_guild=True)
|
||||
async def _remove_setting(self, ctx, option, *, setting=None):
|
||||
"""Unsets/removes an option from one of the settings."""
|
||||
try:
|
||||
coro = getattr(self, f"_handle_remove_{option}")
|
||||
except AttributeError:
|
||||
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
|
||||
else:
|
||||
try:
|
||||
await coro(ctx, setting=setting)
|
||||
except WrongSettingType as exc:
|
||||
await ctx.send(exc.message)
|
||||
except commands.BadArgument:
|
||||
pass
|
||||
else:
|
||||
await ctx.invoke(ctx.bot.get_command("config"), opt=option)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(GuildConfiguration())
|
357
cogs/core.py
|
@ -1,357 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
import subprocess
|
||||
import glob
|
||||
import random
|
||||
import re
|
||||
import calendar
|
||||
import pendulum
|
||||
import datetime
|
||||
import math
|
||||
|
||||
|
||||
class Core:
|
||||
"""Core commands, these are the miscallaneous commands that don't fit into other categories'"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# This is a dictionary used to hold information about which page a certain help message is on
|
||||
self.help_embeds = {}
|
||||
self.results_per_page = 10
|
||||
self.commands = None
|
||||
|
||||
def find_command(self, command):
|
||||
# This method ensures the command given is valid. We need to loop through commands
|
||||
# As self.bot.commands only includes parent commands
|
||||
# So we are splitting the command in parts, looping through the commands
|
||||
# And getting the subcommand based on the next part
|
||||
# If we try to access commands of a command that isn't a group
|
||||
# We'll hit an AttributeError, meaning an invalid command was given
|
||||
# If we loop through and don't find anything, cmd will still be None
|
||||
# And we'll report an invalid was given as well
|
||||
cmd = None
|
||||
|
||||
for part in command.split():
|
||||
try:
|
||||
if cmd is None:
|
||||
cmd = self.bot.commands.get(part)
|
||||
else:
|
||||
cmd = cmd.commands.get(part)
|
||||
except AttributeError:
|
||||
cmd = None
|
||||
break
|
||||
|
||||
return cmd
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def help(self, ctx, *, message=None):
|
||||
"""This command is used to provide a link to the help URL.
|
||||
This can be called on a command to provide more information about that command
|
||||
You can also provide a page number to pull up that page instead of the first page
|
||||
|
||||
EXAMPLE: !help help
|
||||
RESULT: This information"""
|
||||
|
||||
cmd = None
|
||||
page = 1
|
||||
|
||||
if message is not None:
|
||||
# If something is provided, it can either be the page number or a command
|
||||
# Try to convert to an int (the page number), if not, then a command should have been provided
|
||||
try:
|
||||
page = int(message)
|
||||
except:
|
||||
cmd = self.find_command(message)
|
||||
|
||||
if cmd is None:
|
||||
entries = sorted(utils.get_all_commands(self.bot))
|
||||
try:
|
||||
pages = utils.Pages(self.bot, message=ctx.message, entries=entries)
|
||||
await pages.paginate(start_page=page)
|
||||
except utils.CannotPaginate as e:
|
||||
await self.bot.say(str(e))
|
||||
else:
|
||||
# Get the description for a command
|
||||
description = cmd.help
|
||||
if description is not None:
|
||||
# Split into examples, results, and the description itself based on the string
|
||||
example = [x.replace('EXAMPLE: ', '') for x in description.split('\n') if 'EXAMPLE:' in x]
|
||||
result = [x.replace('RESULT: ', '') for x in description.split('\n') if 'RESULT:' in x]
|
||||
description = [x for x in description.split('\n') if x and 'EXAMPLE:' not in x and 'RESULT:' not in x]
|
||||
else:
|
||||
example = None
|
||||
result = None
|
||||
# Also get the subcommands for this command, if they exist
|
||||
subcommands = [x for x in utils.get_subcommands(cmd) if x != cmd.qualified_name]
|
||||
|
||||
# The rest is simple, create the embed, set the thumbail to me, add all fields if they exist
|
||||
embed = discord.Embed(title=cmd.qualified_name)
|
||||
embed.set_thumbnail(url=self.bot.user.avatar_url)
|
||||
if description:
|
||||
embed.add_field(name="Description", value="\n".join(description), inline=False)
|
||||
if example:
|
||||
embed.add_field(name="Example", value="\n".join(example), inline=False)
|
||||
if result:
|
||||
embed.add_field(name="Result", value="\n".join(result), inline=False)
|
||||
if subcommands:
|
||||
embed.add_field(name='Subcommands', value="\n".join(subcommands), inline=False)
|
||||
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def motd(self, *, date=None):
|
||||
"""This command can be used to print the current MOTD (Message of the day)
|
||||
This will most likely not be updated every day, however messages will still be pushed to this every now and then
|
||||
|
||||
EXAMPLE: !motd
|
||||
RESULT: 'This is an example message of the day!'"""
|
||||
if date is None:
|
||||
motd = await utils.get_content('motd')
|
||||
try:
|
||||
# Lets set this to the first one in the list first
|
||||
latest_motd = motd[0]
|
||||
for entry in motd:
|
||||
d = pendulum.parse(entry['date'])
|
||||
|
||||
# Check if the date for this entry is newer than our currently saved latest entry
|
||||
if d > pendulum.parse(latest_motd['date']):
|
||||
latest_motd = entry
|
||||
|
||||
date = latest_motd['date']
|
||||
motd = latest_motd['motd']
|
||||
# This will be hit if we do not have any entries for motd
|
||||
except TypeError:
|
||||
await self.bot.say("No message of the day!")
|
||||
else:
|
||||
fmt = "Last updated: {}\n\n{}".format(date, motd)
|
||||
await self.bot.say(fmt)
|
||||
else:
|
||||
try:
|
||||
motd = await utils.get_content('motd', str(pendulum.parse(date).date()))
|
||||
date = motd['date']
|
||||
motd = motd['motd']
|
||||
fmt = "Message of the day for {}:\n\n{}".format(date, motd)
|
||||
await self.bot.say(fmt)
|
||||
# This one will be hit if we return None for that day
|
||||
except TypeError:
|
||||
await self.bot.say("No message of the day for {}!".format(date))
|
||||
# This will be hit if pendulum fails to parse the date passed
|
||||
except ValueError:
|
||||
now = pendulum.utcnow().to_date_string()
|
||||
await self.bot.say("Invalid date format! Try like {}".format(now))
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def calendar(self, month: str = None, year: int = None):
|
||||
"""Provides a printout of the current month's calendar
|
||||
Provide month and year to print the calendar of that year and month
|
||||
|
||||
EXAMPLE: !calendar january 2011"""
|
||||
|
||||
# calendar takes in a number for the month, not the words
|
||||
# so we need this dictionary to transform the word to the number
|
||||
months = {
|
||||
"january": 1,
|
||||
"february": 2,
|
||||
"march": 3,
|
||||
"april": 4,
|
||||
"may": 5,
|
||||
"june": 6,
|
||||
"july": 7,
|
||||
"august": 8,
|
||||
"september": 9,
|
||||
"october": 10,
|
||||
"november": 11,
|
||||
"december": 12
|
||||
}
|
||||
# In month was not passed, use the current month
|
||||
if month is None:
|
||||
month = datetime.date.today().month
|
||||
else:
|
||||
month = months.get(month.lower())
|
||||
if month is None:
|
||||
await self.bot.say("Please provide a valid Month!")
|
||||
return
|
||||
# If year was not passed, use the current year
|
||||
if year is None:
|
||||
year = datetime.datetime.today().year
|
||||
# Here we create the actual "text" calendar that we are printing
|
||||
cal = calendar.TextCalendar().formatmonth(year, month)
|
||||
await self.bot.say("```\n{}```".format(cal))
|
||||
|
||||
@commands.command(enabled=False)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def info(self):
|
||||
"""This command can be used to print out some of my information"""
|
||||
# fmt is a dictionary so we can set the key to it's output, then print both
|
||||
# The only real use of doing it this way is easier editing if the info
|
||||
# in this command is changed
|
||||
|
||||
bot_data = await utils.get_content('bot_data')
|
||||
total_data = {'member_count': 0,
|
||||
'server_count': 0}
|
||||
for entry in bot_data:
|
||||
total_data['member_count'] += entry['member_count']
|
||||
total_data['server_count'] += entry['server_count']
|
||||
|
||||
# Create the original embed object
|
||||
opts = {'title': 'Dev Server',
|
||||
'description': 'Join the server above for any questions/suggestions about me.',
|
||||
'url': utils.dev_server}
|
||||
embed = discord.Embed(**opts)
|
||||
|
||||
# Add the normal values
|
||||
embed.add_field(name='Total Servers', value=total_data['server_count'])
|
||||
embed.add_field(name='Total Members', value=total_data['member_count'])
|
||||
|
||||
# Count the variable values; hangman, tictactoe, etc.
|
||||
hm_games = len(self.bot.get_cog('Hangman').games)
|
||||
|
||||
ttt_games = len(self.bot.get_cog('TicTacToe').boards)
|
||||
|
||||
bj_games = len(self.bot.get_cog('Blackjack').games)
|
||||
|
||||
count_battles = 0
|
||||
for battles in self.bot.get_cog('Interaction').battles.values():
|
||||
count_battles += len(battles)
|
||||
|
||||
if hm_games:
|
||||
embed.add_field(name='Total Hangman games running', value=hm_games)
|
||||
if ttt_games:
|
||||
embed.add_field(name='Total TicTacToe games running', value=ttt_games)
|
||||
if count_battles:
|
||||
embed.add_field(name='Total battles games running', value=count_battles)
|
||||
if bj_games:
|
||||
embed.add_field(name='Total blackjack games running', value=bj_games)
|
||||
|
||||
embed.add_field(name='Uptime', value=(pendulum.utcnow() - self.bot.uptime).in_words())
|
||||
embed.set_footer(text=self.bot.description)
|
||||
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def uptime(self):
|
||||
"""Provides a printout of the current bot's uptime
|
||||
|
||||
EXAMPLE: !uptime
|
||||
RESULT: A BAJILLION DAYS"""
|
||||
await self.bot.say("Uptime: ```\n{}```".format((pendulum.utcnow() - self.bot.uptime).in_words()))
|
||||
|
||||
@commands.command(aliases=['invite'])
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def addbot(self):
|
||||
"""Provides a link that you can use to add me to a server
|
||||
|
||||
EXAMPLE: !addbot
|
||||
RESULT: http://discord.gg/yo_mama"""
|
||||
perms = discord.Permissions.none()
|
||||
perms.read_messages = True
|
||||
perms.send_messages = True
|
||||
perms.manage_roles = True
|
||||
perms.ban_members = True
|
||||
perms.kick_members = True
|
||||
perms.manage_messages = True
|
||||
perms.embed_links = True
|
||||
perms.read_message_history = True
|
||||
perms.attach_files = True
|
||||
app_info = await self.bot.application_info()
|
||||
await self.bot.say("Use this URL to add me to a server that you'd like!\n{}"
|
||||
.format(discord.utils.oauth_url(app_info.id, perms)))
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def doggo(self):
|
||||
"""Use this to print a random doggo image.
|
||||
|
||||
EXAMPLE: !doggo
|
||||
RESULT: A beautiful picture of a dog o3o"""
|
||||
# Find a random image based on how many we currently have
|
||||
f = random.SystemRandom().choice(glob.glob('images/doggo*'))
|
||||
with open(f, 'rb') as f:
|
||||
await self.bot.upload(f)
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def snek(self):
|
||||
"""Use this to print a random snek image.
|
||||
|
||||
EXAMPLE: !snek
|
||||
RESULT: A beautiful picture of a snek o3o"""
|
||||
# Find a random image based on how many we currently have
|
||||
f = random.SystemRandom().choice(glob.glob('images/snek*'))
|
||||
with open(f, 'rb') as f:
|
||||
await self.bot.upload(f)
|
||||
|
||||
@commands.command()
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def joke(self):
|
||||
"""Prints a random riddle
|
||||
|
||||
EXAMPLE: !joke
|
||||
RESULT: An absolutely terrible joke."""
|
||||
# Use the fortune riddles command because it's funny, I promise
|
||||
fortune_command = "/usr/bin/fortune riddles"
|
||||
while True:
|
||||
try:
|
||||
fortune = subprocess.check_output(
|
||||
fortune_command.split()).decode("utf-8")
|
||||
await self.bot.say(fortune)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def roll(self, ctx, notation: str = "d6"):
|
||||
"""Rolls a die based on the notation given
|
||||
Format should be #d#
|
||||
|
||||
EXAMPLE: !roll d50
|
||||
RESULT: 51 :^)"""
|
||||
# Use regex to get the notation based on what was provided
|
||||
try:
|
||||
# We do not want to try to convert the dice, because we want d# to
|
||||
# be a valid notation
|
||||
dice = re.search("(\d*)d(\d*)", notation).group(1)
|
||||
num = int(re.search("(\d*)d(\d*)", notation).group(2))
|
||||
# Check if something like ed3 was provided, or something else entirely
|
||||
# was provided
|
||||
except (AttributeError, ValueError):
|
||||
await self.bot.say("Please provide the die notation in #d#!")
|
||||
return
|
||||
|
||||
# Dice will be None if d# was provided, assume this means 1d#
|
||||
dice = dice or 1
|
||||
# Since we did not try to convert to int before, do it now after we
|
||||
# have it set
|
||||
dice = int(dice)
|
||||
if dice > 10:
|
||||
await self.bot.say("I'm not rolling more than 10 dice, I have tiny hands")
|
||||
return
|
||||
if num > 100:
|
||||
await self.bot.say("What die has more than 100 sides? Please, calm down")
|
||||
return
|
||||
if num <= 1:
|
||||
await self.bot.say("A {} sided die? You know that's impossible right?".format(num))
|
||||
return
|
||||
|
||||
nums = [random.SystemRandom().randint(1, num) for i in range(0, int(dice))]
|
||||
total = sum(nums)
|
||||
value_str = ", ".join("{}".format(x) for x in nums)
|
||||
|
||||
if dice == 1:
|
||||
fmt = '{0.message.author.name} has rolled a {2} sided die and got the number {3}!'
|
||||
else:
|
||||
fmt = '{0.message.author.name} has rolled {1}, {2} sided dice and got the numbers {3}, for a total of {4}!'
|
||||
await self.bot.say(fmt.format(ctx, dice, num, value_str, total))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Core(bot))
|
|
@ -1,165 +0,0 @@
|
|||
from .utils import checks
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
class Music:
|
||||
"""
|
||||
This cog is simply created in order to add all commands in the playlist cog
|
||||
in case 'this' instance of the bot has not loaded the playlist cog.
|
||||
This is useful to have the possiblity to split the music and text commands,
|
||||
And still use commands that require another command to be passed
|
||||
from the instance that hasn't loaded the playlist cog
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
async def on_voice_state_update(self, before, after):
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def progress(self, ctx):
|
||||
"""Provides the progress of the current song
|
||||
|
||||
EXAMPLE: !progress
|
||||
RESULT: 532 minutes! (Hopefully not)"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def join(self, ctx, *, channel: discord.Channel):
|
||||
"""Joins a voice channel.
|
||||
|
||||
EXAMPLE: !join Music
|
||||
RESULT: I'm in the Music voice channel!"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def summon(self, ctx):
|
||||
"""Summons the bot to join your voice channel.
|
||||
|
||||
EXAMPLE: !summon
|
||||
RESULT: I'm in your voice channel!"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def play(self, ctx, *, song: str):
|
||||
"""Plays a song.
|
||||
If there is a song currently in the queue, then it is
|
||||
queued until the next song is done playing.
|
||||
This command automatically searches as well from YouTube.
|
||||
The list of supported sites can be found here:
|
||||
https://rg3.github.io/youtube-dl/supportedsites.html
|
||||
|
||||
EXAMPLE: !play Song by Band
|
||||
RESULT: Song by Band will be queued to play!
|
||||
"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def volume(self, ctx, value: int = None):
|
||||
"""Sets the volume of the currently playing song.
|
||||
|
||||
EXAMPLE: !volume 50
|
||||
RESULT: My volume is now set to 50"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def pause(self, ctx):
|
||||
"""Pauses the currently played song.
|
||||
|
||||
EXAMPLE: !pause
|
||||
RESULT: I'm paused!"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def resume(self, ctx):
|
||||
"""Resumes the currently played song.
|
||||
|
||||
EXAMPLE: !resume
|
||||
RESULT: Ain't paused no more!"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def stop(self, ctx):
|
||||
"""Stops playing audio and leaves the voice channel.
|
||||
This also clears the queue.
|
||||
|
||||
EXAMPLE: !stop
|
||||
RESULT: No more music"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def eta(self, ctx):
|
||||
"""Provides an ETA on when your next song will play
|
||||
|
||||
EXAMPLE: !eta
|
||||
RESULT: 5,000 days! Lol have fun"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def queue(self, ctx):
|
||||
"""Provides a printout of the songs that are in the queue.
|
||||
\N{LEFTWARDS BLACK ARROW}: Goes to the previous page
|
||||
\N{BLACK RIGHTWARDS ARROW}: Goes to the next page
|
||||
\N{DOWNWARDS BLACK ARROW}: Moves the current song showing back in the queue
|
||||
\N{UPWARDS BLACK ARROW}: Moves the current song showing up in the queue
|
||||
\N{CROSS MARK}: Removes the current song showing from the queue
|
||||
|
||||
EXAMPLE: !queue
|
||||
RESULT: A list of shitty songs you probably don't wanna listen to"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def queuelength(self, ctx):
|
||||
"""Prints the length of the queue
|
||||
|
||||
EXAMPLE: !queuelength
|
||||
RESULT: Probably 10 songs"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def skip(self, ctx):
|
||||
"""Vote to skip a song. The song requester can automatically skip.
|
||||
approximately 1/3 of the members in the voice channel
|
||||
are required to vote to skip for the song to be skipped.
|
||||
|
||||
EXAMPLE: !skip
|
||||
RESULT: You probably still have to wait for others to skip...have fun listening still
|
||||
"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def modskip(self, ctx):
|
||||
"""Forces a song skip, can only be used by a moderator
|
||||
|
||||
EXAMPLE: !modskip
|
||||
RESULT: No more terrible song :D"""
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, enabled=False)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def playing(self, ctx):
|
||||
"""Shows info about the currently played song.
|
||||
|
||||
EXAMPLE: !playing
|
||||
RESULT: Information about the song that's currently playing!"""
|
||||
pass
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Music(bot))
|
153
cogs/events.py
|
@ -1,33 +1,32 @@
|
|||
from .utils import config
|
||||
import aiohttp
|
||||
import logging
|
||||
import json
|
||||
import discord
|
||||
|
||||
from utils import config
|
||||
from discord.ext import commands
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
discord_bots_url = 'https://bots.discord.pw/api'
|
||||
discord_bots_url = 'https://bots.discord.pw/api/bots/{}/stats'
|
||||
discordbots_url = "https://discordbots.org/api/bots/{}/stats"
|
||||
carbonitex_url = 'https://www.carbonitex.net/discord/data/botdata.php'
|
||||
|
||||
|
||||
class StatsUpdate:
|
||||
"""This is used purely to update stats information for carbonitex and botx.discord.pw"""
|
||||
class StatsUpdate(commands.Cog):
|
||||
"""This is used purely to update stats information for the bot sites"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
def __unload(self):
|
||||
def cog_unload(self):
|
||||
self.bot.loop.create_task(self.session.close())
|
||||
|
||||
async def update(self):
|
||||
# Currently disabled
|
||||
return
|
||||
server_count = 0
|
||||
data = await config.get_content('bot_data')
|
||||
|
||||
for entry in data:
|
||||
server_count += entry.get('server_count')
|
||||
server_count = len(self.bot.guilds)
|
||||
|
||||
# Carbonitex request
|
||||
carbon_payload = {
|
||||
'key': config.carbon_key,
|
||||
'servercount': server_count
|
||||
|
@ -36,6 +35,7 @@ class StatsUpdate:
|
|||
async with self.session.post(carbonitex_url, data=carbon_payload) as resp:
|
||||
log.info('Carbonitex statistics returned {} for {}'.format(resp.status, carbon_payload))
|
||||
|
||||
# Discord.bots.pw request
|
||||
payload = json.dumps({
|
||||
'server_count': server_count
|
||||
})
|
||||
|
@ -45,74 +45,89 @@ class StatsUpdate:
|
|||
'content-type': 'application/json'
|
||||
}
|
||||
|
||||
url = '{}/bots/{}/stats'.format(discord_bots_url, self.bot.user.id)
|
||||
url = discord_bots_url.format(self.bot.user.id)
|
||||
async with self.session.post(url, data=payload, headers=headers) as resp:
|
||||
log.info('bots.discord.pw statistics returned {} for {}'.format(resp.status, payload))
|
||||
|
||||
async def on_server_join(self, server):
|
||||
return
|
||||
r_filter = {'shard_id': config.shard_id}
|
||||
server_count = len(self.bot.servers)
|
||||
member_count = len(set(self.bot.get_all_members()))
|
||||
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
|
||||
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
|
||||
if not await config.update_content('bot_data', entry, r_filter):
|
||||
await config.add_content('bot_data', entry, r_filter)
|
||||
self.bot.loop.create_task(self.update())
|
||||
# discordbots.com request
|
||||
url = discordbots_url.format(self.bot.user.id)
|
||||
payload = {
|
||||
"server_count": server_count
|
||||
}
|
||||
|
||||
async def on_server_leave(self, server):
|
||||
return
|
||||
r_filter = {'shard_id': config.shard_id}
|
||||
server_count = len(self.bot.servers)
|
||||
member_count = len(set(self.bot.get_all_members()))
|
||||
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
|
||||
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
|
||||
if not await config.update_content('bot_data', entry, r_filter):
|
||||
await config.add_content('bot_data', entry, r_filter)
|
||||
self.bot.loop.create_task(self.update())
|
||||
headers = {
|
||||
"Authorization": config.discordbots_key
|
||||
}
|
||||
async with self.session.post(url, data=payload, headers=headers) as resp:
|
||||
log.info('discordbots.com statistics retruned {} for {}'.format(resp.status, payload))
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, _):
|
||||
await self.update()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_leave(self, _):
|
||||
await self.update()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
return
|
||||
r_filter = {'shard_id': config.shard_id}
|
||||
server_count = len(self.bot.servers)
|
||||
member_count = len(set(self.bot.get_all_members()))
|
||||
entry = {'server_count': server_count, 'member_count': member_count, "shard_id": config.shard_id}
|
||||
# Check if this was successful, if it wasn't, that means a new shard was added and we need to add that entry
|
||||
if not await config.update_content('bot_data', entry, r_filter):
|
||||
await config.add_content('bot_data', entry, r_filter)
|
||||
self.bot.loop.create_task(self.update())
|
||||
await self.update()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
server = member.server
|
||||
server_settings = await config.get_content('server_settings', server.id)
|
||||
query = """
|
||||
SELECT
|
||||
COALESCE(welcome_alerts, default_alerts) AS channel,
|
||||
welcome_msg AS msg,
|
||||
join_role as role,
|
||||
welcome_notifications as notify
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
id = $1
|
||||
"""
|
||||
settings = await self.bot.db.fetchrow(query, member.guild.id)
|
||||
if settings:
|
||||
message = settings['msg'] or "Welcome to the '{server}' server {member}!"
|
||||
if settings["notify"]:
|
||||
try:
|
||||
channel = member.guild.get_channel(settings['channel'])
|
||||
await channel.send(message.format(server=member.guild.name, member=member.mention))
|
||||
# Forbidden for if the channel has send messages perms off
|
||||
# HTTP Exception to catch any weird happenings
|
||||
# Attribute Error catches when a channel is set, but that channel doesn't exist any more
|
||||
except (discord.Forbidden, discord.HTTPException, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
join_leave_on = server_settings['join_leave']
|
||||
if join_leave_on:
|
||||
channel_id = server_settings.get('notification_channel') or member.server.id
|
||||
else:
|
||||
return
|
||||
except (IndexError, TypeError, KeyError):
|
||||
return
|
||||
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "Welcome to the '{0.server.name}' server {0.mention}!".format(member))
|
||||
try:
|
||||
role = member.guild.get_role(settings['role'])
|
||||
await member.add_roles(role)
|
||||
except (discord.Forbidden, discord.HTTPException, AttributeError):
|
||||
pass
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_remove(self, member):
|
||||
server = member.server
|
||||
server_settings = await config.get_content('server_settings', server.id)
|
||||
|
||||
try:
|
||||
join_leave_on = server_settings['join_leave']
|
||||
if join_leave_on:
|
||||
channel_id = server_settings.get('notification_channel') or member.server.id
|
||||
else:
|
||||
return
|
||||
except (IndexError, TypeError, KeyError):
|
||||
return
|
||||
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "{0} has left the server, I hope it wasn't because of something I said :c".format(member.display_name))
|
||||
query = """
|
||||
SELECT
|
||||
COALESCE(goodbye_alerts, default_alerts) AS channel,
|
||||
goodbye_msg AS msg
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
goodbye_notifications = True
|
||||
AND
|
||||
id = $1
|
||||
AND
|
||||
COALESCE(goodbye_alerts, default_alerts) IS NOT NULL
|
||||
"""
|
||||
settings = await self.bot.db.fetchrow(query, member.guild.id)
|
||||
if settings:
|
||||
message = settings['msg'] or "{member} has left the server, I hope it wasn't because of something I said :c"
|
||||
channel = member.guild.get_channel(settings['channel'])
|
||||
try:
|
||||
await channel.send(message.format(server=member.guild.name, member=member.mention))
|
||||
except (discord.Forbidden, discord.HTTPException, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
139
cogs/hangman.py
|
@ -1,43 +1,11 @@
|
|||
from discord.ext import commands
|
||||
from discord.ext.commands.cooldowns import BucketType
|
||||
import discord
|
||||
|
||||
from .utils import checks
|
||||
from utils import checks
|
||||
|
||||
import re
|
||||
import random
|
||||
|
||||
phrases = ["Eat My Hat", "Par For the Course", "Raining Cats and Dogs", "Roll With the Punches",
|
||||
"Curiosity Killed The Cat", "Man of Few Words", "Cry Over Spilt Milk", "Scot-free", "Rain on Your Parade",
|
||||
"Go For Broke", "Shot In the Dark", "Mountain Out of a Molehill", "Jaws of Death", "A Dime a Dozen",
|
||||
"Jig Is Up", "Elvis Has Left The Building", "Wake Up Call", "Jumping the Gun", "Up In Arms",
|
||||
"Beating Around the Bush", "Flea Market", "Playing For Keeps", "Cut To The Chase", "Fight Fire With Fire",
|
||||
"Keep Your Shirt On", "Poke Fun At", "Everything But The Kitchen Sink", "Jaws of Life",
|
||||
"What Goes Up Must Come Down", "Give a Man a Fish", "Plot Thickens - The",
|
||||
"Not the Sharpest Tool in the Shed", "Needle In a Haystack", "Right Off the Bat", "Throw In the Towel",
|
||||
"Down To Earth", "Lickety Split", "I Smell a Rat", "Long In The Tooth",
|
||||
"You Can't Teach an Old Dog New Tricks", "Back To the Drawing Board", "Down For The Count",
|
||||
"On the Same Page", "Under Your Nose", "Cut The Mustard",
|
||||
"If You Can't Stand the Heat, Get Out of the Kitchen", "Knock Your Socks Off", "Playing Possum",
|
||||
"No-Brainer", "Money Doesn't Grow On Trees", "In a Pickle", "In the Red", "Fit as a Fiddle", "Hear, Hear",
|
||||
"Hands Down", "Off One's Base", "Wild Goose Chase", "Keep Your Eyes Peeled", "A Piece of Cake",
|
||||
"Foaming At The Mouth", "Go Out On a Limb", "Quick and Dirty", "Hit Below The Belt",
|
||||
"Birds of a Feather Flock Together", "Wouldn't Harm a Fly", "Son of a Gun",
|
||||
"Between a Rock and a Hard Place", "Down And Out", "Cup Of Joe", "Down To The Wire",
|
||||
"Don't Look a Gift Horse In The Mouth", "Talk the Talk", "Close But No Cigar",
|
||||
"Jack of All Trades Master of None", "High And Dry", "A Fool and His Money are Soon Parted",
|
||||
"Every Cloud Has a Silver Lining", "Tough It Out", "Under the Weather", "Happy as a Clam",
|
||||
"An Arm and a Leg", "Read 'Em and Weep", "Right Out of the Gate", "Know the Ropes",
|
||||
"It's Not All It's Cracked Up To Be", "On the Ropes", "Burst Your Bubble", "Mouth-watering",
|
||||
"Swinging For the Fences", "Fool's Gold", "On Cloud Nine", "Fish Out Of Water", "Ring Any Bells?",
|
||||
"There's No I in Team", "Ride Him, Cowboy!", "Top Drawer", "No Ifs, Ands, or Buts",
|
||||
"You Can't Judge a Book By Its Cover", "Don't Count Your Chickens Before They Hatch", "Cry Wolf",
|
||||
"Beating a Dead Horse", "Goody Two-Shoes", "Heads Up", "Drawing a Blank", "Keep On Truckin'", "Tug of War",
|
||||
"Short End of the Stick", "Hard Pill to Swallow", "Back to Square One", "Love Birds", "Dropping Like Flies",
|
||||
"Break The Ice", "Knuckle Down", "Lovey Dovey", "Greased Lightning", "Let Her Rip", "All Greek To Me",
|
||||
"Two Down, One to Go", "What Am I, Chopped Liver?", "It's Not Brain Surgery", "Like Father Like Son",
|
||||
"Easy As Pie", "Elephant in the Room", "Quick On the Draw", "Barking Up The Wrong Tree",
|
||||
"A Chip on Your Shoulder", "Put a Sock In It", "Quality Time", "Yada Yada", "Head Over Heels",
|
||||
"My Cup of Tea", "Ugly Duckling", "Drive Me Nuts", "When the Rubber Hits the Road"]
|
||||
import asyncio
|
||||
|
||||
|
||||
class Game:
|
||||
|
@ -96,29 +64,35 @@ class Game:
|
|||
return fmt
|
||||
|
||||
|
||||
class Hangman:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.games = {}
|
||||
class Hangman(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
|
||||
games = {}
|
||||
pending_games = []
|
||||
|
||||
def create(self, word, ctx):
|
||||
# Create a new game, then save it as the server's game
|
||||
game = Game(word)
|
||||
self.games[ctx.message.server.id] = game
|
||||
self.games[ctx.message.guild.id] = game
|
||||
game.author = ctx.message.author.id
|
||||
return game
|
||||
|
||||
@commands.group(aliases=['hm'], pass_context=True, no_pm=True, invoke_without_command=True)
|
||||
@commands.group(aliases=['hm'], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 7, BucketType.user)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@checks.can_run(send_messages=True)
|
||||
async def hangman(self, ctx, *, guess):
|
||||
"""Makes a guess towards the server's currently running hangman game
|
||||
|
||||
EXAMPLE: !hangman e (or) !hangman The Phrase!
|
||||
RESULT: Hopefully a win!"""
|
||||
game = self.games.get(ctx.message.server.id)
|
||||
game = self.games.get(ctx.message.guild.id)
|
||||
if not game:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("There are currently no hangman games running!")
|
||||
await ctx.send("There are currently no hangman games running!")
|
||||
return
|
||||
if game.author == ctx.message.author.id:
|
||||
await ctx.send("You cannot guess on your own hangman game!")
|
||||
return
|
||||
|
||||
# Check if we are guessing a letter or a phrase. Only one letter can be guessed at a time
|
||||
|
@ -126,9 +100,9 @@ class Hangman:
|
|||
# We're creating a fmt variable, so that we can add a message for if a guess was correct or not
|
||||
# And also add a message for a loss/win
|
||||
if len(guess) == 1:
|
||||
if guess in game.guessed_letters:
|
||||
if guess.lower() in game.guessed_letters:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("That letter has already been guessed!")
|
||||
await ctx.send("That letter has already been guessed!")
|
||||
# Return here as we don't want to count this as a failure
|
||||
return
|
||||
if game.guess_letter(guess):
|
||||
|
@ -142,18 +116,19 @@ class Hangman:
|
|||
fmt = "Sorry that's not the correct phrase..."
|
||||
|
||||
if game.win():
|
||||
fmt += " You guys got it! The word was `{}`".format(game.word)
|
||||
del self.games[ctx.message.server.id]
|
||||
fmt += " You guys got it! The phrase was `{}`".format(game.word)
|
||||
del self.games[ctx.message.guild.id]
|
||||
elif game.failed():
|
||||
fmt += " Sorry, you guys failed...the word was `{}`".format(game.word)
|
||||
del self.games[ctx.message.server.id]
|
||||
fmt += " Sorry, you guys failed...the phrase was `{}`".format(game.word)
|
||||
del self.games[ctx.message.guild.id]
|
||||
else:
|
||||
fmt += str(game)
|
||||
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@hangman.command(name='create', aliases=['start'], no_pm=True, pass_context=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@hangman.command(name='create', aliases=['start'])
|
||||
@commands.guild_only()
|
||||
@checks.can_run(send_messages=True)
|
||||
async def create_hangman(self, ctx):
|
||||
"""This is used to create a new hangman game
|
||||
A predefined phrase will be randomly chosen as the phrase to use
|
||||
|
@ -163,17 +138,55 @@ class Hangman:
|
|||
|
||||
# Only have one hangman game per server, since anyone
|
||||
# In a server (except the creator) can guess towards the current game
|
||||
if self.games.get(ctx.message.server.id) is not None:
|
||||
await self.bot.say("Sorry but only one Hangman game can be running per server!")
|
||||
if self.games.get(ctx.message.guild.id) is not None:
|
||||
await ctx.send("Sorry but only one Hangman game can be running per server!")
|
||||
return
|
||||
if ctx.guild.id in self.pending_games:
|
||||
await ctx.send("Someone has already started one, and I'm now waiting for them...")
|
||||
return
|
||||
|
||||
game = self.create(random.SystemRandom().choice(phrases), ctx)
|
||||
try:
|
||||
msg = await ctx.message.author.send(
|
||||
"Please respond with a phrase you would like to use for your hangman game in **{}**\n\nPlease keep "
|
||||
"phrases less than 31 characters".format(
|
||||
ctx.message.guild.name))
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
"I can't message you {}! Please allow DM's so I can message you and ask for the hangman phrase you "
|
||||
"want to use!".format(ctx.message.author.display_name))
|
||||
return
|
||||
|
||||
await ctx.send("I have DM'd you {}, please respond there with the phrase you would like to setup".format(
|
||||
ctx.message.author.display_name))
|
||||
|
||||
def check(m):
|
||||
return m.channel == msg.channel and len(m.content) <= 30
|
||||
|
||||
self.pending_games.append(ctx.guild.id)
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
self.pending_games.remove(ctx.guild.id)
|
||||
await ctx.send(
|
||||
"You took too long! Please look at your DM's as that's where I'm asking for the phrase you want to use")
|
||||
return
|
||||
else:
|
||||
self.pending_games.remove(ctx.guild.id)
|
||||
|
||||
forbidden_phrases = ['stop', 'delete', 'remove', 'end', 'create', 'start']
|
||||
if msg.content in forbidden_phrases:
|
||||
await ctx.send("Detected forbidden hangman phrase; current forbidden phrases are: \n{}".format(
|
||||
"\n".join(forbidden_phrases)))
|
||||
return
|
||||
|
||||
game = self.create(msg.content, ctx)
|
||||
# Let them know the game has started, then print the current game so that the blanks are shown
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
"Alright, a hangman game has just started, you can start guessing now!\n{}".format(str(game)))
|
||||
|
||||
@hangman.command(name='delete', aliases=['stop', 'remove', 'end'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
@hangman.command(name='delete', aliases=['stop', 'remove', 'end'])
|
||||
@commands.guild_only()
|
||||
@checks.can_run(kick_members=True)
|
||||
async def stop_game(self, ctx):
|
||||
"""Force stops a game of hangman
|
||||
This should realistically only be used in a situation like one player leaves
|
||||
|
@ -181,12 +194,12 @@ class Hangman:
|
|||
|
||||
EXAMPLE: !hangman stop
|
||||
RESULT: No more men being hung"""
|
||||
if self.games.get(ctx.message.server.id) is None:
|
||||
await self.bot.say("There are no Hangman games running on this server!")
|
||||
if self.games.get(ctx.message.guild.id) is None:
|
||||
await ctx.send("There are no Hangman games running on this server!")
|
||||
return
|
||||
|
||||
del self.games[ctx.message.server.id]
|
||||
await self.bot.say("I have just stopped the game of Hangman, a new should be able to be started now!")
|
||||
del self.games[ctx.message.guild.id]
|
||||
await ctx.send("I have just stopped the game of Hangman, a new should be able to be started now!")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
291
cogs/images.py
Normal file
|
@ -0,0 +1,291 @@
|
|||
from discord.ext import commands
|
||||
import discord
|
||||
import itertools
|
||||
import random
|
||||
import re
|
||||
import math
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
class Images(commands.Cog):
|
||||
"""Commands that post images, or look up images"""
|
||||
|
||||
async def horse_noodle_api(self, ctx, animal):
|
||||
data = await utils.request(f"http://hrsendl.com/api/{animal}")
|
||||
|
||||
try:
|
||||
url = data["data"]["file_url_size_large"]
|
||||
filename = data["data"]["file_name"]
|
||||
except (KeyError, TypeError):
|
||||
return await ctx.send(f"I couldn't connect! Sorry no {animal}s right now ;w;")
|
||||
else:
|
||||
image = await utils.download_image(url)
|
||||
f = discord.File(image, filename=filename)
|
||||
try:
|
||||
await ctx.send(file=f)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
f"File to large to send as attachment, here is the URL: {url}"
|
||||
)
|
||||
|
||||
@commands.command(aliases=['rc'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def cat(self, ctx):
|
||||
"""Use this to print a random cat image.
|
||||
|
||||
EXAMPLE: !cat
|
||||
RESULT: A beautiful picture of a cat o3o"""
|
||||
url = "http://thecatapi.com/api/images/get"
|
||||
opts = {"format": "src"}
|
||||
result = await utils.request(url, attr='url', payload=opts)
|
||||
|
||||
try:
|
||||
image = await utils.download_image(result)
|
||||
except (ValueError, AttributeError):
|
||||
await ctx.send("I couldn't connect! Sorry no cats right now ;w;")
|
||||
|
||||
f = discord.File(image, filename=result.name)
|
||||
try:
|
||||
await ctx.send(file=f)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
f"File to large to send as attachment, here is the URL: {url}"
|
||||
)
|
||||
|
||||
@commands.command(aliases=['dog', 'rd'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def doggo(self, ctx):
|
||||
"""Use this to print a random doggo image.
|
||||
|
||||
EXAMPLE: !doggo
|
||||
RESULT: A beautiful picture of a dog o3o"""
|
||||
result = await utils.request('https://random.dog/woof.json')
|
||||
try:
|
||||
url = result.get("url")
|
||||
filename = re.match("https://random.dog/(.*)", url).group(1)
|
||||
except AttributeError:
|
||||
await ctx.send("I couldn't connect! Sorry no dogs right now ;w;")
|
||||
return
|
||||
|
||||
image = await utils.download_image(url)
|
||||
f = discord.File(image, filename=filename)
|
||||
try:
|
||||
await ctx.send(file=f)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
f"File to large to send as attachment, here is the URL: {url}"
|
||||
)
|
||||
|
||||
@commands.command(aliases=['snake'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def snek(self, ctx):
|
||||
"""Use this to print a random snek image.
|
||||
|
||||
EXAMPLE: !snek
|
||||
RESULT: A beautiful picture of a snek o3o"""
|
||||
await self.horse_noodle_api(ctx, "snake")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def horse(self, ctx):
|
||||
"""Use this to print a random horse image.
|
||||
|
||||
EXAMPLE: !horse
|
||||
RESULT: A beautiful picture of a horse o3o"""
|
||||
await self.horse_noodle_api(ctx, "horse")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def duck(self, ctx):
|
||||
"""Use this to print a random duck image.
|
||||
|
||||
EXAMPLE: !duck
|
||||
RESULT: A beautiful picture of a duck o3o"""
|
||||
await self.horse_noodle_api(ctx, "duck")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def snail(self, ctx):
|
||||
"""Use this to print a random snail image.
|
||||
|
||||
EXAMPLE: !snail
|
||||
RESULT: A beautiful picture of a snail o3o"""
|
||||
await self.horse_noodle_api(ctx, "snail")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def pleco(self, ctx):
|
||||
"""Use this to print a random pleco image.
|
||||
|
||||
EXAMPLE: !pleco
|
||||
RESULT: A beautiful picture of a pleco o3o"""
|
||||
await self.horse_noodle_api(ctx, "pleco")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def moth(self, ctx):
|
||||
"""Use this to print a random moth image.
|
||||
|
||||
EXAMPLE: !moth
|
||||
RESULT: A beautiful picture of a moth o3o"""
|
||||
await self.horse_noodle_api(ctx, "moth")
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def avatar(self, ctx, member: discord.Member = None):
|
||||
"""Provides an image for the provided person's avatar (yours if no other member is provided)
|
||||
|
||||
EXAMPLE: !avatar @person
|
||||
RESULT: A full image of that person's avatar"""
|
||||
|
||||
if member is None:
|
||||
member = ctx.message.author
|
||||
|
||||
url = str(member.avatar_url)
|
||||
if '.gif' not in url:
|
||||
url = str(member.avatar_url_as(format='png'))
|
||||
filename = 'avatar.png'
|
||||
else:
|
||||
filename = 'avatar.gif'
|
||||
if ctx.message.guild.me.permissions_in(ctx.message.channel).attach_files:
|
||||
filedata = await utils.download_image(url)
|
||||
if filedata is None:
|
||||
await ctx.send(url)
|
||||
else:
|
||||
try:
|
||||
f = discord.File(filedata, filename=filename)
|
||||
await ctx.send(file=f)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Sorry but that avatar is too large for me to send!")
|
||||
else:
|
||||
await ctx.send(url)
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def derpi(self, ctx, *search: str):
|
||||
"""Provides a random image from the first page of derpibooru.org for the following term
|
||||
|
||||
EXAMPLE: !derpi Rainbow Dash
|
||||
RESULT: A picture of Rainbow Dash!"""
|
||||
|
||||
if len(search) > 0:
|
||||
url = 'https://derpibooru.org/search.json'
|
||||
|
||||
# Ensure a filter was not provided, as we either want to use our own, or none (for safe pics)
|
||||
query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value))
|
||||
params = {'q': query}
|
||||
|
||||
nsfw = utils.channel_is_nsfw(ctx.message.channel)
|
||||
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms
|
||||
# Also use the custom filter that I have setup, that blocks some certain tags
|
||||
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
|
||||
if nsfw:
|
||||
params['q'] += ", (explicit OR suggestive)"
|
||||
params['filter_id'] = 95938
|
||||
else:
|
||||
params['q'] += ", safe"
|
||||
# Lets filter out some of the "crap" that's on derpibooru by requiring an image with a score higher than 15
|
||||
params['q'] += ', score.gt:15'
|
||||
|
||||
try:
|
||||
# Get the response from derpibooru and parse the 'search' result from it
|
||||
data = await utils.request(url, payload=params)
|
||||
|
||||
if data is None:
|
||||
await ctx.send("Sorry but I failed to connect to Derpibooru!")
|
||||
return
|
||||
results = data['search']
|
||||
except KeyError:
|
||||
await ctx.send("No results with that search term, {0}!".format(ctx.message.author.mention))
|
||||
return
|
||||
|
||||
# The first request we've made ensures there are results
|
||||
# Now we can get the total count from that, and make another request based on the number of pages as well
|
||||
if len(results) > 0:
|
||||
# Get the total number of pages
|
||||
pages = math.ceil(data['total'] / len(results))
|
||||
# Set a new paramater to set which page to use, randomly based on the number of pages
|
||||
params['page'] = random.SystemRandom().randint(1, pages)
|
||||
data = await utils.request(url, payload=params)
|
||||
if data is None:
|
||||
await ctx.send("Sorry but I failed to connect to Derpibooru!")
|
||||
return
|
||||
# Now get the results again
|
||||
results = data['search']
|
||||
|
||||
# Get the image link from the now random page'd and random result from that page
|
||||
index = random.SystemRandom().randint(0, len(results) - 1)
|
||||
# image_link = 'https://derpibooru.org/{}'.format(results[index]['id'])
|
||||
image_link = 'https:{}'.format(results[index]['image'])
|
||||
else:
|
||||
await ctx.send("No results with that search term, {0}!".format(ctx.message.author.mention))
|
||||
return
|
||||
else:
|
||||
# If no search term was provided, search for a random image
|
||||
# .url will be the URL we end up at, not the one requested.
|
||||
# https://derpibooru.org/images/random redirects to a random image, so this is exactly what we want
|
||||
image_link = await utils.request('https://derpibooru.org/images/random', attr='url')
|
||||
await ctx.send(image_link)
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def e621(self, ctx, *, tags: str):
|
||||
"""Searches for a random image from e621.net
|
||||
Format for the search terms need to be 'search term 1, search term 2, etc.'
|
||||
If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit
|
||||
|
||||
EXAMPLE: !e621 dragon
|
||||
RESULT: A picture of a dragon (hopefully, screw your tagging system e621)"""
|
||||
|
||||
# This changes the formatting for queries, so we don't
|
||||
# Have to use e621's stupid formatting when using the command
|
||||
|
||||
tags = tags.replace(' ', '_')
|
||||
tags = tags.replace(',_', ' ')
|
||||
|
||||
url = 'https://e621.net/posts.json'
|
||||
params = {
|
||||
'login': utils.config.e621_user,
|
||||
'api_key': utils.config.e621_key,
|
||||
'limit': 5,
|
||||
'tags': tags
|
||||
}
|
||||
headers = {'User-Agent': utils.config.user_agent}
|
||||
nsfw = utils.channel_is_nsfw(ctx.message.channel)
|
||||
|
||||
# e621 by default does not filter explicit content, so tack on
|
||||
# safe/explicit based on if this channel is nsfw or not
|
||||
params['tags'] += " rating:explicit" if nsfw else " rating:safe"
|
||||
# Tack on a random order
|
||||
params['tags'] += " order:random"
|
||||
|
||||
data = await utils.request(url, payload=params, headers=headers)
|
||||
|
||||
if data is None:
|
||||
await ctx.send("Sorry, I had trouble connecting at the moment; please try again later")
|
||||
return
|
||||
|
||||
# Try to find an image from the list. If there were no results, we're going to attempt to find
|
||||
# A number between (0,-1) and receive an error.
|
||||
# The response should be in a list format, so we'll end up getting a key error if the response was in json
|
||||
# i.e. it responded with a 404/504/etc.
|
||||
try:
|
||||
for image in data["posts"]:
|
||||
# Will support in the future
|
||||
blacklist = []
|
||||
tags = itertools.chain.from_iterable(image["tags"].values())
|
||||
# Check if any of the tags are in the blacklist
|
||||
if any(tag in tags for tag in blacklist):
|
||||
continue
|
||||
# If this image is fine, then send this and break
|
||||
await ctx.send(image["file"]["url"])
|
||||
return
|
||||
except (ValueError, KeyError):
|
||||
await ctx.send("No results with that tag {}".format(ctx.message.author.mention))
|
||||
return
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Images(bot))
|
|
@ -1,186 +1,223 @@
|
|||
from discord.ext import commands
|
||||
from discord.ext.commands.cooldowns import BucketType
|
||||
from collections import defaultdict
|
||||
|
||||
from . import utils
|
||||
import utils
|
||||
|
||||
import discord
|
||||
import random
|
||||
import functools
|
||||
|
||||
battle_outcomes = \
|
||||
["A meteor fell on {1}, {0} is left standing and has been declared the victor!",
|
||||
"{0} has bucked {1} into a tree, even Big Mac would be impressed at that kick!",
|
||||
"As they were battling, {1} was struck by lightning! {0} you lucked out this time!",
|
||||
"{1} tried to dive at {0} while fighting, somehow they missed and landed in quicksand."
|
||||
"Try paying more attention next time {1}",
|
||||
"{1} got a little...heated during the battle and ended up getting set on fire. {0} wins by remaining cool",
|
||||
"Princess Celestia came in and banished {1} to the moon. Good luck getting into any battles up there",
|
||||
"{1} took an arrow to the knee, they are no longer an adventurer. Keep on adventuring {0}",
|
||||
"Common sense should make it obvious not to get into battle with {0}. Apparently {1} didn't get the memo",
|
||||
"{0} had a nice cup of tea with {1} over their conflict, and mutually agreed that {0} was Best Pony",
|
||||
"{0} and {1} had an intense staring contest. "
|
||||
"Sadly, {1} forgot to breathe and lost much morethan the staring contest",
|
||||
"It appears {1} is actually a pacifist, they ran away screaming and crying. "
|
||||
["A meteor fell on {loser}, {winner} is left standing and has been declared the victor!",
|
||||
"{loser} was shot through the heart, and {winner} is to blame",
|
||||
"{winner} has bucked {loser} into a tree, even Big Mac would be impressed at that kick!",
|
||||
"As they were battling, {loser} was struck by lightning! {winner} you lucked out this time!",
|
||||
"{loser} tried to dive at {winner} while fighting, somehow they missed and landed in quicksand."
|
||||
"Try paying more attention next time {loser}",
|
||||
"{loser} got a little...heated during the battle and ended up getting set on fire. "
|
||||
"{winner} wins by remaining cool",
|
||||
"Princess Celestia came in and banished {loser} to the moon. Good luck getting into any battles up there",
|
||||
"{loser} took an arrow to the knee, they are no longer an adventurer. Keep on adventuring {winner}",
|
||||
"Common sense should make it obvious not to get into battle with {winner}. Apparently {loser} didn't get the memo",
|
||||
"{winner} had a nice cup of tea with {loser} over their conflict, and mutually agreed that {winner} was Best Pony",
|
||||
"{winner} and {loser} had an intense staring contest. "
|
||||
"Sadly, {loser} forgot to breathe and lost much morethan the staring contest",
|
||||
"It appears {loser} is actually a pacifist, they ran away screaming and crying. "
|
||||
"Maybe you should have thought of that before getting in a fight?",
|
||||
"A bunch of parasprites came in and ate up the jetpack while {1} was flying with it. Those pesky critters...",
|
||||
"{0} used their charm to seduce {1} to surrender.",
|
||||
"{1} slipped on a banana peel and fell into a pit of spikes. That's actually impressive.",
|
||||
"{0} realized it was high noon, {1} never even saw it coming.",
|
||||
"{1} spontaneously combusted...lol rip",
|
||||
"after many turns {0} summons exodia and {1} is sent to the shadow realm",
|
||||
"{0} and {1} sit down for an intense game of chess, in the heat of the moment {0} forgot they were playing a "
|
||||
"A bunch of parasprites came in and ate up the jetpack while {loser} was flying with it. Those pesky critters...",
|
||||
"{winner} used their charm to seduce {loser} to surrender.",
|
||||
"{loser} slipped on a banana peel and fell into a pit of spikes. That's actually impressive.",
|
||||
"{winner} realized it was high noon, {loser} never even saw it coming.",
|
||||
"{loser} spontaneously combusted...lol rip",
|
||||
"after many turns {winner} summons exodia and {loser} is sent to the shadow realm",
|
||||
"{winner} and {loser} sit down for an intense game of chess, "
|
||||
"in the heat of the moment {winner} forgot they were playing a "
|
||||
"game and summoned a real knight",
|
||||
"{0} challenges {1} to rock paper scissors, unfortunately for {1}, {0} chose scissors and stabbed them",
|
||||
"{0} goes back in time and becomes {1}'s best friend, winning without ever throwing a punch",
|
||||
"{1} trips down some stairs on their way to the battle with {0}",
|
||||
"{0} books {1} a one way ticket to Flugendorf prison",
|
||||
"{1} was already dead",
|
||||
"{1} was crushed under the weight of expectations",
|
||||
"{1} was wearing a redshirt and it was their first day",
|
||||
"{0} and {1} were walking along when suddenly {1} got kidnapped by a flying monkey; hope they had water with them",
|
||||
"{0} brought an army to a fist fight, {1} never saw their opponent once",
|
||||
"{0} used multiple simultaneous devestating defensive deep strikes to overwhelm {1}",
|
||||
"{0} and {1} engage in a dance off; {0} wiped the floor with {1}",
|
||||
"{1} tried to hide in the sand to catch {0} off guard, unfortunately looks like a Giant Antlion had the same "
|
||||
"{winner} challenges {loser} to rock paper scissors, "
|
||||
"unfortunately for {loser}, {winner} chose scissors and stabbed them",
|
||||
"{winner} goes back in time and becomes {loser}'s best friend, winning without ever throwing a punch",
|
||||
"{loser} trips down some stairs on their way to the battle with {winner}",
|
||||
"{winner} books {loser} a one way ticket to Flugendorf prison",
|
||||
"{loser} was already dead",
|
||||
"{loser} was crushed under the weight of expectations",
|
||||
"{loser} was wearing a redshirt and it was their first day",
|
||||
"{winner} and {loser} were walking along when suddenly {loser} "
|
||||
"got kidnapped by a flying monkey; hope they had water with them",
|
||||
"{winner} brought an army to a fist fight, {loser} never saw their opponent once",
|
||||
"{winner} used multiple simultaneous devestating defensive deep strikes to overwhelm {loser}",
|
||||
"{winner} and {loser} engage in a dance off; {winner} wiped the floor with {loser}",
|
||||
"{loser} tried to hide in the sand to catch {winner} off guard, "
|
||||
"unfortunately looks like a Giant Antlion had the same "
|
||||
"idea for him",
|
||||
"{1} was busy playing trash videogames the night before the fight and collapsed before {0}",
|
||||
"{0} threw a sick meme and {1} totally got PRANK'D",
|
||||
"{0} and {1} go on a skiing trip together, turns out {1} forgot how to pizza french-fry",
|
||||
"{0} is the cure and {1} is the disease....well {1} was the disease",
|
||||
"{1} talked their mouth off at {0}...literally...",
|
||||
"Looks like {1} didn't put enough points into kazoo playing, who knew they would have needed it",
|
||||
"{1} was too scared by the illuminati and extra-dimensional talking horses to show up",
|
||||
"{1} didn't press x enough to not die",
|
||||
"{0} and {1} go fishing to settle their debate, {0} caught a sizeable fish and {1} caught a boot older than time",
|
||||
"{0} did a hero landing and {1} was so surprised they gave up immediately"]
|
||||
"{loser} was busy playing trash videogames the night before the fight and collapsed before {winner}",
|
||||
"{winner} threw a sick meme and {loser} totally got PRANK'D",
|
||||
"{winner} and {loser} go on a skiing trip together, turns out {loser} forgot how to pizza french-fry",
|
||||
"{winner} is the cure and {loser} is the disease....well {loser} was the disease",
|
||||
"{loser} talked their mouth off at {winner}...literally...",
|
||||
"Looks like {loser} didn't put enough points into kazoo playing, who knew they would have needed it",
|
||||
"{loser} was too scared by the illuminati and extra-dimensional talking horses to show up",
|
||||
"{loser} didn't press x enough to not die",
|
||||
"{winner} and {loser} go fishing to settle their debate, "
|
||||
"{winner} caught a sizeable fish and {loser} caught a boot older than time",
|
||||
"{winner} did a hero landing and {loser} was so surprised they gave up immediately"]
|
||||
|
||||
hugs = \
|
||||
["*hugs {}.*",
|
||||
"*tackles {} for a hug.*",
|
||||
"*drags {} into her dungeon where hugs ensue*",
|
||||
"*pulls {} to the side for a warm hug*",
|
||||
"*goes out to buy a big enough blanket to embrace {}*",
|
||||
"*hard codes an electric hug to {}*",
|
||||
"*hires mercenaries to take {} out....to a nice dinner*",
|
||||
"*pays $10 to not touch {}*",
|
||||
"*clones herself to create a hug pile with {}*",
|
||||
"*orders an airstrike of hugs {}*",
|
||||
"*glomps {}*",
|
||||
"*hears a knock at her door, opens it, finds {} and hugs them excitedly*",
|
||||
"*goes in for a punch but misses and ends up hugging {}*",
|
||||
"*hugs {} from behind*",
|
||||
"*denies a hug from {}*",
|
||||
"*does a hug to {}*",
|
||||
"*lets {} cuddle nonchalantly*",
|
||||
"*cuddles {}*",
|
||||
"*burrows underground and pops up underneath {} she hugs their legs.*",
|
||||
"*approaches {} after having gone to the gym for several months and almost crushes them.*"]
|
||||
["*hugs {user}.*",
|
||||
"*tackles {user} for a hug.*",
|
||||
"*drags {user} into her dungeon where hugs ensue*",
|
||||
"*pulls {user} to the side for a warm hug*",
|
||||
"*goes out to buy a big enough blanket to embrace {user}*",
|
||||
"*hard codes an electric hug to {user}*",
|
||||
"*hires mercenaries to take {user} out....to a nice dinner*",
|
||||
"*pays $10 to not touch {user}*",
|
||||
"*clones herself to create a hug pile with {user}*",
|
||||
"*orders an airstrike of hugs {user}*",
|
||||
"*glomps {user}*",
|
||||
"*hears a knock at her door, opens it, finds {user} and hugs them excitedly*",
|
||||
"*goes in for a punch but misses and ends up hugging {user}*",
|
||||
"*hugs {user} from behind*",
|
||||
"*denies a hug from {user}*",
|
||||
"*does a hug to {user}*",
|
||||
"*lets {user} cuddle nonchalantly*",
|
||||
"*cuddles {user}*",
|
||||
"*burrows underground and pops up underneath {user} she hugs their legs.*",
|
||||
"*approaches {user} after having gone to the gym for several months and almost crushes them.*"]
|
||||
|
||||
|
||||
class Interaction:
|
||||
class Interaction(commands.Cog):
|
||||
"""Commands that interact with another user"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Format for battles: {'serverid': {'player1': 'player2', 'player1': 'player2'}}
|
||||
self.battles = {}
|
||||
battles = defaultdict(list)
|
||||
|
||||
def user_battling(self, ctx, player2=None):
|
||||
battling = self.battles.get(ctx.message.server.id)
|
||||
def get_receivers_battle(self, receiver):
|
||||
for battle in self.battles.get(receiver.guild.id, []):
|
||||
if battle.is_receiver(receiver):
|
||||
return battle
|
||||
|
||||
# If no one is battling, obviously the user is not battling
|
||||
if battling is None:
|
||||
return False
|
||||
# Check if the author is battling
|
||||
if ctx.message.author.id in battling.values() or ctx.message.author.id in battling.keys():
|
||||
return True
|
||||
# Check if the player2 was provided, if they are check if they're in the list
|
||||
if player2 and (player2.id in battling.values() or player2.id in battling.keys()):
|
||||
return True
|
||||
# If neither are found, no one is battling
|
||||
return False
|
||||
def can_initiate_battle(self, player):
|
||||
for battle in self.battles.get(player.guild.id, []):
|
||||
if battle.is_initiator(player):
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_receive_battle(self, player):
|
||||
for battle in self.battles.get(player.guild.id, []):
|
||||
if battle.is_receiver(player):
|
||||
return False
|
||||
return True
|
||||
|
||||
def start_battle(self, initiator, receiver):
|
||||
battle = Battle(initiator, receiver)
|
||||
self.battles[initiator.guild.id].append(battle)
|
||||
return battle
|
||||
|
||||
# Handles removing the author from the dictionary of battles
|
||||
def battling_off(self, ctx):
|
||||
battles = self.battles.get(ctx.message.server.id) or {}
|
||||
player_id = ctx.message.author.id
|
||||
# Create a new dictionary, exactly the way the last one was setup
|
||||
# But don't include any that have the author's ID
|
||||
self.battles[ctx.message.server.id] = {p1: p2 for p1, p2 in battles.items() if
|
||||
not p2 == player_id and not p1 == player_id}
|
||||
def battling_off(self, battle):
|
||||
for guild, battles in self.battles.items():
|
||||
if battle in battles:
|
||||
battles.remove(battle)
|
||||
return
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def hug(self, ctx, user: discord.Member = None):
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def hug(self, ctx, user=None):
|
||||
"""Makes me hug a person!
|
||||
|
||||
EXAMPLE: !hug @Someone
|
||||
RESULT: I hug the shit out of that person"""
|
||||
if ctx.message.mention_everyone:
|
||||
await ctx.send("Your arms aren't big enough")
|
||||
return
|
||||
if user is None:
|
||||
user = ctx.message.author
|
||||
|
||||
fmt = random.SystemRandom().choice(hugs)
|
||||
await self.bot.say(fmt.format(user.display_name))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def avatar(self, ctx, member: discord.Member = None):
|
||||
"""Provides an image for the provided person's avatar (yours if no other member is provided)
|
||||
|
||||
EXAMPLE: !avatar @person
|
||||
RESULT: A full image of that person's avatar"""
|
||||
|
||||
if member is None:
|
||||
member = ctx.message.author
|
||||
|
||||
url = member.avatar_url
|
||||
if ctx.message.server.me.permissions_in(ctx.message.channel).attach_files:
|
||||
file = await utils.download_image(url)
|
||||
if file is None:
|
||||
await self.bot.say(url)
|
||||
else:
|
||||
if '.gif' in url:
|
||||
filename = 'avatar.gif'
|
||||
else:
|
||||
filename = 'avatar.webp'
|
||||
file = utils.convert_to_jpeg(file)
|
||||
await self.bot.upload(file, filename=filename)
|
||||
user = ctx.author
|
||||
else:
|
||||
await self.bot.say(url)
|
||||
converter = commands.converter.MemberConverter()
|
||||
try:
|
||||
user = await converter.convert(ctx, user)
|
||||
except commands.converter.BadArgument:
|
||||
await ctx.send("Error: Could not find user: {}".format(user))
|
||||
return
|
||||
|
||||
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
|
||||
@commands.cooldown(1, 180, BucketType.user)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def battle(self, ctx, player2: discord.Member):
|
||||
settings = await ctx.bot.db.fetchrow(
|
||||
"SELECT custom_hugs, include_default_hugs FROM guilds WHERE id = $1",
|
||||
ctx.guild.id
|
||||
)
|
||||
msgs = hugs.copy()
|
||||
if settings:
|
||||
custom_msgs = settings["custom_hugs"]
|
||||
default_on = settings["include_default_hugs"]
|
||||
if custom_msgs:
|
||||
if default_on or default_on is None:
|
||||
msgs += custom_msgs
|
||||
else:
|
||||
msgs = custom_msgs
|
||||
# Otherwise we simply just want to use the default, no matter what the default setting is
|
||||
|
||||
fmt = random.SystemRandom().choice(msgs)
|
||||
await ctx.send(fmt.format(user=user.display_name))
|
||||
|
||||
@commands.command(aliases=['1v1'])
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 20, BucketType.user)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def battle(self, ctx, player2=None):
|
||||
"""Challenges the mentioned user to a battle
|
||||
|
||||
EXAMPLE: !battle @player2
|
||||
RESULT: A battle to the death"""
|
||||
if ctx.message.author.id == player2.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("Why would you want to battle yourself? Suicide is not the answer")
|
||||
# First check if everyone was mentioned
|
||||
if ctx.message.mention_everyone:
|
||||
await ctx.send("You want to battle {} people? Good luck with that...".format(
|
||||
len(ctx.channel.members) - 1)
|
||||
)
|
||||
return
|
||||
if self.bot.user.id == player2.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("I always win, don't even try it.")
|
||||
# Then check if nothing was provided
|
||||
if player2 is None:
|
||||
await ctx.send("Who are you trying to battle...?")
|
||||
return
|
||||
if self.user_battling(ctx, player2):
|
||||
else:
|
||||
# Otherwise, try to convert to an actual member
|
||||
converter = commands.converter.MemberConverter()
|
||||
try:
|
||||
player2 = await converter.convert(ctx, player2)
|
||||
except commands.converter.BadArgument:
|
||||
await ctx.send("Error: Could not find user: {}".format(player2))
|
||||
return
|
||||
# Then check if the person used is the author
|
||||
if ctx.author.id == player2.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("You or the person you are trying to battle is already in a battle!")
|
||||
await ctx.send("Why would you want to battle yourself? Suicide is not the answer")
|
||||
return
|
||||
# Check if the person battled is me
|
||||
if ctx.bot.user.id == player2.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await ctx.send("I always win, don't even try it.")
|
||||
return
|
||||
# Next two checks are to see if the author or person battled can be battled
|
||||
if not self.can_initiate_battle(ctx.author):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await ctx.send("You are already battling someone!")
|
||||
return
|
||||
if not self.can_receive_battle(player2):
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await ctx.send("{} is already being challenged to a battle!".format(player2))
|
||||
return
|
||||
|
||||
# Add the author and player provided in a new battle
|
||||
battles = self.battles.get(ctx.message.server.id) or {}
|
||||
battles[ctx.message.author.id] = player2.id
|
||||
self.battles[ctx.message.server.id] = battles
|
||||
battle = self.start_battle(ctx.author, player2)
|
||||
|
||||
fmt = "{0.message.author.mention} has challenged you to a battle {1.mention}\n" \
|
||||
"{0.prefix}accept or {0.prefix}decline"
|
||||
fmt = f"{ctx.author.mention} has challenged you to a battle {player2.mention}\n" \
|
||||
f"{ctx.prefix}accept or {ctx.prefix}decline"
|
||||
# Add a call to turn off battling, if the battle is not accepted/declined in 3 minutes
|
||||
self.bot.loop.call_later(180, self.battling_off, ctx)
|
||||
await self.bot.say(fmt.format(ctx, player2))
|
||||
part = functools.partial(self.battling_off, battle)
|
||||
ctx.bot.loop.call_later(180, part)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def accept(self, ctx):
|
||||
"""Accepts the battle challenge
|
||||
|
||||
|
@ -188,31 +225,136 @@ class Interaction:
|
|||
RESULT: Hopefully the other person's death"""
|
||||
# This is a check to make sure that the author is the one being BATTLED
|
||||
# And not the one that started the battle
|
||||
battles = self.battles.get(ctx.message.server.id) or {}
|
||||
p1 = [p1_id for p1_id, p2_id in battles.items() if p2_id == ctx.message.author.id]
|
||||
if len(p1) == 0:
|
||||
await self.bot.say("You are not currently being challenged to a battle!")
|
||||
battle = self.get_receivers_battle(ctx.author)
|
||||
if battle is None:
|
||||
await ctx.send("You are not currently being challenged to a battle!")
|
||||
return
|
||||
|
||||
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
|
||||
battleP2 = ctx.message.author
|
||||
if ctx.guild.get_member(battle.initiator.id) is None:
|
||||
await ctx.send("The person who challenged you to a battle has apparently left the server....why?")
|
||||
self.battling_off(battle)
|
||||
return
|
||||
|
||||
# Get a random win message from our list
|
||||
fmt = random.SystemRandom().choice(battle_outcomes)
|
||||
# Lets get the settings
|
||||
settings = await ctx.bot.db.fetchrow(
|
||||
"SELECT custom_battles, include_default_battles FROM guilds WHERE id = $1",
|
||||
ctx.guild.id
|
||||
)
|
||||
msgs = battle_outcomes
|
||||
if settings:
|
||||
custom_msgs = settings["custom_battles"]
|
||||
default_on = settings["include_default_battles"]
|
||||
# if they exist, then we want to see if we want to use default as well
|
||||
if custom_msgs:
|
||||
if default_on or default_on is None:
|
||||
msgs += custom_msgs
|
||||
else:
|
||||
msgs = custom_msgs
|
||||
|
||||
fmt = random.SystemRandom().choice(msgs)
|
||||
# Due to our previous checks, the ID should only be in the dictionary once, in the current battle we're checking
|
||||
self.battling_off(ctx)
|
||||
self.battling_off(battle)
|
||||
|
||||
# Randomize the order of who is printed/sent to the update system
|
||||
# All we need to do is change what order the challengers are printed/added as a paramater
|
||||
if random.SystemRandom().randint(0, 1):
|
||||
await self.bot.say(fmt.format(battleP1.mention, battleP2.mention))
|
||||
await utils.update_records('battle_records', battleP1, battleP2)
|
||||
# Randomize the winner/loser
|
||||
winner, loser = battle.choose()
|
||||
|
||||
member_list = [m.id for m in ctx.guild.members]
|
||||
query = """
|
||||
SELECT id, rank, battle_rating, battle_wins, battle_losses
|
||||
FROM
|
||||
(SELECT
|
||||
id,
|
||||
ROW_NUMBER () OVER (ORDER BY battle_rating DESC) as "rank",
|
||||
battle_rating,
|
||||
battle_wins,
|
||||
battle_losses
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = any($1::bigint[]) AND
|
||||
battle_rating IS NOT NULL
|
||||
) AS sub
|
||||
WHERE id = any($2)
|
||||
"""
|
||||
results = await ctx.bot.db.fetch(query, member_list, [winner.id, loser.id])
|
||||
|
||||
old_winner = old_loser = None
|
||||
for result in results:
|
||||
if result['id'] == loser.id:
|
||||
old_loser = result
|
||||
else:
|
||||
old_winner = result
|
||||
|
||||
winner_rating, loser_rating, = utils.update_rating(
|
||||
old_winner["battle_rating"] if old_winner else 1000,
|
||||
old_loser["battle_rating"] if old_loser else 1000,
|
||||
)
|
||||
|
||||
update_query = """
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
battle_rating = $1,
|
||||
battle_wins = $2,
|
||||
battle_losses = $3
|
||||
WHERE
|
||||
id=$4
|
||||
"""
|
||||
insert_query = """
|
||||
INSERT INTO
|
||||
users (id, battle_rating, battle_wins, battle_losses)
|
||||
VALUES
|
||||
($1, $2, $3, $4)
|
||||
"""
|
||||
if old_loser:
|
||||
await ctx.bot.db.execute(
|
||||
update_query,
|
||||
loser_rating,
|
||||
old_loser['battle_wins'],
|
||||
old_loser['battle_losses'] + 1,
|
||||
loser.id
|
||||
)
|
||||
else:
|
||||
await self.bot.say(fmt.format(battleP2.mention, battleP1.mention))
|
||||
await utils.update_records('battle_records', battleP2, battleP1)
|
||||
await ctx.bot.db.execute(insert_query, loser.id, loser_rating, 0, 1)
|
||||
if old_winner:
|
||||
await ctx.bot.db.execute(
|
||||
update_query,
|
||||
winner_rating,
|
||||
old_winner['battle_wins'] + 1,
|
||||
old_winner['battle_losses'],
|
||||
winner.id
|
||||
)
|
||||
else:
|
||||
await ctx.bot.db.execute(insert_query, winner.id, winner_rating, 1, 0)
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
results = await ctx.bot.db.fetch(query, member_list, [winner.id, loser.id])
|
||||
|
||||
new_winner_rank = new_loser_rank = None
|
||||
for result in results:
|
||||
if result['id'] == loser.id:
|
||||
new_loser_rank = result['rank']
|
||||
else:
|
||||
new_winner_rank = result['rank']
|
||||
|
||||
fmt = fmt.format(winner=winner.display_name, loser=loser.display_name)
|
||||
if old_winner:
|
||||
fmt += "\n{} - Rank: {} ( +{} )".format(
|
||||
winner.display_name, new_winner_rank, old_winner["rank"] - new_winner_rank
|
||||
)
|
||||
else:
|
||||
fmt += "\n{} - Rank: {}".format(winner.display_name, new_winner_rank)
|
||||
if old_loser:
|
||||
fmt += "\n{} - Rank: {} ( -{} )".format(
|
||||
loser.display_name, new_loser_rank, new_loser_rank - old_loser["rank"]
|
||||
)
|
||||
else:
|
||||
fmt += "\n{} - Rank: {}".format(loser.display_name, new_loser_rank)
|
||||
|
||||
await ctx.send(fmt)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def decline(self, ctx):
|
||||
"""Declines the battle challenge
|
||||
|
||||
|
@ -220,62 +362,74 @@ class Interaction:
|
|||
RESULT: You chicken out"""
|
||||
# This is a check to make sure that the author is the one being BATTLED
|
||||
# And not the one that started the battle
|
||||
battles = self.battles.get(ctx.message.server.id) or {}
|
||||
p1 = [p1_id for p1_id, p2_id in battles.items() if p2_id == ctx.message.author.id]
|
||||
if len(p1) == 0:
|
||||
await self.bot.say("You are not currently being challenged to a battle!")
|
||||
battle = self.get_receivers_battle(ctx.author)
|
||||
if battle is None:
|
||||
await ctx.send("You are not currently being challenged to a battle!")
|
||||
return
|
||||
|
||||
battleP1 = discord.utils.find(lambda m: m.id == p1[0], ctx.message.server.members)
|
||||
battleP2 = ctx.message.author
|
||||
self.battling_off(battle)
|
||||
await ctx.send("{} has chickened out! What a loser~".format(ctx.author.mention))
|
||||
|
||||
# There's no need to update the stats for the members if they declined the battle
|
||||
self.battling_off(ctx)
|
||||
await self.bot.say("{0} has chickened out! What a loser~".format(battleP2.mention, battleP1.mention))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@commands.cooldown(1, 180, BucketType.user)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def boop(self, ctx, boopee: discord.Member = None, *, message = ""):
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(1, 10, BucketType.user)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def boop(self, ctx, boopee: discord.Member = None, *, message=""):
|
||||
"""Boops the mentioned person
|
||||
|
||||
EXAMPLE: !boop @OtherPerson
|
||||
RESULT: You do a boop o3o"""
|
||||
booper = ctx.message.author
|
||||
booper = ctx.author
|
||||
if boopee is None:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("You try to boop the air, the air boops back. Be afraid....")
|
||||
await ctx.send("You try to boop the air, the air boops back. Be afraid....")
|
||||
return
|
||||
# To keep formatting easier, keep it either "" or the message with a space in front
|
||||
if message is not None:
|
||||
message = " " + message
|
||||
if boopee.id == booper.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("You can't boop yourself! Silly...")
|
||||
await ctx.send("You can't boop yourself! Silly...")
|
||||
return
|
||||
if boopee.id == self.bot.user.id:
|
||||
if boopee.id == ctx.bot.user.id:
|
||||
ctx.command.reset_cooldown(ctx)
|
||||
await self.bot.say("Why the heck are you booping me? Get away from me >:c")
|
||||
await ctx.send("Why the heck are you booping me? Get away from me >:c")
|
||||
return
|
||||
|
||||
key = booper.id
|
||||
boops = await utils.get_content('boops', key)
|
||||
if boops is not None:
|
||||
boops = boops['boops']
|
||||
# If the booper has never booped the member provided, assure it's 0
|
||||
amount = boops.get(boopee.id, 0) + 1
|
||||
boops[boopee.id] = amount
|
||||
|
||||
await utils.update_content('boops', {'boops': boops}, key)
|
||||
else:
|
||||
entry = {'member_id': booper.id,
|
||||
'boops': {boopee.id: 1}}
|
||||
|
||||
await utils.add_content('boops', entry)
|
||||
query = "SELECT amount FROM boops WHERE booper = $1 AND boopee = $2"
|
||||
amount = await ctx.bot.db.fetchrow(query, booper.id, boopee.id)
|
||||
if amount is None:
|
||||
amount = 1
|
||||
replacement_query = "INSERT INTO boops (booper, boopee, amount) VALUES($1, $2, $3)"
|
||||
else:
|
||||
replacement_query = "UPDATE boops SET amount=$3 WHERE booper=$1 AND boopee=$2"
|
||||
amount = amount['amount'] + 1
|
||||
|
||||
fmt = "{0.mention} has just booped {1.mention}{3}! That's {2} times now!"
|
||||
await self.bot.say(fmt.format(booper, boopee, amount, message))
|
||||
await ctx.send(f"{booper.mention} has just booped {boopee.mention}{message}! That's {amount} times now!")
|
||||
await ctx.bot.db.execute(replacement_query, booper.id, boopee.id, amount)
|
||||
|
||||
|
||||
class Battle:
|
||||
|
||||
def __init__(self, initiator, receiver):
|
||||
self.initiator = initiator
|
||||
self.receiver = receiver
|
||||
self.rand = random.SystemRandom()
|
||||
|
||||
def is_initiator(self, player):
|
||||
return player.id == self.initiator.id and player.guild.id == self.initiator.guild.id
|
||||
|
||||
def is_receiver(self, player):
|
||||
return player.id == self.receiver.id and player.guild.id == self.receiver.guild.id
|
||||
|
||||
def is_battling(self, player):
|
||||
return self.is_initiator(player) or self.is_receiver(player)
|
||||
|
||||
def choose(self):
|
||||
"""Returns the two users in the order winner, loser"""
|
||||
choices = [self.initiator, self.receiver]
|
||||
self.rand.shuffle(choices)
|
||||
return choices
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
193
cogs/links.py
|
@ -1,24 +1,19 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
import utils
|
||||
|
||||
from bs4 import BeautifulSoup as bs
|
||||
|
||||
import discord
|
||||
import random
|
||||
import re
|
||||
import math
|
||||
|
||||
|
||||
class Links:
|
||||
class Links(commands.Cog):
|
||||
"""This class contains all the commands that make HTTP requests
|
||||
In other words, all commands here rely on other URL's to complete their requests"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(pass_context=True, aliases=['g'])
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command(aliases=['g'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def google(self, ctx, *, query: str):
|
||||
"""Searches google for a provided query
|
||||
|
||||
|
@ -27,7 +22,7 @@ class Links:
|
|||
url = "https://www.google.com/search"
|
||||
|
||||
# Turn safe filter on or off, based on whether or not this is a nsfw channel
|
||||
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
|
||||
nsfw = utils.channel_is_nsfw(ctx.message.channel)
|
||||
safe = 'off' if nsfw else 'on'
|
||||
|
||||
params = {'q': query,
|
||||
|
@ -42,7 +37,7 @@ class Links:
|
|||
data = await utils.request(url, payload=params, attr='text')
|
||||
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel, "I failed to connect to google! (That can happen??)")
|
||||
await ctx.send("I failed to connect to google! (That can happen??)")
|
||||
return
|
||||
|
||||
# Convert to a BeautifulSoup element and loop through each result clasified by h3 tags with a class of 'r'
|
||||
|
@ -54,23 +49,23 @@ class Links:
|
|||
try:
|
||||
result_url = re.search('(?<=q=).*(?=&sa=)', element.find('a').get('href')).group(0)
|
||||
except AttributeError:
|
||||
await self.bot.say("I couldn't find any results for {}!".format(query))
|
||||
await ctx.send("I couldn't find any results for {}!".format(query))
|
||||
return
|
||||
|
||||
# Get the next sibling, find the span where the description is, and get the text from this
|
||||
try:
|
||||
description = element.next_sibling.find('span', class_='st').text
|
||||
except:
|
||||
except Exception:
|
||||
description = ""
|
||||
|
||||
# Add this to our text we'll use to send
|
||||
fmt += '\n\n**URL**: <{}>\n**Description**: {}'.format(result_url, description)
|
||||
|
||||
fmt = "**Top 3 results for the query** _{}_:{}".format(query, fmt)
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@commands.command(aliases=['yt'], pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command(aliases=['yt'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def youtube(self, ctx, *, query: str):
|
||||
"""Searches youtube for a provided query
|
||||
|
||||
|
@ -86,13 +81,13 @@ class Links:
|
|||
data = await utils.request(url, payload=params)
|
||||
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to youtube!")
|
||||
await ctx.send("Sorry but I failed to connect to youtube!")
|
||||
return
|
||||
|
||||
try:
|
||||
result = data['items'][0]
|
||||
except IndexError:
|
||||
await self.bot.say("I could not find any results with the search term {}".format(query))
|
||||
await ctx.send("I could not find any results with the search term {}".format(query))
|
||||
return
|
||||
|
||||
result_url = "https://youtube.com/watch?v={}".format(result['id']['videoId'])
|
||||
|
@ -100,10 +95,10 @@ class Links:
|
|||
description = result['snippet']['description']
|
||||
|
||||
fmt = "**Title:** {}\n\n**Description:** {}\n\n**URL:** <{}>".format(title, description, result_url)
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def wiki(self, ctx, *, query: str):
|
||||
"""Pulls the top match for a specific term from wikipedia, and returns the result
|
||||
|
||||
|
@ -119,11 +114,11 @@ class Links:
|
|||
data = await utils.request(base_url, payload=params)
|
||||
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to Wikipedia!")
|
||||
await ctx.send("Sorry but I failed to connect to Wikipedia!")
|
||||
return
|
||||
|
||||
if len(data['query']['search']) == 0:
|
||||
await self.bot.say("I could not find any results with that term, I tried my best :c")
|
||||
await ctx.send("I could not find any results with that term, I tried my best :c")
|
||||
return
|
||||
# Wiki articles' URLs are in the format https://en.wikipedia.org/wiki/[Titlehere]
|
||||
# Replace spaces with %20
|
||||
|
@ -135,148 +130,44 @@ class Links:
|
|||
snippet = re.sub('</span>', '', snippet)
|
||||
snippet = re.sub('"', '"', snippet)
|
||||
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
"Here is the best match I found with the query `{}`:\nURL: <{}>\nSnippet: \n```\n{}```".format(query, url,
|
||||
snippet))
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def urban(self, ctx, *, msg: str):
|
||||
"""Pulls the top urbandictionary.com definition for a term
|
||||
|
||||
EXAMPLE: !urban a normal phrase
|
||||
RESULT: Probably something lewd; this is urban dictionary we're talking about"""
|
||||
url = "http://api.urbandictionary.com/v0/define"
|
||||
params = {"term": msg}
|
||||
try:
|
||||
data = await utils.request(url, payload=params)
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to urban dictionary!")
|
||||
return
|
||||
|
||||
# List is the list of definitions found, if it's empty then nothing was found
|
||||
if len(data['list']) == 0:
|
||||
await self.bot.say("No result with that term!")
|
||||
# If the list is not empty, use the first result and print it's defintion
|
||||
else:
|
||||
await self.bot.say(data['list'][0]['definition'])
|
||||
# Urban dictionary has some long definitions, some might not be able to be sent
|
||||
except discord.HTTPException:
|
||||
await self.bot.say('```\nError: Definition is too long for me to send```')
|
||||
except KeyError:
|
||||
await self.bot.say("Sorry but I failed to connect to urban dictionary!")
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def derpi(self, ctx, *search: str):
|
||||
"""Provides a random image from the first page of derpibooru.org for the following term
|
||||
|
||||
EXAMPLE: !derpi Rainbow Dash
|
||||
RESULT: A picture of Rainbow Dash!"""
|
||||
if len(search) > 0:
|
||||
url = 'https://derpibooru.org/search.json'
|
||||
|
||||
# Ensure a filter was not provided, as we either want to use our own, or none (for safe pics)
|
||||
query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value))
|
||||
params = {'q': query}
|
||||
|
||||
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
|
||||
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms
|
||||
# Also use the custom filter that I have setup, that blocks some certain tags
|
||||
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
|
||||
if nsfw:
|
||||
params['q'] += ", (explicit OR suggestive)"
|
||||
params['filter_id'] = 95938
|
||||
else:
|
||||
params['q'] += ", safe"
|
||||
|
||||
await self.bot.say("Looking up an image with those tags....")
|
||||
|
||||
if utils.channel_is_nsfw(ctx.message.channel):
|
||||
url = "http://api.urbandictionary.com/v0/define"
|
||||
params = {"term": msg}
|
||||
try:
|
||||
# Get the response from derpibooru and parse the 'search' result from it
|
||||
data = await utils.request(url, payload=params)
|
||||
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel, "Sorry but I failed to connect to Derpibooru!")
|
||||
await ctx.send("Sorry but I failed to connect to urban dictionary!")
|
||||
return
|
||||
results = data['search']
|
||||
|
||||
# List is the list of definitions found, if it's empty then nothing was found
|
||||
if len(data['list']) == 0:
|
||||
await ctx.send("No result with that term!")
|
||||
# If the list is not empty, use the first result and print it's defintion
|
||||
else:
|
||||
entries = [x['definition'] for x in data['list']]
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=entries[:5], per_page=1)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
# Urban dictionary has some long definitions, some might not be able to be sent
|
||||
except discord.HTTPException:
|
||||
await ctx.send('```\nError: Definition is too long for me to send```')
|
||||
except KeyError:
|
||||
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
|
||||
return
|
||||
|
||||
# The first request we've made ensures there are results
|
||||
# Now we can get the total count from that, and make another request based on the number of pages as well
|
||||
if len(results) > 0:
|
||||
# Get the total number of pages
|
||||
pages = math.ceil(data['total'] / len(results))
|
||||
# Set a new paramater to set which page to use, randomly based on the number of pages
|
||||
params['page'] = random.SystemRandom().randint(1, pages)
|
||||
data = await utils.request(url, payload=params)
|
||||
if data is None:
|
||||
await self.bot.say("Sorry but I failed to connect to Derpibooru!")
|
||||
return
|
||||
# Now get the results again
|
||||
results = data['search']
|
||||
|
||||
# Get the image link from the now random page'd and random result from that page
|
||||
index = random.SystemRandom().randint(0, len(results) - 1)
|
||||
image_link = 'https://derpibooru.org/{}'.format(results[index]['id'])
|
||||
else:
|
||||
await self.bot.say("No results with that search term, {0}!".format(ctx.message.author.mention))
|
||||
return
|
||||
await ctx.send("Sorry but I failed to connect to urban dictionary!")
|
||||
else:
|
||||
# If no search term was provided, search for a random image
|
||||
# .url will be the URL we end up at, not the one requested.
|
||||
# https://derpibooru.org/images/random redirects to a random image, so this is exactly what we want
|
||||
image_link = await utils.request('https://derpibooru.org/images/random', attr='url')
|
||||
await self.bot.say(image_link)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def e621(self, ctx, *, tags: str):
|
||||
"""Searches for a random image from e621.net
|
||||
Format for the search terms need to be 'search term 1, search term 2, etc.'
|
||||
If the channel the command is ran in, is registered as a nsfw channel, this image will be explicit
|
||||
|
||||
EXAMPLE: !e621 dragon
|
||||
RESULT: A picture of a dragon (hopefully, screw your tagging system e621)"""
|
||||
|
||||
# This changes the formatting for queries, so we don't
|
||||
# Have to use e621's stupid formatting when using the command
|
||||
tags = tags.replace(' ', '_')
|
||||
tags = tags.replace(',_', ' ')
|
||||
|
||||
url = 'https://e621.net/post/index.json'
|
||||
params = {'limit': 320,
|
||||
'tags': tags}
|
||||
# e621 provides a way to change how many images can be shown on one request
|
||||
# This gives more of a chance of random results, however it causes the lookup to take longer than most
|
||||
# Due to this, send a message saying we're looking up the information first
|
||||
await self.bot.say("Looking up an image with those tags....")
|
||||
|
||||
nsfw = await utils.channel_is_nsfw(ctx.message.channel)
|
||||
|
||||
# e621 by default does not filter explicit content, so tack on
|
||||
# safe/explicit based on if this channel is nsfw or not
|
||||
params['tags'] += " rating:explicit" if nsfw else " rating:safe"
|
||||
|
||||
data = await utils.request(url, payload=params)
|
||||
|
||||
if data is None:
|
||||
await self.bot.send_message(ctx.message.channel,
|
||||
"Sorry, I had trouble connecting at the moment; please try again later")
|
||||
return
|
||||
|
||||
# Try to find an image from the list. If there were no results, we're going to attempt to find
|
||||
# A number between (0,-1) and receive an error.
|
||||
# The response should be in a list format, so we'll end up getting a key error if the response was in json
|
||||
# i.e. it responded with a 404/504/etc.
|
||||
try:
|
||||
rand_image = data[random.SystemRandom().randint(0, len(data) - 1)]['file_url']
|
||||
await self.bot.say(rand_image)
|
||||
except (ValueError, KeyError):
|
||||
await self.bot.say("No results with that tag {}".format(ctx.message.author.mention))
|
||||
return
|
||||
await ctx.send("This command is limited to nsfw channels")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
405
cogs/misc.py
Normal file
|
@ -0,0 +1,405 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import utils
|
||||
|
||||
import random
|
||||
import re
|
||||
import calendar
|
||||
import pendulum
|
||||
import datetime
|
||||
import psutil
|
||||
|
||||
|
||||
def _command_signature(cmd):
|
||||
result = [cmd.qualified_name]
|
||||
if cmd.usage:
|
||||
result.append(cmd.usage)
|
||||
return ' '.join(result)
|
||||
|
||||
params = cmd.clean_params
|
||||
if not params:
|
||||
return ' '.join(result)
|
||||
|
||||
for name, param in params.items():
|
||||
if param.default is not param.empty:
|
||||
# We don't want None or '' to trigger the [name=value] case and instead it should
|
||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
||||
should_print = param.default if isinstance(param.default, str) else param.default is not None
|
||||
if should_print:
|
||||
result.append(f'[{name}={param.default!r}]')
|
||||
else:
|
||||
result.append(f'[{name}]')
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
result.append(f'[{name}...]')
|
||||
else:
|
||||
result.append(f'<{name}>')
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
class Miscallaneous(commands.Cog):
|
||||
"""Core commands, these are the miscallaneous commands that don't fit into other categories'"""
|
||||
process = psutil.Process()
|
||||
process.cpu_percent()
|
||||
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 3, commands.cooldowns.BucketType.user)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def help(self, ctx, *, command: str = None):
|
||||
"""Shows help about a command or the bot"""
|
||||
|
||||
try:
|
||||
if command is None:
|
||||
p = await utils.HelpPaginator.from_bot(ctx)
|
||||
else:
|
||||
entity = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
|
||||
|
||||
if entity is None:
|
||||
clean = command.replace('@', '@\u200b')
|
||||
return await ctx.send(f'Command or category "{clean}" not found.')
|
||||
elif isinstance(entity, commands.Command):
|
||||
p = await utils.HelpPaginator.from_command(ctx, entity)
|
||||
else:
|
||||
p = await utils.HelpPaginator.from_cog(ctx, entity)
|
||||
|
||||
await p.paginate()
|
||||
except Exception as e:
|
||||
await ctx.send(e)
|
||||
|
||||
async def _help(self, ctx, *, entity: str = None):
|
||||
chunks = []
|
||||
|
||||
if entity:
|
||||
entity = ctx.bot.get_cog(entity) or ctx.bot.get_command(entity)
|
||||
if entity is None:
|
||||
fmt = "Hello! Here is a list of the sections of commands that I have " \
|
||||
"(there are a lot of commands so just start with the sections...I know, I'm pretty great)\n"
|
||||
fmt += "To use a command's paramaters, you need to know the notation for them:\n"
|
||||
fmt += "\t<argument> This means the argument is __**required**__.\n"
|
||||
fmt += "\t[argument] This means the argument is __**optional**__.\n"
|
||||
fmt += "\t[A|B] This means the it can be __**either A or B**__.\n"
|
||||
fmt += "\t[argument...] This means you can have multiple arguments.\n"
|
||||
fmt += "\n**Type `{}help section` to get help on a specific section**\n".format(ctx.prefix)
|
||||
fmt += "**CASE MATTERS** Sections are in `Title Case` and commands are in `lower case`\n\n"
|
||||
|
||||
chunks.append(fmt)
|
||||
|
||||
cogs = sorted(ctx.bot.cogs.values(), key=lambda c: c.__class__.__name__)
|
||||
for cog in cogs:
|
||||
tmp = "**{}**\n".format(cog.__class__.__name__)
|
||||
if cog.__doc__:
|
||||
tmp += "\t{}\n".format(cog.__doc__)
|
||||
if len(chunks[len(chunks) - 1] + tmp) > 2000:
|
||||
chunks.append(tmp)
|
||||
else:
|
||||
chunks[len(chunks) - 1] += tmp
|
||||
elif isinstance(entity, (commands.core.Command, commands.core.Group)):
|
||||
tmp = "**{}**".format(_command_signature(entity))
|
||||
tmp += "\n{}".format(entity.help)
|
||||
chunks.append(tmp)
|
||||
else:
|
||||
cmds = sorted(entity.get_commands(), key=lambda c: c.name)
|
||||
fmt = "Here are a list of commands under the section {}\n".format(entity.__class__.__name__)
|
||||
fmt += "Type `{}help command` to get more help on a specific command\n\n".format(ctx.prefix)
|
||||
|
||||
chunks.append(fmt)
|
||||
|
||||
for command in cmds:
|
||||
for subcommand in command.walk_commands():
|
||||
tmp = "**{}**\n\t{}\n".format(subcommand.qualified_name, subcommand.short_doc)
|
||||
if len(chunks[len(chunks) - 1] + tmp) > 2000:
|
||||
chunks.append(tmp)
|
||||
else:
|
||||
chunks[len(chunks) - 1] += tmp
|
||||
|
||||
if utils.dev_server:
|
||||
tmp = "\n\nIf I'm having issues, then please visit the dev server and ask for help. {}".format(
|
||||
utils.dev_server)
|
||||
if len(chunks[len(chunks) - 1] + tmp) > 2000:
|
||||
chunks.append(tmp)
|
||||
else:
|
||||
chunks[len(chunks) - 1] += tmp
|
||||
|
||||
if len(chunks) == 1 and len(chunks[0]) < 1000:
|
||||
destination = ctx.channel
|
||||
else:
|
||||
destination = ctx.author
|
||||
|
||||
try:
|
||||
for chunk in chunks:
|
||||
await destination.send(chunk)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
await ctx.send("I cannot DM you, please allow DM's from this server to run this command")
|
||||
else:
|
||||
if ctx.guild and destination == ctx.author:
|
||||
await ctx.send("I have just DM'd you some information about me!")
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def ping(self, ctx):
|
||||
"""Returns the latency between the server websocket, and between reading messages"""
|
||||
msg_latency = datetime.datetime.utcnow() - ctx.message.created_at
|
||||
fmt = "Message latency {0:.2f} seconds".format(msg_latency.seconds + msg_latency.microseconds / 1000000)
|
||||
fmt += "\nWebsocket latency {0:.2f} seconds".format(ctx.bot.latency)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@commands.command(aliases=["coin"])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def coinflip(self, ctx):
|
||||
"""Flips a coin and responds with either heads or tails
|
||||
|
||||
EXAMPLE: !coinflip
|
||||
RESULT: Heads!"""
|
||||
|
||||
result = "Heads!" if random.SystemRandom().randint(0, 1) else "Tails!"
|
||||
await ctx.send(result)
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def say(self, ctx, *, msg: str):
|
||||
"""Tells the bot to repeat what you say
|
||||
|
||||
EXAMPLE: !say I really like orange juice
|
||||
RESULT: I really like orange juice"""
|
||||
fmt = "\u200B{}".format(msg)
|
||||
await ctx.send(fmt)
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def calendar(self, ctx, month: str = None, year: int = None):
|
||||
"""Provides a printout of the current month's calendar
|
||||
Provide month and year to print the calendar of that year and month
|
||||
|
||||
EXAMPLE: !calendar january 2011"""
|
||||
|
||||
# calendar takes in a number for the month, not the words
|
||||
# so we need this dictionary to transform the word to the number
|
||||
months = {
|
||||
"january": 1,
|
||||
"february": 2,
|
||||
"march": 3,
|
||||
"april": 4,
|
||||
"may": 5,
|
||||
"june": 6,
|
||||
"july": 7,
|
||||
"august": 8,
|
||||
"september": 9,
|
||||
"october": 10,
|
||||
"november": 11,
|
||||
"december": 12
|
||||
}
|
||||
# In month was not passed, use the current month
|
||||
if month is None:
|
||||
month = datetime.date.today().month
|
||||
else:
|
||||
month = months.get(month.lower())
|
||||
if month is None:
|
||||
await ctx.send("Please provide a valid Month!")
|
||||
return
|
||||
# If year was not passed, use the current year
|
||||
if year is None:
|
||||
year = datetime.datetime.today().year
|
||||
# Here we create the actual "text" calendar that we are printing
|
||||
cal = calendar.TextCalendar().formatmonth(year, month)
|
||||
await ctx.send("```\n{}```".format(cal))
|
||||
|
||||
@commands.command(aliases=['about'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def info(self, ctx):
|
||||
"""This command can be used to print out some of my information"""
|
||||
# fmt is a dictionary so we can set the key to it's output, then print both
|
||||
# The only real use of doing it this way is easier editing if the info
|
||||
# in this command is changed
|
||||
|
||||
# Create the original embed object
|
||||
# Set the description include dev server (should be required) and the optional patreon link
|
||||
description = "[Dev Server]({})".format(utils.dev_server)
|
||||
if utils.patreon_link:
|
||||
description += "\n[Patreon]({})".format(utils.patreon_link)
|
||||
# Now creat the object
|
||||
opts = {'title': 'Bonfire',
|
||||
'description': description,
|
||||
'colour': discord.Colour.green()}
|
||||
|
||||
# Set the owner
|
||||
embed = discord.Embed(**opts)
|
||||
if hasattr(ctx.bot, 'owner'):
|
||||
embed.set_author(name=str(ctx.bot.owner), icon_url=ctx.bot.owner.avatar_url)
|
||||
|
||||
# Setup the process statistics
|
||||
name = "Process statistics"
|
||||
value = ""
|
||||
|
||||
memory_usage = self.process.memory_full_info().uss / 1024 ** 2
|
||||
cpu_usage = self.process.cpu_percent() / psutil.cpu_count()
|
||||
value += 'Memory: {:.2f} MiB'.format(memory_usage)
|
||||
value += '\nCPU: {}%'.format(cpu_usage)
|
||||
if hasattr(ctx.bot, 'uptime'):
|
||||
value += "\nUptime: {}".format((pendulum.now(tz="UTC") - ctx.bot.uptime).in_words())
|
||||
embed.add_field(name=name, value=value, inline=False)
|
||||
|
||||
# Setup the user and guild statistics
|
||||
name = "User/Guild statistics"
|
||||
value = ""
|
||||
|
||||
value += "Channels: {}".format(len(list(ctx.bot.get_all_channels())))
|
||||
value += "\nUsers: {}".format(len(ctx.bot.users))
|
||||
value += "\nServers: {}".format(len(ctx.bot.guilds))
|
||||
embed.add_field(name=name, value=value, inline=False)
|
||||
|
||||
# The game statistics
|
||||
name = "Game statistics"
|
||||
# To get the newlines right, since we're not sure what will and won't be included
|
||||
# Lets make this one a list and join it at the end
|
||||
value = []
|
||||
|
||||
hm = ctx.bot.get_cog('Hangman')
|
||||
ttt = ctx.bot.get_cog('TicTacToe')
|
||||
bj = ctx.bot.get_cog('Blackjack')
|
||||
interaction = ctx.bot.get_cog('Interaction')
|
||||
|
||||
if hm:
|
||||
value.append("Hangman games: {}".format(len(hm.games)))
|
||||
if ttt:
|
||||
value.append("TicTacToe games: {}".format(len(ttt.boards)))
|
||||
if bj:
|
||||
value.append("Blackjack games: {}".format(len(bj.games)))
|
||||
if interaction:
|
||||
count_battles = 0
|
||||
for battles in ctx.bot.get_cog('Interaction').battles.values():
|
||||
count_battles += len(battles)
|
||||
value.append("Battles running: {}".format(len(bj.games)))
|
||||
embed.add_field(name=name, value="\n".join(value), inline=False)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def uptime(self, ctx):
|
||||
"""Provides a printout of the current bot's uptime
|
||||
|
||||
EXAMPLE: !uptime
|
||||
RESULT: A BAJILLION DAYS"""
|
||||
if hasattr(ctx.bot, 'uptime'):
|
||||
await ctx.send("Uptime: ```\n{}```".format((pendulum.now(tz="UTC") - ctx.bot.uptime).in_words()))
|
||||
else:
|
||||
await ctx.send("I've just restarted and not quite ready yet...gimme time I'm not a morning pony :c")
|
||||
|
||||
@commands.command(aliases=['invite'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def addbot(self, ctx):
|
||||
"""Provides a link that you can use to add me to a server
|
||||
|
||||
EXAMPLE: !addbot
|
||||
RESULT: http://discord.gg/yo_mama"""
|
||||
perms = discord.Permissions.none()
|
||||
perms.read_messages = True
|
||||
perms.send_messages = True
|
||||
perms.manage_roles = True
|
||||
perms.ban_members = True
|
||||
perms.kick_members = True
|
||||
perms.manage_messages = True
|
||||
perms.embed_links = True
|
||||
perms.read_message_history = True
|
||||
perms.attach_files = True
|
||||
perms.speak = True
|
||||
perms.connect = True
|
||||
perms.attach_files = True
|
||||
perms.add_reactions = True
|
||||
app_info = await ctx.bot.application_info()
|
||||
await ctx.send("Use this URL to add me to a server that you'd like!\n<{}>"
|
||||
.format(discord.utils.oauth_url(app_info.id, perms)))
|
||||
|
||||
@commands.command(enabled=False)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def joke(self, ctx):
|
||||
"""Prints a random riddle
|
||||
|
||||
EXAMPLE: !joke
|
||||
RESULT: An absolutely terrible joke."""
|
||||
# Currently disabled until I can find a free API
|
||||
pass
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def roll(self, ctx, *, notation: str = "d6"):
|
||||
"""Rolls a die based on the notation given
|
||||
Format should be #d#
|
||||
|
||||
EXAMPLE: !roll d50
|
||||
RESULT: 51 :^)"""
|
||||
# Use regex to get the notation based on what was provided
|
||||
try:
|
||||
# We do not want to try to convert the dice, because we want d# to
|
||||
# be a valid notation
|
||||
dice = re.search("(\d*)d(\d*)", notation).group(1)
|
||||
num = int(re.search("(\d*)d(\d*)", notation).group(2))
|
||||
# Attempt to get addition/subtraction
|
||||
add = re.search("\+ ?(\d+)", notation)
|
||||
subtract = re.search("- ?(\d+)", notation)
|
||||
# Check if something like ed3 was provided, or something else entirely
|
||||
# was provided
|
||||
except (AttributeError, ValueError):
|
||||
await ctx.send("Please provide the die notation in #d#!")
|
||||
return
|
||||
|
||||
# Dice will be None if d# was provided, assume this means 1d#
|
||||
dice = dice or 1
|
||||
# Since we did not try to convert to int before, do it now after we
|
||||
# have it set
|
||||
dice = int(dice)
|
||||
if dice > 30:
|
||||
await ctx.send("I'm not rolling more than 30 dice, I have tiny hands")
|
||||
return
|
||||
if num > 100:
|
||||
await ctx.send("What die has more than 100 sides? Please, calm down")
|
||||
return
|
||||
if num <= 1:
|
||||
await ctx.send("A {} sided die? You know that's impossible right?".format(num))
|
||||
return
|
||||
|
||||
nums = [random.SystemRandom().randint(1, num) for _ in range(0, int(dice))]
|
||||
subtotal = total = sum(nums)
|
||||
# After totalling, if we have add/subtract seperately, apply them
|
||||
if add:
|
||||
add = int(add.group(1))
|
||||
total += add
|
||||
if subtract:
|
||||
subtract = int(subtract.group(1))
|
||||
total -= subtract
|
||||
value_str = ", ".join("{}".format(x) for x in nums)
|
||||
|
||||
if dice == 1:
|
||||
fmt = '{0.message.author.name} has rolled a {1} sided die and got the number {2}!'.format(
|
||||
ctx, num, value_str
|
||||
)
|
||||
if add or subtract:
|
||||
fmt += "\nTotal: {} ({}".format(total, subtotal)
|
||||
if add:
|
||||
fmt += " + {}".format(add)
|
||||
if subtract:
|
||||
fmt += " - {}".format(subtract)
|
||||
fmt += ")"
|
||||
else:
|
||||
fmt = '{0.message.author.name} has rolled {1}, {2} sided dice and got the numbers {3}!'.format(
|
||||
ctx, dice, num, value_str
|
||||
)
|
||||
if add or subtract:
|
||||
fmt += "\nTotal: {} ({}".format(total, subtotal)
|
||||
if add:
|
||||
fmt += " + {}".format(add)
|
||||
if subtract:
|
||||
fmt += " - {}".format(subtract)
|
||||
fmt += ")"
|
||||
else:
|
||||
fmt += "\nTotal: {}".format(total)
|
||||
await ctx.send(fmt)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Miscallaneous(bot))
|
513
cogs/mod.py
|
@ -1,69 +1,31 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
import utils
|
||||
|
||||
import discord
|
||||
import re
|
||||
import asyncio
|
||||
import rethinkdb as r
|
||||
|
||||
valid_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
|
||||
|
||||
|
||||
class Mod:
|
||||
"""Commands that can be used by a or an admin, depending on the command"""
|
||||
class Moderation(commands.Cog):
|
||||
"""Moderation commands, things that help control a server...but not the settings of the server"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
def find_command(self, command):
|
||||
# This method ensures the command given is valid. We need to loop through commands
|
||||
# As self.bot.commands only includes parent commands
|
||||
# So we are splitting the command in parts, looping through the commands
|
||||
# And getting the subcommand based on the next part
|
||||
# If we try to access commands of a command that isn't a group
|
||||
# We'll hit an AttributeError, meaning an invalid command was given
|
||||
# If we loop through and don't find anything, cmd will still be None
|
||||
# And we'll report an invalid was given as well
|
||||
cmd = None
|
||||
|
||||
for part in command.split():
|
||||
try:
|
||||
if cmd is None:
|
||||
cmd = self.bot.commands.get(part)
|
||||
else:
|
||||
cmd = cmd.commands.get(part)
|
||||
except AttributeError:
|
||||
cmd = None
|
||||
break
|
||||
|
||||
return cmd
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True, aliases=['nick'])
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def nickname(self, ctx, *, name=None):
|
||||
"""Used to set the nickname for Bonfire (provide no nickname and it will reset)
|
||||
|
||||
EXAMPLE: !nick Music Bot
|
||||
RESULT: My nickname is now music bot"""
|
||||
await self.bot.change_nickname(ctx.message.server.me, name)
|
||||
await self.bot.say("\N{OK HAND SIGN}")
|
||||
|
||||
@commands.command(no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def kick(self, member: discord.Member):
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def kick(self, ctx, member: discord.Member, *, reason=None):
|
||||
"""Used to kick a member from this server
|
||||
|
||||
EXAMPLE: !kick @Member
|
||||
RESULT: They're kicked from the server?"""
|
||||
try:
|
||||
await self.bot.kick(member)
|
||||
await self.bot.say("\N{OK HAND SIGN}")
|
||||
await member.kick(reason=reason)
|
||||
await ctx.send("\N{OK HAND SIGN}")
|
||||
except discord.Forbidden:
|
||||
await self.bot.say("But I can't, muh permissions >:c")
|
||||
await ctx.send("But I can't, muh permissions >:c")
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(ban_members=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(ban_members=True)
|
||||
async def unban(self, ctx, member_id: int):
|
||||
"""Used to unban a member from this server
|
||||
Due to the fact that I cannot find a user without being in a server with them
|
||||
|
@ -74,18 +36,18 @@ class Mod:
|
|||
|
||||
# Lets only accept an int for this method, in order to ensure only an ID is provided
|
||||
# Due to that though, we need to ensure a string is passed as the member's ID
|
||||
member = discord.Object(id=str(member_id))
|
||||
try:
|
||||
await self.bot.unban(ctx.message.server, member)
|
||||
await self.bot.say("\N{OK HAND SIGN}")
|
||||
await ctx.bot.http.unban(member_id, ctx.guild.id)
|
||||
await ctx.send("\N{OK HAND SIGN}")
|
||||
except discord.Forbidden:
|
||||
await self.bot.say("But I can't, muh permissions >:c")
|
||||
await ctx.send("But I can't, muh permissions >:c")
|
||||
except discord.HTTPException:
|
||||
await self.bot.say("Sorry, I failed to unban that user!")
|
||||
await ctx.send("Sorry, I failed to unban that user!")
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(ban_members=True)
|
||||
async def ban(self, ctx, *, member):
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(ban_members=True)
|
||||
async def ban(self, ctx, member, *, reason=None):
|
||||
"""Used to ban a member
|
||||
This can be used to ban someone preemptively as well.
|
||||
Provide the ID of the user and this should ban them without them being in the server
|
||||
|
@ -95,310 +57,60 @@ class Mod:
|
|||
|
||||
# Lets first check if a user ID was provided, as that will be the easiest case to ban
|
||||
if member.isdigit():
|
||||
# First convert it to a discord object based on the ID that was given
|
||||
member = discord.Object(id=member)
|
||||
# Next, to ban from the server the API takes a server obejct and uses that ID
|
||||
# So set "this" server as the member's server. This creates the "fake" member we need
|
||||
member.server = ctx.message.server
|
||||
try:
|
||||
await ctx.bot.http.ban(member, ctx.guild.id, reason=reason)
|
||||
await ctx.send("\N{OK HAND SIGN}")
|
||||
except discord.Forbidden:
|
||||
await ctx.send("But I can't, muh permissions >:c")
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Sorry, I failed to ban that user!")
|
||||
finally:
|
||||
return
|
||||
else:
|
||||
# If no ID was provided, lets try to convert what was given using the internal coverter
|
||||
converter = commands.converter.UserConverter(ctx, member)
|
||||
converter = commands.converter.MemberConverter()
|
||||
try:
|
||||
member = converter.convert()
|
||||
member = await converter.convert(ctx, member)
|
||||
except commands.converter.BadArgument:
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
'{} does not appear to be a valid member. If this member is not in this server, please provide '
|
||||
'their ID'.format(member))
|
||||
return
|
||||
# Now lets try actually banning the member we've been given
|
||||
try:
|
||||
await self.bot.ban(member)
|
||||
await self.bot.say("\N{OK HAND SIGN}")
|
||||
except discord.Forbidden:
|
||||
await self.bot.say("But I can't, muh permissions >:c")
|
||||
except discord.HTTPException:
|
||||
await self.bot.say("Sorry, I failed to ban that user!")
|
||||
|
||||
@commands.command(no_pm=True, aliases=['alerts'], pass_context=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def notifications(self, ctx, channel: discord.Channel):
|
||||
"""This command is used to set a channel as the server's 'notifications' channel
|
||||
Any notifications (like someone going live on Twitch, or Picarto) will go to that channel
|
||||
EXAMPLE: !alerts #alerts
|
||||
RESULT: No more alerts spammed in #general!"""
|
||||
if str(channel.type) != "text":
|
||||
await self.bot.say("The notifications channel must be a text channel!")
|
||||
return
|
||||
|
||||
key = ctx.message.server.id
|
||||
entry = {'server_id': key,
|
||||
'notification_channel': channel.id}
|
||||
if not await utils.update_content('server_settings', entry, key):
|
||||
await utils.add_content('server_settings', entry)
|
||||
await self.bot.say("I have just changed this server's 'notifications' channel"
|
||||
"\nAll notifications will now go to `{}`".format(channel))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def usernotify(self, ctx, on_off: str):
|
||||
"""This command can be used to set whether or not you want user notificaitons to show
|
||||
Provide on, yes, or true to set it on; otherwise it will be turned off
|
||||
EXAMPLE: !usernotify on
|
||||
RESULT: Annying join/leave notifications! Yay!"""
|
||||
# Join/Leave notifications can be kept separate from normal alerts
|
||||
# So we base this channel on it's own and not from alerts
|
||||
# When mod logging becomes available, that will be kept to it's own channel if wanted as well
|
||||
on_off = True if re.search("(on|yes|true)", on_off.lower()) else False
|
||||
key = ctx.message.server.id
|
||||
entry = {'server_id': key,
|
||||
'join_leave': on_off}
|
||||
if not await utils.update_content('server_settings', entry, key):
|
||||
await utils.add_content('server_settings', entry)
|
||||
|
||||
fmt = "notify" if on_off else "not notify"
|
||||
await self.bot.say("This server will now {} if someone has joined or left".format(fmt))
|
||||
|
||||
@commands.group(pass_context=True)
|
||||
async def nsfw(self, ctx):
|
||||
"""Handles adding or removing a channel as a nsfw channel"""
|
||||
pass
|
||||
|
||||
@nsfw.command(name="add", pass_context=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def nsfw_add(self, ctx):
|
||||
"""Registers this channel as a 'nsfw' channel
|
||||
EXAMPLE: !nsfw add
|
||||
RESULT: ;)"""
|
||||
key = ctx.message.server.id
|
||||
entry = {'server_id': key,
|
||||
'nsfw_channels': [ctx.message.channel.id]}
|
||||
update = {'nsfw_channels': r.row['nsfw_channels'].append(ctx.message.channel.id)}
|
||||
|
||||
server_settings = await utils.get_content('server_settings', key)
|
||||
if server_settings and 'nsfw_channels' in server_settings.keys():
|
||||
await utils.update_content('server_settings', update, key)
|
||||
elif server_settings:
|
||||
await utils.update_content('server_settings', entry, key)
|
||||
else:
|
||||
await utils.add_content('server_settings', entry)
|
||||
|
||||
await self.bot.say("This channel has just been registered as 'nsfw'! Have fun you naughties ;)")
|
||||
|
||||
@nsfw.command(name="remove", aliases=["delete"], pass_context=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def nsfw_remove(self, ctx):
|
||||
"""Removes this channel as a 'nsfw' channel
|
||||
EXAMPLE: !nsfw remove
|
||||
RESULT: ;("""
|
||||
|
||||
key = ctx.message.server.id
|
||||
server_settings = await utils.get_content('server_settings', key)
|
||||
channel = ctx.message.channel.id
|
||||
try:
|
||||
channels = server_settings['nsfw_channels']
|
||||
if channel in channels:
|
||||
channels.remove(channel)
|
||||
|
||||
entry = {'nsfw_channels': channels}
|
||||
await utils.update_content('server_settings', entry, key)
|
||||
await self.bot.say("This channel has just been unregistered as a nsfw channel")
|
||||
return
|
||||
except (TypeError, IndexError):
|
||||
pass
|
||||
|
||||
await self.bot.say("This channel is not registered as a 'nsfw' channel!")
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def say(self, ctx, *, msg: str):
|
||||
"""Tells the bot to repeat what you say
|
||||
|
||||
EXAMPLE: !say I really like orange juice
|
||||
RESULT: I really like orange juice"""
|
||||
fmt = "\u200B{}".format(msg)
|
||||
await self.bot.say(fmt)
|
||||
try:
|
||||
await self.bot.delete_message(ctx.message)
|
||||
except:
|
||||
pass
|
||||
|
||||
@commands.group(pass_context=True, invoke_without_command=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def perms(self, ctx, *, command: str = None):
|
||||
"""This command can be used to print the current allowed permissions on a specific command
|
||||
This supports groups as well as subcommands; pass no argument to print a list of available permissions
|
||||
EXAMPLE: !perms help RESULT: Hopefully a result saying you just need send_messages permissions; otherwise lol
|
||||
this server's admin doesn't like me """
|
||||
if command is None:
|
||||
await self.bot.say(
|
||||
"Valid permissions are: ```\n{}```".format("\n".join("{}".format(i) for i in valid_perms)))
|
||||
return
|
||||
|
||||
cmd = utils.find_command(self.bot, command)
|
||||
|
||||
if cmd is None:
|
||||
await self.bot.say("That is not a valid command!")
|
||||
return
|
||||
|
||||
server_settings = await utils.get_content('server_settings', ctx.message.server.id)
|
||||
try:
|
||||
server_perms = server_settings['permissions']
|
||||
except (TypeError, IndexError, KeyError):
|
||||
server_perms = {}
|
||||
|
||||
perms_value = server_perms.get(cmd.qualified_name)
|
||||
if perms_value is None:
|
||||
# If we don't find custom permissions, get the required permission for a command
|
||||
# based on what we set in utils.custom_perms, if custom_perms isn't found, we'll get an IndexError
|
||||
# Now lets try actually banning the member we've been given
|
||||
try:
|
||||
custom_perms = [func for func in cmd.checks if "custom_perms" in func.__qualname__][0]
|
||||
except IndexError:
|
||||
# Loop through and check if there is a check called is_owner
|
||||
# If we loop through and don't find one, this means that the only other choice is to be
|
||||
# Able to manage the server (for the utils on perm commands)
|
||||
for func in cmd.checks:
|
||||
if "is_owner" in func.__qualname__:
|
||||
await self.bot.say("You need to own the bot to run this command")
|
||||
return
|
||||
await self.bot.say(
|
||||
"You are required to have `manage_server` permissions to run `{}`".format(cmd.qualified_name))
|
||||
return
|
||||
await member.ban(reason=reason)
|
||||
await ctx.send("\N{OK HAND SIGN}")
|
||||
except discord.Forbidden:
|
||||
await ctx.send("But I can't, muh permissions >:c")
|
||||
except discord.HTTPException:
|
||||
await ctx.send("Sorry, I failed to ban that user!")
|
||||
|
||||
# Perms will be an attribute if custom_perms is found no matter what, so no need to check this
|
||||
perms = "\n".join(attribute for attribute, setting in custom_perms.perms.items() if setting)
|
||||
await self.bot.say(
|
||||
"You are required to have `{}` permissions to run `{}`".format(perms, cmd.qualified_name))
|
||||
else:
|
||||
# Permissions are saved as bit values, so create an object based on that value
|
||||
# Then check which permission is true, that is our required permission
|
||||
# There's no need to check for errors here, as we ensure a permission is valid when adding it
|
||||
permissions = discord.Permissions(perms_value)
|
||||
needed_perm = [perm[0] for perm in permissions if perm[1]][0]
|
||||
await self.bot.say("You need to have the permission `{}` "
|
||||
"to use the command `{}` in this server".format(needed_perm, command))
|
||||
|
||||
@perms.command(name="add", aliases=["setup,create"], pass_context=True, no_pm=True)
|
||||
@commands.has_permissions(manage_server=True)
|
||||
async def add_perms(self, ctx, *msg: str):
|
||||
"""Sets up custom permissions on the provided command
|
||||
Format must be 'perms add <command> <permission>'
|
||||
If you want to open the command to everyone, provide 'none' as the permission
|
||||
EXAMPLE: !perms add skip ban_members
|
||||
RESULT: No more random people voting to skip a song"""
|
||||
|
||||
# Since subcommands exist, base the last word in the list as the permission, and the rest of it as the command
|
||||
command = " ".join(msg[0:len(msg) - 1])
|
||||
try:
|
||||
permissions = msg[len(msg) - 1]
|
||||
except IndexError:
|
||||
await self.bot.say("Please provide the permissions you want to setup, the format for this must be in:\n"
|
||||
"`perms add <command> <permission>`")
|
||||
return
|
||||
|
||||
cmd = utils.find_command(self.bot, command)
|
||||
|
||||
if cmd is None:
|
||||
await self.bot.say(
|
||||
"That command does not exist! You can't have custom permissions on a non-existant command....")
|
||||
return
|
||||
|
||||
# If a user can run a command, they have to have send_messages permissions; so use this as the base
|
||||
if permissions.lower() == "none":
|
||||
permissions = "send_messages"
|
||||
|
||||
# Convert the string to an int value of the permissions object, based on the required permission
|
||||
# If we hit an attribute error, that means the permission given was not correct
|
||||
perm_obj = discord.Permissions.none()
|
||||
try:
|
||||
setattr(perm_obj, permissions, True)
|
||||
except AttributeError:
|
||||
await self.bot.say("{} does not appear to be a valid permission! Valid permissions are: ```\n{}```"
|
||||
.format(permissions, "\n".join(valid_perms)))
|
||||
return
|
||||
perm_value = perm_obj.value
|
||||
|
||||
# Two cases I use should never have custom permissions setup on them, is_owner for obvious reasons
|
||||
# The other case is if I'm using the default has_permissions case
|
||||
# Which means I do not want to check custom permissions at all
|
||||
# Currently the second case is only on adding and removing permissions, to avoid abuse on these
|
||||
for check in cmd.checks:
|
||||
if "is_owner" == check.__name__ or "has_permissions" in str(check):
|
||||
await self.bot.say("This command cannot have custom permissions setup!")
|
||||
return
|
||||
|
||||
key = ctx.message.server.id
|
||||
entry = {'server_id': key,
|
||||
'permissions': {cmd.qualified_name: perm_value}}
|
||||
|
||||
if not await utils.update_content('server_settings', entry, key):
|
||||
await utils.add_content('server_settings', entry)
|
||||
|
||||
await self.bot.say("I have just added your custom permissions; "
|
||||
"you now need to have `{}` permissions to use the command `{}`".format(permissions, command))
|
||||
|
||||
@perms.command(name="remove", aliases=["delete"], pass_context=True, no_pm=True)
|
||||
@commands.has_permissions(manage_server=True)
|
||||
async def remove_perms(self, ctx, *, command: str):
|
||||
"""Removes the custom permissions setup on the command specified
|
||||
|
||||
EXAMPLE: !perms remove play
|
||||
RESULT: Freedom!"""
|
||||
|
||||
cmd = utils.find_command(self.bot, command)
|
||||
|
||||
if cmd is None:
|
||||
await self.bot.say(
|
||||
"That command does not exist! You can't have custom permissions on a non-existant command....")
|
||||
return
|
||||
|
||||
update = {'permissions': {cmd.qualified_name: None}}
|
||||
await utils.update_content('server_settings', update, ctx.message.server.id)
|
||||
await self.bot.say("I have just removed the custom permissions for {}!".format(cmd))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(manage_server=True)
|
||||
async def prefix(self, ctx, *, prefix: str):
|
||||
"""This command can be used to set a custom prefix per server
|
||||
|
||||
EXAMPLE: !prefix new_prefix
|
||||
RESULT: You probably screwing it up and not realizing you now need to do new_prefixprefix"""
|
||||
key = ctx.message.server.id
|
||||
if prefix.lower().strip() == "none":
|
||||
prefix = None
|
||||
|
||||
entry = {'server_id': key,
|
||||
'prefix': prefix}
|
||||
|
||||
if not await utils.update_content('server_settings', entry, key):
|
||||
await utils.add_content('server_settings', entry)
|
||||
|
||||
if prefix is None:
|
||||
fmt = "I have just cleared your custom prefix, the default prefix will have to be used now"
|
||||
else:
|
||||
fmt = "I have just updated the prefix for this server; you now need to call commands with `{0}`. " \
|
||||
"For example, you can call this command again with {0}prefix".format(prefix)
|
||||
await self.bot.say(fmt)
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(manage_messages=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_messages=True)
|
||||
async def purge(self, ctx, limit: int = 100):
|
||||
"""This command is used to a purge a number of messages from the channel
|
||||
|
||||
EXAMPLE: !purge 50
|
||||
RESULT: -50 messages in this channel"""
|
||||
if not ctx.message.channel.permissions_for(ctx.message.server.me).manage_messages:
|
||||
await self.bot.say("I do not have permission to delete messages...")
|
||||
if not ctx.message.channel.permissions_for(ctx.message.guild.me).manage_messages:
|
||||
await ctx.send("I do not have permission to delete messages...")
|
||||
return
|
||||
try:
|
||||
await self.bot.purge_from(ctx.message.channel, limit=limit)
|
||||
await ctx.message.channel.purge(limit=limit, before=ctx.message)
|
||||
await ctx.message.delete()
|
||||
except discord.HTTPException:
|
||||
await self.bot.send_message(ctx.message.channel, "Detected messages that are too far "
|
||||
"back for me to delete; I can only bulk delete messages"
|
||||
" that are under 14 days old.")
|
||||
try:
|
||||
await ctx.message.channel.send("Detected messages that are too far "
|
||||
"back for me to delete; I can only bulk delete messages"
|
||||
" that are under 14 days old.")
|
||||
except:
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(manage_messages=True)
|
||||
async def prune(self, ctx, limit=None):
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_messages=True)
|
||||
async def prune(self, ctx, *specifications):
|
||||
"""This command can be used to prune messages from certain members
|
||||
Mention any user you want to prune messages from; if no members are mentioned, the messages removed will be mine
|
||||
If no limit is provided, then 100 will be used. This is also the max limit we can use
|
||||
|
@ -406,124 +118,59 @@ class Mod:
|
|||
EXAMPLE: !prune 50
|
||||
RESULT: 50 of my messages are removed from this channel"""
|
||||
# We can only get logs from 100 messages at a time, so make sure we are not above that threshold
|
||||
try:
|
||||
# We may not have been passed a limit, and only mentions
|
||||
# If this happens, the limit will be set to that first mention
|
||||
limit = int(limit)
|
||||
except (TypeError, ValueError):
|
||||
limit = 100
|
||||
|
||||
if limit > 100:
|
||||
limit = 100
|
||||
if limit < 0:
|
||||
await self.bot.say("Limit cannot be less than 0!")
|
||||
return
|
||||
limit = 100
|
||||
for x in specifications:
|
||||
try:
|
||||
limit = int(x)
|
||||
if limit <= 100:
|
||||
break
|
||||
else:
|
||||
limit = 100
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
# If no members are provided, assume we're trying to prune our own messages
|
||||
members = ctx.message.mentions
|
||||
roles = ctx.message.role_mentions
|
||||
|
||||
if len(members) == 0:
|
||||
members = [ctx.message.server.me]
|
||||
members = [ctx.message.guild.me]
|
||||
|
||||
# Our check for if a message should be deleted
|
||||
def check(m):
|
||||
if m.author in members:
|
||||
return True
|
||||
if any(x in m.author.roles for x in roles):
|
||||
if any(r in m.author.roles for r in roles):
|
||||
return True
|
||||
return False
|
||||
|
||||
# If we're not setting the user to the bot, then we're deleting someone elses messages
|
||||
# To do so, we need manage_messages permission, so check if we have that
|
||||
if not ctx.message.channel.permissions_for(ctx.message.server.me).manage_messages:
|
||||
await self.bot.say("I do not have permission to delete messages...")
|
||||
if not ctx.message.channel.permissions_for(ctx.message.guild.me).manage_messages:
|
||||
await ctx.send("I do not have permission to delete messages...")
|
||||
return
|
||||
|
||||
# Since logs_from will give us any message, not just the user's we need
|
||||
# We'll increment count, and stop deleting messages if we hit the limit.
|
||||
count = 0
|
||||
async for msg in self.bot.logs_from(ctx.message.channel, before=ctx.message):
|
||||
async for msg in ctx.message.channel.history(before=ctx.message):
|
||||
if check(msg):
|
||||
try:
|
||||
await self.bot.delete_message(msg)
|
||||
await msg.delete()
|
||||
count += 1
|
||||
except:
|
||||
pass
|
||||
if count >= limit:
|
||||
break
|
||||
msg = await self.bot.say("{} messages succesfully deleted".format(count))
|
||||
|
||||
msg = await ctx.send("{} messages succesfully deleted".format(count))
|
||||
await asyncio.sleep(5)
|
||||
try:
|
||||
await self.bot.delete_message(msg)
|
||||
await self.bot.delete_message(ctx.message)
|
||||
except discord.NotFound:
|
||||
await msg.delete()
|
||||
await ctx.message.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
@commands.group(aliases=['rule'], pass_context=True, no_pm=True, invoke_without_command=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def rules(self, ctx, rule: int = None):
|
||||
"""This command can be used to view the current rules on the server
|
||||
|
||||
EXAMPLE: !rules 5
|
||||
RESULT: Rule 5 is printed"""
|
||||
server_settings = await utils.get_content('server_settings', ctx.message.server.id)
|
||||
rules = server_settings.get('rules')
|
||||
|
||||
if not rules or len(rules) == 0:
|
||||
await self.bot.say("This server currently has no rules on it! I see you like to live dangerously...")
|
||||
return
|
||||
|
||||
if rule is None:
|
||||
try:
|
||||
pages = utils.Pages(self.bot, message=ctx.message, entries=rules, per_page=5)
|
||||
pages.title = "Rules for {}".format(ctx.message.server.name)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await self.bot.say(str(e))
|
||||
else:
|
||||
try:
|
||||
fmt = rules[rule - 1]
|
||||
except IndexError:
|
||||
await self.bot.say("That rules does not exist.")
|
||||
return
|
||||
await self.bot.say("Rule {}: \"{}\"".format(rule, fmt))
|
||||
|
||||
@rules.command(name='add', aliases=['create'], pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(manage_server=True)
|
||||
async def rules_add(self, ctx, *, rule: str):
|
||||
"""Adds a rule to this server's rules
|
||||
|
||||
EXAMPLE: !rules add No fun allowed in this server >:c
|
||||
RESULT: No more fun...unless they break the rules!"""
|
||||
key = ctx.message.server.id
|
||||
entry = {'server_id': key,
|
||||
'rules': [rule]}
|
||||
update = {'rules': r.row['rules'].append(rule)}
|
||||
|
||||
server_settings = await utils.get_content('server_settings', key)
|
||||
if server_settings and 'rules' in server_settings.keys():
|
||||
await utils.update_content('server_settings', update, key)
|
||||
elif server_settings:
|
||||
await utils.update_content('server_settings', entry, key)
|
||||
else:
|
||||
await utils.add_content('server_settings', entry)
|
||||
|
||||
await self.bot.say("I have just saved your new rule, use the rules command to view this server's current rules")
|
||||
|
||||
@rules.command(name='remove', aliases=['delete'], pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(manage_server=True)
|
||||
async def rules_delete(self, ctx, rule: int):
|
||||
"""Removes one of the rules from the list of this server's rules
|
||||
Provide a number to delete that rule
|
||||
|
||||
EXAMPLE: !rules delete 5
|
||||
RESULT: Freedom from opression!"""
|
||||
update = {'rules': r.row['rules'].delete_at(rule - 1)}
|
||||
if not await utils.update_content('server_settings', update, ctx.message.server.id):
|
||||
await self.bot.say("That is not a valid rule number, try running the command again.")
|
||||
else:
|
||||
await self.bot.say("I have just removed that rule from your list of rules!")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Mod(bot))
|
||||
bot.add_cog(Moderation(bot))
|
||||
|
|
703
cogs/music.py
|
@ -1,703 +0,0 @@
|
|||
from .voice_utilities import *
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
import math
|
||||
import time
|
||||
import asyncio
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
import socket
|
||||
import inspect
|
||||
|
||||
if not discord.opus.is_loaded():
|
||||
discord.opus.load_opus('/usr/lib64/libopus.so.0')
|
||||
|
||||
|
||||
class VoiceState:
|
||||
def __init__(self, bot, download):
|
||||
self.current = None
|
||||
self.voice = None
|
||||
self.bot = bot
|
||||
self.play_next_song = asyncio.Event()
|
||||
# This is the queue that holds all VoiceEntry's
|
||||
self.songs = Playlist(bot)
|
||||
self.required_skips = 0
|
||||
# a set of user_ids that voted
|
||||
self.skip_votes = set()
|
||||
# Our actual task that handles the queue system
|
||||
self.audio_player = self.bot.loop.create_task(self.audio_player_task())
|
||||
self.opts = {
|
||||
'default_search': 'auto',
|
||||
'quiet': True
|
||||
}
|
||||
self.volume = 50
|
||||
self.downloader = download
|
||||
self.file_names = []
|
||||
|
||||
def is_playing(self):
|
||||
# If our VoiceClient or current VoiceEntry do not exist, then we are not playing a song
|
||||
if self.voice is None or self.current is None:
|
||||
return False
|
||||
|
||||
# If they do exist, check if the current player has finished
|
||||
player = self.current.player
|
||||
try:
|
||||
return not player.is_done()
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def player(self):
|
||||
return self.current.player
|
||||
|
||||
def skip(self):
|
||||
# Make sure we clear the votes, before stopping the player
|
||||
# When the player is stopped, our toggle_next method is called, so the next song can be played
|
||||
self.skip_votes.clear()
|
||||
if self.is_playing():
|
||||
self.player.stop()
|
||||
|
||||
def toggle_next(self):
|
||||
# Set the Event so that the next song in the queue can be played
|
||||
self.bot.loop.call_soon_threadsafe(self.play_next_song.set)
|
||||
|
||||
async def audio_player_task(self):
|
||||
while True:
|
||||
# At the start of our task, clear the Event, so we can wait till it is next set
|
||||
self.play_next_song.clear()
|
||||
# Clear the votes skip that were for the last song
|
||||
self.skip_votes.clear()
|
||||
# Now wait for the next song in the queue
|
||||
self.current = await self.songs.get_next_entry()
|
||||
|
||||
# Make sure we find a song
|
||||
while self.current is None:
|
||||
self.clear_audio_files()
|
||||
await asyncio.sleep(1)
|
||||
self.current = await self.songs.get_next_entry()
|
||||
|
||||
# At this point we're sure we have a song, however it needs to be downloaded
|
||||
while not getattr(self.current, 'filename'):
|
||||
print("Downloading...")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Now add this file to our list of filenames, so that it can be deleted later
|
||||
if self.current.filename not in self.file_names:
|
||||
self.file_names.append(self.current.filename)
|
||||
|
||||
# Create the player object
|
||||
self.current.player = self.voice.create_ffmpeg_player(
|
||||
self.current.filename,
|
||||
before_options="-nostdin",
|
||||
options="-vn -b:a 128k",
|
||||
after=self.toggle_next
|
||||
)
|
||||
|
||||
# Now we can start actually playing the song
|
||||
self.current.player.start()
|
||||
self.current.player.volume = self.volume / 100
|
||||
|
||||
# Save the variable for when our time for this song has started
|
||||
self.current.start_time = time.time()
|
||||
|
||||
# Wait till the Event has been set, before doing our task again
|
||||
await self.play_next_song.wait()
|
||||
|
||||
def clear_audio_files(self):
|
||||
"""Deletes all the audio files this guild has created"""
|
||||
for f in self.file_names:
|
||||
os.remove(f)
|
||||
self.file_names = []
|
||||
|
||||
class Music:
|
||||
"""Voice related commands.
|
||||
Works in multiple servers at once.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.voice_states = {}
|
||||
down = Downloader(download_folder='audio_tmp')
|
||||
self.downloader = down
|
||||
self.bot.downloader = down
|
||||
self.clear_audio_tmp()
|
||||
|
||||
def clear_audio_tmp(self):
|
||||
files = glob.glob('audio_tmp/*')
|
||||
for f in files:
|
||||
os.remove(f)
|
||||
|
||||
def get_voice_state(self, server):
|
||||
state = self.voice_states.get(server.id)
|
||||
|
||||
# Internally handle creating a voice state if there isn't a current state
|
||||
# This can be used for example, in case something is skipped when not being connected
|
||||
# We create the voice state when checked
|
||||
# This only creates the state, we are still not playing anything, which can then be handled separately
|
||||
if state is None:
|
||||
state = VoiceState(self.bot, self.downloader)
|
||||
self.voice_states[server.id] = state
|
||||
|
||||
return state
|
||||
|
||||
async def create_voice_client(self, channel):
|
||||
"""Creates a voice client and saves it"""
|
||||
# First join the channel and get the VoiceClient that we'll use to save per server
|
||||
await self.remove_voice_client(channel.server)
|
||||
|
||||
server = channel.server
|
||||
state = self.get_voice_state(server)
|
||||
voice = self.bot.voice_client_in(server)
|
||||
# Attempt 3 times
|
||||
for i in range(3):
|
||||
try:
|
||||
if voice is None:
|
||||
state.voice = await self.bot.join_voice_channel(channel)
|
||||
if state.voice:
|
||||
return True
|
||||
elif voice.channel == channel:
|
||||
state.voice = voice
|
||||
return True
|
||||
else:
|
||||
# This shouldn't theoretically ever happen yet it does. Thanks Discord
|
||||
await voice.disconnect()
|
||||
state.voice = await self.bot.join_voice_channel(channel)
|
||||
if state.voice:
|
||||
return True
|
||||
except (discord.ClientException, socket.gaierror, ConnectionResetError):
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def remove_voice_client(self, server):
|
||||
"""Removes any voice clients from a server
|
||||
This is sometimes needed, due to the unreliability of Discord's voice connection
|
||||
We do not want to end up with a voice client stuck somewhere, so this cancels any found for a server"""
|
||||
state = self.get_voice_state(server)
|
||||
voice = self.bot.voice_client_in(server)
|
||||
|
||||
if voice:
|
||||
await voice.disconnect()
|
||||
if state.voice:
|
||||
await state.voice.disconnect()
|
||||
|
||||
def __unload(self):
|
||||
# If this is unloaded, cancel all players and disconnect from all channels
|
||||
for state in self.voice_states.values():
|
||||
try:
|
||||
state.audio_player.cancel()
|
||||
if state.voice:
|
||||
self.bot.loop.create_task(state.voice.disconnect())
|
||||
except:
|
||||
pass
|
||||
|
||||
async def on_voice_state_update(self, before, after):
|
||||
state = self.get_voice_state(after.server)
|
||||
if state.voice is None:
|
||||
return
|
||||
voice_channel = state.voice.channel
|
||||
num_members = len(voice_channel.voice_members)
|
||||
state.required_skips = math.ceil((num_members + 1) / 3)
|
||||
|
||||
async def queue_embed_task(self, state, channel, author):
|
||||
index = 0
|
||||
message = None
|
||||
fmt = None
|
||||
# Our check to ensure the only one who reacts is the bot
|
||||
def check(reaction, user):
|
||||
return user == author
|
||||
possible_reactions = ['\u27A1', '\u2B05', '\u2b06', '\u2b07', '\u274c']
|
||||
while True:
|
||||
# Get the current queue (It might change while we're doing this)
|
||||
# So do this in the while loop
|
||||
queue = state.songs.entries
|
||||
count = len(queue)
|
||||
# This means the last song was removed
|
||||
if count == 0:
|
||||
await self.bot.send_message(channel, "Nothing currently in the queue")
|
||||
break
|
||||
# Get the current entry
|
||||
entry = queue[index]
|
||||
# Get the entry's embed
|
||||
embed = entry.to_embed()
|
||||
# Set the embed's title to indicate the amount of things in the queue
|
||||
count = len(queue)
|
||||
embed.title = "Current Queue [{}/{}]".format(index+1, count)
|
||||
# Now we need to send the embed, so check if the message is already set
|
||||
# If not, then we need to send a new one (i.e. this is the first time called)
|
||||
if message:
|
||||
message = await self.bot.edit_message(message, fmt, embed=embed)
|
||||
# There's only one reaction we want to make sure we remove in the circumstances
|
||||
# If the member doesn't have kick_members permissions, and isn't the requester
|
||||
# Then they can't remove the song, otherwise they can
|
||||
if not author.server_permissions.kick_members and author != entry.requester:
|
||||
try:
|
||||
await self.bot.remove_reaction(message, '\u274c', channel.server.me)
|
||||
except:
|
||||
pass
|
||||
elif not author.server_permissions.kick_members and author == entry.requester:
|
||||
try:
|
||||
await self.bot.add_reaction(message, '\u274c')
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
message = await self.bot.send_message(channel, embed=embed)
|
||||
await self.bot.add_reaction(message, '\N{LEFTWARDS BLACK ARROW}')
|
||||
await self.bot.add_reaction(message, '\N{BLACK RIGHTWARDS ARROW}')
|
||||
# The moderation tools that can be used
|
||||
if author.server_permissions.kick_members:
|
||||
await self.bot.add_reaction(message, '\N{DOWNWARDS BLACK ARROW}')
|
||||
await self.bot.add_reaction(message, '\N{UPWARDS BLACK ARROW}')
|
||||
await self.bot.add_reaction(message, '\N{CROSS MARK}')
|
||||
elif author == entry.requester:
|
||||
await self.bot.add_reaction(message, '\N{CROSS MARK}')
|
||||
# Reset the fmt message
|
||||
fmt = None
|
||||
# Now we wait for the next reaction
|
||||
res = await self.bot.wait_for_reaction(possible_reactions, message=message, check=check, timeout=180)
|
||||
if res is None:
|
||||
break
|
||||
else:
|
||||
reaction, user = res
|
||||
# Now we can prepare for the next embed to be sent
|
||||
# If right is clicked
|
||||
if '\u27A1' in reaction.emoji:
|
||||
index += 1
|
||||
if index >= count:
|
||||
index = 0
|
||||
# If left is clicked
|
||||
elif '\u2B05' in reaction.emoji:
|
||||
index -= 1
|
||||
if index < 0:
|
||||
index = count - 1
|
||||
# If up is clicked
|
||||
elif '\u2b06' in reaction.emoji:
|
||||
# A second check just to make sure, as well as ensuring index is higher than 0
|
||||
if author.server_permissions.kick_members and index > 0:
|
||||
if entry != queue[index]:
|
||||
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
||||
else:
|
||||
# Remove the current entry
|
||||
del queue[index]
|
||||
# Add it one position higher
|
||||
queue.insert(index - 1, entry)
|
||||
# Lets move the index to look at the new place of the entry
|
||||
index -= 1
|
||||
# If down is clicked
|
||||
elif '\u2b07' in reaction.emoji:
|
||||
# A second check just to make sure, as well as ensuring index is lower than last
|
||||
if author.server_permissions.kick_members and index < (count - 1):
|
||||
if entry != queue[index]:
|
||||
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
||||
else:
|
||||
# Remove the current entry
|
||||
del queue[index]
|
||||
# Add it one position lower
|
||||
queue.insert(index + 1, entry)
|
||||
# Lets move the index to look at the new place of the entry
|
||||
index += 1
|
||||
# If x is clicked
|
||||
elif '\u274c' in reaction.emoji:
|
||||
# A second check just to make sure
|
||||
if author.server_permissions.kick_members or author == entry.requester:
|
||||
if entry != queue[index]:
|
||||
fmt = "`Error: Position of this entry has changed, cannot complete your action`"
|
||||
else:
|
||||
# Simply remove the entry in place
|
||||
del queue[index]
|
||||
# This is the only check we need to make, to ensure index is now not more than last
|
||||
new_count = count - 1
|
||||
if index >= new_count:
|
||||
index = new_count - 1
|
||||
try:
|
||||
await self.bot.remove_reaction(message, reaction.emoji, user)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
await self.bot.delete_message(message)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.check(utils.is_owner)
|
||||
async def vdebug(self, ctx, *, code : str):
|
||||
"""Evaluates code."""
|
||||
code = code.strip('` ')
|
||||
python = '```py\n{}\n```'
|
||||
result = None
|
||||
|
||||
env = {
|
||||
'bot': self.bot,
|
||||
'ctx': ctx,
|
||||
'message': ctx.message,
|
||||
'server': ctx.message.server,
|
||||
'channel': ctx.message.channel,
|
||||
'author': ctx.message.author
|
||||
}
|
||||
|
||||
env.update(globals())
|
||||
|
||||
try:
|
||||
result = eval(code, env)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
except Exception as e:
|
||||
await self.bot.say(python.format(type(e).__name__ + ': ' + str(e)))
|
||||
return
|
||||
|
||||
await self.bot.say(python.format(result))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def progress(self, ctx):
|
||||
"""Provides the progress of the current song"""
|
||||
|
||||
# Make sure we're playing first
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing anything.')
|
||||
else:
|
||||
progress = state.current.progress
|
||||
length = state.current.length
|
||||
# Another check, just to make sure; this may happen for a very brief amount of time
|
||||
# Between when the song was requested, and still downloading to play
|
||||
if not progress or not length:
|
||||
await self.bot.say('Not playing anything.')
|
||||
return
|
||||
|
||||
# Otherwise just format this nicely
|
||||
progress = divmod(round(progress, 0), 60)
|
||||
length = divmod(round(length, 0), 60)
|
||||
fmt = "Current song progress: {0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
|
||||
await self.bot.say(fmt)
|
||||
|
||||
@commands.command(no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def join(self, *, channel: discord.Channel):
|
||||
"""Joins a voice channel."""
|
||||
try:
|
||||
await self.create_voice_client(channel)
|
||||
# Check if the channel given was an actual voice channel
|
||||
except discord.InvalidArgument:
|
||||
await self.bot.say('This is not a voice channel...')
|
||||
except (asyncio.TimeoutError, discord.ConnectionClosed):
|
||||
await self.bot.say("I failed to connect! This usually happens if I don't have permission to join the"
|
||||
" channel, but can sometimes be caused by your server region being far away."
|
||||
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
|
||||
await self.remove_voice_client(channel.server)
|
||||
else:
|
||||
await self.bot.say('Ready to play audio in ' + channel.name)
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def summon(self, ctx):
|
||||
"""Summons the bot to join your voice channel."""
|
||||
# This method will be invoked by other commands, so we should return True or False instead of just returning
|
||||
# First check if the author is even in a voice_channel
|
||||
summoned_channel = ctx.message.author.voice_channel
|
||||
if summoned_channel is None:
|
||||
await self.bot.say('You are not in a voice channel.')
|
||||
return False
|
||||
|
||||
# Then simply create a voice client
|
||||
try:
|
||||
success = await self.create_voice_client(summoned_channel)
|
||||
except (asyncio.TimeoutError, discord.ConnectionClosed):
|
||||
await self.bot.say("I failed to connect! This usually happens if I don't have permission to join the"
|
||||
" channel, but can sometimes be caused by your server region being far away."
|
||||
" Otherwise this is an issue on Discord's end, causing the connect to timeout!")
|
||||
await self.remove_voice_client(summoned_channel.server)
|
||||
return False
|
||||
|
||||
if success:
|
||||
try:
|
||||
await self.bot.say('Ready to play audio in ' + summoned_channel.name)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
return success
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def play(self, ctx, *, song: str):
|
||||
"""Plays a song.
|
||||
If there is a song currently in the queue, then it is
|
||||
queued until the next song is done playing.
|
||||
This command automatically searches as well from YouTube.
|
||||
The list of supported sites can be found here:
|
||||
https://rg3.github.io/youtube-dl/supportedsites.html
|
||||
"""
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
|
||||
# First check if we are connected to a voice channel at all, if not summon to the channel the author is in
|
||||
# Since summon utils if the author is in a channel, we don't need to handle that here, just return if it failed
|
||||
if state.voice is None:
|
||||
success = await ctx.invoke(self.summon)
|
||||
if not success:
|
||||
return
|
||||
|
||||
# If the queue is full, we ain't adding anything to it
|
||||
if state.songs.full:
|
||||
await self.bot.say("The queue is currently full! You'll need to wait to add a new song")
|
||||
return
|
||||
|
||||
author_channel = ctx.message.author.voice.voice_channel
|
||||
my_channel = ctx.message.server.me.voice.voice_channel
|
||||
|
||||
if my_channel is None:
|
||||
# If we're here this means that after 3 attempts...4 different "failsafes"...
|
||||
# Discord has returned saying the connection was successful, and returned a None connection
|
||||
await self.bot.say("I failed to connect to the channel! Please try again soon")
|
||||
return
|
||||
|
||||
# To try to avoid some abuse, ensure the requester is actually in our channel
|
||||
if my_channel != author_channel:
|
||||
await self.bot.say("You are not currently in the channel; please join before trying to request a song.")
|
||||
return
|
||||
|
||||
# Set the number of required skips to start
|
||||
num_members = len(my_channel.voice_members)
|
||||
state.required_skips = math.ceil((num_members + 1) / 3)
|
||||
|
||||
# Create the player, and check if this was successful
|
||||
# Here all we want is to get the information of the player
|
||||
song = re.sub('[<>\[\]]', '', song)
|
||||
|
||||
try:
|
||||
_entry, position = await state.songs.add_entry(song, ctx.message.author)
|
||||
except WrongEntryTypeError:
|
||||
# This means that a song was attempted to be searched, instead of a link provided
|
||||
try:
|
||||
info = await self.downloader.extract_info(self.bot.loop, song, download=False, process=True)
|
||||
song = info.get('entries', [])[0]['webpage_url']
|
||||
except IndexError:
|
||||
await self.bot.send_message(ctx.message.channel, "No results found for {}!".format(song))
|
||||
return
|
||||
except ExtractionError as e:
|
||||
# This gets the youtube_dl error, instead of our error raised
|
||||
error = str(e).split("\n\n")[1]
|
||||
# Youtube has a "fancy" colour error message it prints to the console
|
||||
# Obviously this doesn't work in Discord, so just remove this
|
||||
error = " ".join(error.split()[1:])
|
||||
await self.bot.send_message(ctx.message.channel, error)
|
||||
return
|
||||
try:
|
||||
_entry, position = await state.songs.add_entry(song, ctx.message.author)
|
||||
except WrongEntryTypeError:
|
||||
# This is either a playlist, or something not supported
|
||||
fmt = "Sorry but I couldn't download that! Either you provided a playlist, a streamed link, or " \
|
||||
"a page that is not supported to download."
|
||||
await self.bot.send_message(ctx.message.channel, fmt)
|
||||
return
|
||||
except ExtractionError as e:
|
||||
# This gets the youtube_dl error, instead of our error raised
|
||||
error = str(e).split("\n\n")[1]
|
||||
# Youtube has a "fancy" colour error message it prints to the console
|
||||
# Obviously this doesn't work in Discord, so just remove this
|
||||
error = " ".join(error.split()[1:])
|
||||
# Make sure we are not over our 2000 message limit length (there are some youtube errors that are)
|
||||
if len(error) >= 2000:
|
||||
error = "{}...".format(error[:1996])
|
||||
await self.bot.send_message(ctx.message.channel, error)
|
||||
return
|
||||
await self.bot.say('Enqueued ' + str(_entry))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def volume(self, ctx, value: int = None):
|
||||
"""Sets the volume of the currently playing song."""
|
||||
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if value is None:
|
||||
volume = state.volume
|
||||
await self.bot.say("Current volume is {}".format(volume))
|
||||
return
|
||||
if value > 200:
|
||||
await self.bot.say("Sorry but the max volume is 200")
|
||||
return
|
||||
state.volume = value
|
||||
if state.is_playing():
|
||||
player = state.player
|
||||
player.volume = value / 100
|
||||
await self.bot.say('Set the volume to {:.0%}'.format(player.volume))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def pause(self, ctx):
|
||||
"""Pauses the currently played song."""
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if state.is_playing():
|
||||
state.player.pause()
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def resume(self, ctx):
|
||||
"""Resumes the currently played song."""
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if state.is_playing():
|
||||
state.player.resume()
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def stop(self, ctx):
|
||||
"""Stops playing audio and leaves the voice channel.
|
||||
This also clears the queue.
|
||||
"""
|
||||
server = ctx.message.server
|
||||
state = self.get_voice_state(server)
|
||||
|
||||
# Stop playing whatever song is playing.
|
||||
if state.is_playing():
|
||||
state.player.stop()
|
||||
|
||||
state.songs.clear()
|
||||
|
||||
# This will stop cancel the audio event we're using to loop through the queue
|
||||
# Then erase the voice_state entirely, and disconnect from the channel
|
||||
try:
|
||||
state.audio_player.cancel()
|
||||
state.clear_audio_files()
|
||||
await self.remove_voice_client(ctx.message.server)
|
||||
del self.voice_states[server.id]
|
||||
except:
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def eta(self, ctx):
|
||||
"""Provides an ETA on when your next song will play"""
|
||||
# Note: There is no way to tell how long a song has been playing, or how long there is left on a song
|
||||
# That is why this is called an "ETA"
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
author = ctx.message.author
|
||||
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing any music right now...')
|
||||
return
|
||||
|
||||
queue = state.songs.entries
|
||||
if len(queue) == 0:
|
||||
await self.bot.say("Nothing currently in the queue")
|
||||
return
|
||||
|
||||
# Start off by adding the remaining length of the current song
|
||||
count = state.current.remaining
|
||||
found = False
|
||||
# Loop through the songs in the queue, until the author is found as the requester
|
||||
# The found bool is used to see if we actually found the author, or we just looped through the whole queue
|
||||
for song in queue:
|
||||
if song.requester == author:
|
||||
found = True
|
||||
break
|
||||
count += song.duration
|
||||
|
||||
# This is checking if nothing from the queue has been added to the total
|
||||
# If it has not, then we have not looped through the queue at all
|
||||
# Since the queue was already checked to have more than one song in it, this means the author is next
|
||||
if count == state.current.duration:
|
||||
await self.bot.say("You are next in the queue!")
|
||||
return
|
||||
if not found:
|
||||
await self.bot.say("You are not in the queue!")
|
||||
return
|
||||
await self.bot.say("ETA till your next play is: {0[0]}m {0[1]}s".format(divmod(round(count, 0), 60)))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def queue(self, ctx):
|
||||
"""Provides a printout of the songs that are in the queue"""
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing any music right now...')
|
||||
return
|
||||
|
||||
# Asyncio provides no non-private way to access the queue, so we have to use _queue
|
||||
queue = state.songs.entries
|
||||
if len(queue) == 0:
|
||||
await self.bot.say("Nothing currently in the queue")
|
||||
else:
|
||||
self.bot.loop.create_task(self.queue_embed_task(state, ctx.message.channel, ctx.message.author))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def queuelength(self, ctx):
|
||||
"""Prints the length of the queue"""
|
||||
await self.bot.say("There are a total of {} songs in the queue"
|
||||
.format(len(self.get_voice_state(ctx.message.server).songs.entries)))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def skip(self, ctx):
|
||||
"""Vote to skip a song. The song requester can automatically skip.
|
||||
approximately 1/3 of the members in the voice channel
|
||||
are required to vote to skip for the song to be skipped.
|
||||
"""
|
||||
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing any music right now...')
|
||||
return
|
||||
|
||||
# Check if the person requesting a skip is the requester of the song, if so automatically skip
|
||||
voter = ctx.message.author
|
||||
if voter == state.current.requester:
|
||||
await self.bot.say('Requester requested skipping song...')
|
||||
state.skip()
|
||||
# Otherwise check if the voter has already voted
|
||||
elif voter.id not in state.skip_votes:
|
||||
state.skip_votes.add(voter.id)
|
||||
total_votes = len(state.skip_votes)
|
||||
|
||||
# Now check how many votes have been made, if 3 then go ahead and skip, otherwise add to the list of votes
|
||||
if total_votes >= state.required_skips:
|
||||
await self.bot.say('Skip vote passed, skipping song...')
|
||||
state.skip()
|
||||
else:
|
||||
await self.bot.say('Skip vote added, currently at [{}/{}]'.format(total_votes, state.required_skips))
|
||||
else:
|
||||
await self.bot.say('You have already voted to skip this song.')
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(kick_members=True)
|
||||
async def modskip(self, ctx):
|
||||
"""Forces a song skip, can only be used by a moderator"""
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing any music right now...')
|
||||
return
|
||||
|
||||
state.skip()
|
||||
await self.bot.say('Song has just been skipped.')
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def playing(self, ctx):
|
||||
"""Shows info about the currently played song."""
|
||||
|
||||
state = self.get_voice_state(ctx.message.server)
|
||||
if not state.is_playing():
|
||||
await self.bot.say('Not playing anything.')
|
||||
else:
|
||||
# Create the embed object we'll use
|
||||
embed = discord.Embed()
|
||||
# Fill in the simple things
|
||||
embed.add_field(name='Title', value=state.current.title, inline=False)
|
||||
embed.add_field(name='Requester', value=state.current.requester.display_name, inline=False)
|
||||
# Get the amount of current skips, and display how many have been skipped/how many required
|
||||
skip_count = len(state.skip_votes)
|
||||
embed.add_field(name='Skip Count', value='{}/{}'.format(skip_count, state.required_skips), inline=False)
|
||||
# Get the current progress and display this
|
||||
progress = state.current.progress
|
||||
length = state.current.length
|
||||
progress = divmod(round(progress, 0), 60)
|
||||
length = divmod(round(length, 0), 60)
|
||||
fmt = "{0[0]}m {0[1]}s/{1[0]}m {1[1]}s".format(progress, length)
|
||||
embed.add_field(name='Progress', value=fmt,inline=False)
|
||||
# And send the embed
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Music(bot))
|
329
cogs/osu.py
|
@ -1,191 +1,198 @@
|
|||
from .utils import config
|
||||
from .utils import checks
|
||||
from .utils import images
|
||||
import utils
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
|
||||
import aiohttp
|
||||
from osuapi import OsuApi, AHConnector
|
||||
from asyncpg import UniqueViolationError
|
||||
|
||||
# https://github.com/ppy/osu-api/wiki
|
||||
base_url = 'https://osu.ppy.sh/api/'
|
||||
BASE_URL = 'https://osu.ppy.sh/api/'
|
||||
MAX_RETRIES = 5
|
||||
|
||||
|
||||
class Osu:
|
||||
class Osu(commands.Cog):
|
||||
"""View OSU stats"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.headers = {'User-Agent': config.user_agent}
|
||||
self.key = config.osu_key
|
||||
self.api = OsuApi(utils.osu_key, connector=AHConnector())
|
||||
self.bot.loop.create_task(self.get_users())
|
||||
self.osu_users = {}
|
||||
|
||||
async def _request(self, payload, endpoint):
|
||||
"""Handles requesting to the API"""
|
||||
async def get_user(self, member, username):
|
||||
"""A function used to get and save user data in cache"""
|
||||
user = self.osu_users.get(member.id)
|
||||
if user is None:
|
||||
user = await self.get_user_from_api(username)
|
||||
if user is not None:
|
||||
self.osu_users[member.id] = user
|
||||
return user
|
||||
# Just return False for the "osu API does not work correctly" issue
|
||||
elif user is False:
|
||||
return False
|
||||
else:
|
||||
if user.username.lower() == username.lower():
|
||||
return user
|
||||
else:
|
||||
user = await self.get_user_from_api(username)
|
||||
if user is not None:
|
||||
self.osu_users[member.id] = user
|
||||
return user
|
||||
|
||||
# Format the URL we'll need based on the base_url, and the endpoint we want to hit
|
||||
url = "{}{}".format(base_url, endpoint)
|
||||
|
||||
# Check if our key was added, if it wasn't, add it
|
||||
key = payload.get('k', self.key)
|
||||
payload['k'] = key
|
||||
|
||||
# Attempt to connect up to our max retries
|
||||
for x in range(MAX_RETRIES):
|
||||
try:
|
||||
async with aiohttp.get(url, headers=self.headers, params=payload) as r:
|
||||
# If we failed to connect, attempt again
|
||||
if r.status != 200:
|
||||
continue
|
||||
|
||||
data = await r.json()
|
||||
return data
|
||||
# If any error happened when making the request, attempt again
|
||||
except:
|
||||
continue
|
||||
|
||||
async def find_beatmap(self, query):
|
||||
"""Finds a beatmap ID based on the first match of searching a beatmap"""
|
||||
pass
|
||||
|
||||
async def get_beatmap(self, b_id):
|
||||
"""Gets beatmap info based on the ID provided"""
|
||||
params = {'b': b_id}
|
||||
endpoint = 'get_beatmaps'
|
||||
data = await self._request(params, endpoint)
|
||||
async def get_user_from_api(self, username):
|
||||
"""A simple helper function to parse the list given and handle failures"""
|
||||
try:
|
||||
return data[0]
|
||||
user = await self.api.get_user(username)
|
||||
return user[0]
|
||||
except IndexError:
|
||||
return None
|
||||
# So we can send a specific message for this error
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
@commands.group(pass_context=True, invoke_without_command=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def osu(self, ctx):
|
||||
pass
|
||||
async def get_users(self):
|
||||
"""A task used to 'cache' all member's and their Osu profile's"""
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
@osu.command(name='scores', aliases=['score'], pass_context=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def osu_user_scores(self, ctx, user, song=1):
|
||||
"""Used to get the top scores for a user
|
||||
You can provide either your Osu ID or your username
|
||||
However, if your username is only numbers, this will confuse the API
|
||||
If you have only numbers in your username, you will need to provide your ID
|
||||
This will by default return the top song, provide song [up to 100] to get that song, in order from best to worst
|
||||
query = "SELECT id, osu FROM users WHERE osu IS NOT NULL;"
|
||||
rows = await self.bot.db.fetch(query)
|
||||
|
||||
EXAMPLE: !osu MyUsername 5
|
||||
RESULT: Info about your 5th best song"""
|
||||
for row in rows:
|
||||
user = await self.get_user_from_api(row['osu'])
|
||||
if user is not None:
|
||||
self.osu_users[row['id']] = user
|
||||
|
||||
await self.bot.say("Looking up your Osu information...")
|
||||
# To make this easy for the user, it's indexed starting at 1, so lets subtract by 1
|
||||
song -= 1
|
||||
# Make sure the song is not negative however, if so set to 0
|
||||
if song < 0:
|
||||
song = 0
|
||||
@commands.group(invoke_without_command=True)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def osu(self, ctx, member: discord.Member = None):
|
||||
"""Provides basic information about a specific user
|
||||
|
||||
# A list of the possible values we'll receive, that we want to display
|
||||
wanted_info = ['username', 'maxcombo', 'count300', 'count100', 'count50', 'countmiss',
|
||||
'perfect', 'enabled_mods', 'date', 'rank' 'pp', 'beatmap_title', 'beatmap_version',
|
||||
'max_combo', 'artist', 'difficulty']
|
||||
EXAMPLE: !osu @Person
|
||||
RESULT: Informationa bout that person's osu account"""
|
||||
|
||||
# A couple of these aren't the best names to display, so setup a map to change these just a little bit
|
||||
key_map = {'maxcombo': 'combo',
|
||||
'count300': '300 hits',
|
||||
'count100': '100 hits',
|
||||
'count50': '50 hits',
|
||||
'countmiss': 'misses',
|
||||
'perfect': 'got_max_combo'}
|
||||
if member is None:
|
||||
member = ctx.message.author
|
||||
|
||||
params = {'u': user,
|
||||
'limit': 100}
|
||||
# The endpoint that we're accessing to get this informatin
|
||||
endpoint = 'get_user_best'
|
||||
data = await self._request(params, endpoint)
|
||||
|
||||
try:
|
||||
data = data[song]
|
||||
except IndexError:
|
||||
if len(data) == 0:
|
||||
await self.bot.say("I could not find any top songs for the user {}".format(user))
|
||||
return
|
||||
else:
|
||||
data = data[len(data) - 1]
|
||||
|
||||
# There's a little bit more info that we need, some info specific to the beatmap.
|
||||
# Due to this we'll need to make a second request
|
||||
beatmap_data = await self.get_beatmap(data.get('beatmap_id', None))
|
||||
|
||||
# Lets add the extra data we want
|
||||
data['beatmap_title'] = beatmap_data.get('title')
|
||||
data['beatmap_version'] = beatmap_data.get('version')
|
||||
data['max_combo'] = beatmap_data.get('max_combo')
|
||||
data['artist'] = beatmap_data.get('artist')
|
||||
# Lets round this, no need for such a long number
|
||||
data['difficulty'] = round(float(beatmap_data.get('difficultyrating')), 2)
|
||||
|
||||
# Now lets create our dictionary needed to create the image
|
||||
# The dict comprehension we're using is simpler than it looks, it's simply they key: value
|
||||
# If the key is in our wanted_info list
|
||||
# We also get the wanted value from the key_map if it exists, using the key itself if it doesn't
|
||||
# We then title it and replace _ with a space to ensure nice formatting
|
||||
fmt = [(key_map.get(k, k).title().replace('_', ' '), v) for k, v in data.items() if k in wanted_info]
|
||||
|
||||
# Attempt to create our banner and upload that
|
||||
# If we can't find the images needed, or don't have permissions, just send a message instead
|
||||
try:
|
||||
banner = await images.create_banner(ctx.message.author, "Osu User Stats", fmt)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
_fmt = "\n".join("{}: {}".format(k, r) for k, r in fmt)
|
||||
await self.bot.say("```\n{}```".format(_fmt))
|
||||
|
||||
@osu.command(name='user', pass_context=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def osu_user_info(self, ctx, *, user):
|
||||
"""Used to get information about a specific user
|
||||
You can provide either your Osu ID or your username
|
||||
However, if your username is only numbers, this will confuse the API
|
||||
If you have only numbers in your username, you will need to provide your ID
|
||||
|
||||
EXAMPLE: !osu user MyUserName
|
||||
RESULT: Info about your user"""
|
||||
|
||||
await self.bot.say("Looking up your Osu information...")
|
||||
# A list of the possible values we'll receive, that we want to display
|
||||
wanted_info = ['username', 'playcount', 'ranked_score', 'pp_rank', 'level', 'pp_country_rank',
|
||||
'accuracy', 'country', 'pp_country_rank', 'count_rank_s', 'count_rank_a']
|
||||
|
||||
# A couple of these aren't the best names to display, so setup a map to change these just a little bit
|
||||
key_map = {'playcount': 'play_count',
|
||||
'count_rank_ss': 'total_SS_ranks',
|
||||
'count_rank_s': 'total_s_ranks',
|
||||
'count_rank_a': 'total_a_ranks'}
|
||||
|
||||
# The paramaters that we'll send to osu to get the information needed
|
||||
params = {'u': user}
|
||||
# The endpoint that we're accessing to get this informatin
|
||||
endpoint = 'get_user'
|
||||
data = await self._request(params, endpoint)
|
||||
|
||||
# Make sure we found a result, we should only find one with the way we're searching
|
||||
try:
|
||||
data = data[0]
|
||||
except IndexError:
|
||||
await self.bot.say("I could not find anyone with the user name/id of {}".format(user))
|
||||
user = self.osu_users.get(member.id)
|
||||
if user is None:
|
||||
await ctx.send("I do not have {}'s Osu user saved!".format(member.display_name))
|
||||
return
|
||||
|
||||
# Now lets create our dictionary needed to create the image
|
||||
# The dict comprehension we're using is simpler than it looks, it's simply they key: value
|
||||
# If the key is in our wanted_info list
|
||||
# We also get the wanted value from the key_map if it exists, using the key itself if it doesn't
|
||||
# We then title it and replace _ with a space to ensure nice formatting
|
||||
fmt = {key_map.get(k, k).title().replace('_', ' '): v for k, v in data.items() if k in wanted_info}
|
||||
e = discord.Embed(title='Osu profile for {}'.format(user.username))
|
||||
e.add_field(name='Rank', value="{:,}".format(user.pp_rank))
|
||||
e.add_field(name='Level', value=user.level)
|
||||
e.add_field(name='Performance Points', value="{:,}".format(user.pp_raw))
|
||||
e.add_field(name='Accuracy', value="{:.2%}".format(user.accuracy / 100))
|
||||
e.add_field(name='SS Ranks', value="{:,}".format(user.count_rank_ss))
|
||||
e.add_field(name='S Ranks', value="{:,}".format(user.count_rank_s))
|
||||
e.add_field(name='A Ranks', value="{:,}".format(user.count_rank_a))
|
||||
e.add_field(name='Country', value=user.country)
|
||||
e.add_field(name='Country Rank', value="{:,}".format(user.pp_country_rank))
|
||||
e.add_field(name='Playcount', value="{:,}".format(user.playcount))
|
||||
e.add_field(name='Ranked Score', value="{:,}".format(user.ranked_score))
|
||||
e.add_field(name='Total Score', value="{:,}".format(user.total_score))
|
||||
|
||||
await ctx.send(embed=e)
|
||||
|
||||
@osu.command(name='add', aliases=['create', 'connect'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def osu_add(self, ctx, *, username):
|
||||
"""Links an osu account to your discord account
|
||||
|
||||
EXAMPLE: !osu add username
|
||||
RESULT: Links your username to your account, and allows stats to be pulled from it"""
|
||||
|
||||
author = ctx.message.author
|
||||
user = await self.get_user(author, username)
|
||||
if user is None:
|
||||
await ctx.send("I couldn't find an osu user that matches {}".format(username))
|
||||
return
|
||||
elif user is False:
|
||||
await ctx.send("Unfortunately OSU's API is a 'beta', and for some users they do not return **any** data."
|
||||
"In this case, that's you! Congrats?")
|
||||
|
||||
# Attempt to create our banner and upload that
|
||||
# If we can't find the images needed, or don't have permissions, just send a message instead
|
||||
try:
|
||||
banner = await images.create_banner(ctx.message.author, "Osu User Stats", fmt)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
_fmt = "\n".join("{}: {}".format(k, r) for k, r in fmt.items())
|
||||
await self.bot.say("```\n{}```".format(_fmt))
|
||||
await ctx.bot.db.execute("INSERT INTO users (id, osu) VALUES ($1, $2)", ctx.author.id, user.username)
|
||||
except UniqueViolationError:
|
||||
await ctx.bot.db.execute("UPDATE users SET osu = $1 WHERE id = $2", user.username, ctx.author.id)
|
||||
await ctx.send("I have just saved your Osu user {}".format(author.display_name))
|
||||
|
||||
@osu.command(name='score', aliases=['scores'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def osu_scores(self, ctx, *data):
|
||||
"""Find the top x osu scores for a provided member
|
||||
Note: You can only get the top 50 songs for a user
|
||||
|
||||
EXAMPLE: !osu scores @Person 5
|
||||
RESULT: The top 5 maps for the user @Person"""
|
||||
|
||||
# Set the defaults before we go through our passed data to figure out what we want
|
||||
limit = 5
|
||||
member = ctx.message.author
|
||||
# Only loop through the first 2....we care about nothing after that
|
||||
for piece in data[:2]:
|
||||
# First lets try to convert to an int, for limit
|
||||
try:
|
||||
limit = int(piece)
|
||||
# Since Osu's API returns no information about the beatmap on scores
|
||||
# We also need to call it to get the beatmap...in order to not get rate limited
|
||||
# Lets limit this to 50
|
||||
if limit > 50:
|
||||
limit = 50
|
||||
elif limit < 1:
|
||||
limit = 5
|
||||
except Exception:
|
||||
converter = commands.converter.MemberConverter()
|
||||
try:
|
||||
member = await converter.convert(ctx, piece)
|
||||
except commands.converter.BadArgument:
|
||||
pass
|
||||
|
||||
user = self.osu_users.get(member.id)
|
||||
if user is None:
|
||||
await ctx.send("I don't have that user's Osu account saved!")
|
||||
return
|
||||
|
||||
scores = await self.api.get_user_best(user.username, limit=limit)
|
||||
entries = []
|
||||
if len(scores) == 0:
|
||||
await ctx.send("Sorry, but I can't find any scores for you {}!".format(member.display_name))
|
||||
return
|
||||
|
||||
for i in scores:
|
||||
m = await self.api.get_beatmaps(beatmap_id=i.beatmap_id)
|
||||
m = m[0]
|
||||
entry = {
|
||||
'title': 'Top {} Osu scores for {}'.format(limit, member.display_name),
|
||||
'fields': [
|
||||
{'name': 'Artist', 'value': m.artist},
|
||||
{'name': 'Title', 'value': m.title},
|
||||
{'name': 'Creator', 'value': m.creator},
|
||||
{'name': 'CS (Circle Size)', 'value': m.diff_size},
|
||||
{'name': 'AR (Approach Rate)', 'value': m.diff_approach},
|
||||
{'name': 'HP (Health Drain)', 'value': m.diff_drain},
|
||||
{'name': 'OD (Overall Difficulty)', 'value': m.diff_overall},
|
||||
{'name': 'Length', 'value': m.total_length},
|
||||
{'name': 'Score', 'value': i.score},
|
||||
{'name': 'Max Combo', 'value': i.maxcombo},
|
||||
{'name': 'Hits',
|
||||
'value': "{}/{}/{}/{} (300/100/50/misses)".format(i.count300, i.count100, i.count50, i.countmiss),
|
||||
"inline": False},
|
||||
{'name': 'Perfect', 'value': "Yes" if i.perfect else "No"},
|
||||
{'name': 'Rank', 'value': i.rank},
|
||||
{'name': 'PP', 'value': i.pp},
|
||||
{'name': 'Mods', 'value': str(i.enabled_mods)},
|
||||
{'name': 'Date', 'value': str(i.date)}
|
||||
]
|
||||
}
|
||||
|
||||
entries.append(entry)
|
||||
try:
|
||||
pages = utils.DetailedPages(ctx.bot, message=ctx.message, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from . import utils
|
||||
import utils
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import logging
|
||||
|
||||
BASE_URL = "https://api.owapi.net/api/v3/u/"
|
||||
# This is a list of the possible things that we may want to retrieve from the stats
|
||||
|
@ -12,45 +13,46 @@ check_g_stats = ["eliminations", "deaths", 'kpd', 'wins', 'losses', 'time_played
|
|||
'cards', 'damage_done', 'healing_done', 'multikills']
|
||||
check_o_stats = ['wins']
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
class Overwatch:
|
||||
|
||||
class Overwatch(commands.Cog):
|
||||
"""Class for viewing Overwatch stats"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.group(no_pm=True)
|
||||
async def ow(self):
|
||||
@commands.group()
|
||||
async def ow(self, ctx):
|
||||
"""Command used to lookup information on your own user, or on another's
|
||||
When adding your battletag, it is quite picky, use the exact format user#xxxx
|
||||
Multiple names with the same username can be used, this is why the numbers are needed
|
||||
Capitalization also matters"""
|
||||
pass
|
||||
|
||||
@ow.command(name="stats", pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@ow.command(name="stats")
|
||||
@utils.can_run(send_messages=True)
|
||||
async def ow_stats(self, ctx, user: discord.Member = None, hero: str = ""):
|
||||
"""Prints out a basic overview of a member's stats
|
||||
Provide a hero after the member to get stats for that specific hero
|
||||
|
||||
EXAMPLE: !ow stats @OtherPerson Junkrat
|
||||
RESULT: Whether or not you should unfriend this person because they're a dirty rat"""
|
||||
|
||||
user = user or ctx.message.author
|
||||
ow_stats = await utils.get_content('overwatch', user.id)
|
||||
bt = ctx.bot.db.load('overwatch', key=str(user.id), pluck='battletag')
|
||||
|
||||
if ow_stats is None:
|
||||
await self.bot.say("I do not have this user's battletag saved!")
|
||||
if bt is None:
|
||||
await ctx.send("I do not have this user's battletag saved!")
|
||||
return
|
||||
# This API sometimes takes a while to look up information, so send a message saying we're processing
|
||||
|
||||
bt = ow_stats['battletag']
|
||||
|
||||
if hero == "":
|
||||
# If no hero was provided, we just want the base stats for a player
|
||||
url = BASE_URL + "{}/stats".format(bt)
|
||||
data = await utils.request(url)
|
||||
region = [x for x in data.keys() if data[x] is not None][0]
|
||||
if data is None:
|
||||
await ctx.send("I couldn't connect to overwatch at the moment!")
|
||||
return
|
||||
|
||||
log.info(data)
|
||||
|
||||
region = [x for x in data.keys() if data[x] is not None and x in ['us', 'any', 'kr', 'eu']][0]
|
||||
stats = data[region]['stats']['quickplay']
|
||||
|
||||
output_data = [(k.title().replace("_", " "), r) for k, r in stats['game_stats'].items() if
|
||||
|
@ -60,14 +62,17 @@ class Overwatch:
|
|||
hero = hero.lower().replace('-', '')
|
||||
url = BASE_URL + "{}/heroes".format(bt)
|
||||
data = await utils.request(url)
|
||||
if data is None:
|
||||
await ctx.send("I couldn't connect to overwatch at the moment!")
|
||||
return
|
||||
|
||||
region = [x for x in data.keys() if data[x] is not None][0]
|
||||
region = [x for x in data.keys() if data[x] is not None and x in ['us', 'any', 'kr', 'eu']][0]
|
||||
stats = data[region]['heroes']['stats']['quickplay'].get(hero)
|
||||
|
||||
if stats is None:
|
||||
fmt = "I couldn't find data with that hero, make sure that is a valid hero, " \
|
||||
"otherwise {} has never used the hero {} before!".format(user.display_name, hero)
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
return
|
||||
|
||||
# Same list comprehension as before
|
||||
|
@ -77,53 +82,56 @@ class Overwatch:
|
|||
output_data.append((k.title().replace("_", " "), r))
|
||||
try:
|
||||
banner = await utils.create_banner(user, "Overwatch", output_data)
|
||||
await self.bot.upload(banner)
|
||||
await ctx.send(file=discord.File(banner, filename='banner.png'))
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
fmt = "\n".join("{}: {}".format(k, r) for k, r in output_data)
|
||||
await self.bot.say("Overwatch stats for {}: ```py\n{}```".format(user.name, fmt))
|
||||
await ctx.send("Overwatch stats for {}: ```py\n{}```".format(user.name, fmt))
|
||||
|
||||
@ow.command(pass_context=True, name="add")
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@ow.command(name="add")
|
||||
@utils.can_run(send_messages=True)
|
||||
async def add(self, ctx, bt: str):
|
||||
"""Saves your battletag for looking up information
|
||||
|
||||
EXAMPLE: !ow add Username#1234
|
||||
RESULT: Your battletag is now saved"""
|
||||
await ctx.message.channel.trigger_typing()
|
||||
|
||||
|
||||
# Battletags are normally provided like name#id
|
||||
# However the API needs this to be a -, so repliace # with - if it exists
|
||||
bt = bt.replace("#", "-")
|
||||
key = ctx.message.author.id
|
||||
key = str(ctx.message.author.id)
|
||||
|
||||
# All we're doing here is ensuring that the status is 200 when looking up someone's general information
|
||||
# If it's not, let them know exactly how to format their tag
|
||||
url = BASE_URL + "{}/stats".format(bt)
|
||||
data = await utils.request(url)
|
||||
if data is None:
|
||||
await self.bot.say("Profile does not exist! Battletags are picky, "
|
||||
"format needs to be `user#xxxx`. Capitalization matters")
|
||||
await ctx.send("Profile does not exist! Battletags are picky, "
|
||||
"format needs to be `user#xxxx`. Capitalization matters")
|
||||
return
|
||||
|
||||
# Now just save the battletag
|
||||
entry = {'member_id': key, 'battletag': bt}
|
||||
update = {'battletag': bt}
|
||||
# Try adding this first, if that fails, update the saved entry
|
||||
if not await utils.add_content('overwatch', entry):
|
||||
await utils.update_content('overwatch', update, key)
|
||||
await self.bot.say("I have just saved your battletag {}".format(ctx.message.author.mention))
|
||||
entry = {
|
||||
'member_id': key,
|
||||
'battletag': bt
|
||||
}
|
||||
|
||||
@ow.command(pass_context=True, name="delete", aliases=['remove'])
|
||||
@utils.custom_perms(send_messages=True)
|
||||
await ctx.bot.db.save('overwatch', entry)
|
||||
await ctx.send("I have just saved your battletag {}".format(ctx.message.author.mention))
|
||||
|
||||
@ow.command(name="delete", aliases=['remove'])
|
||||
@utils.can_run(send_messages=True)
|
||||
async def delete(self, ctx):
|
||||
"""Removes your battletag from the records
|
||||
|
||||
EXAMPLE: !ow delete
|
||||
RESULT: Your battletag is no longer saved"""
|
||||
if await utils.remove_content('overwatch', ctx.message.author.id):
|
||||
await self.bot.say("I no longer have your battletag saved {}".format(ctx.message.author.mention))
|
||||
else:
|
||||
await self.bot.say("I don't even have your battletag saved {}".format(ctx.message.author.mention))
|
||||
entry = {
|
||||
'member_id': str(ctx.message.author.id),
|
||||
'battletag': None
|
||||
}
|
||||
await ctx.bot.db.save('overwatch', entry)
|
||||
await ctx.send("I no longer have your battletag saved {}".format(ctx.message.author.mention))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
280
cogs/owner.py
|
@ -1,94 +1,226 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
import re
|
||||
import glob
|
||||
import asyncio
|
||||
import discord
|
||||
import inspect
|
||||
import aiohttp
|
||||
import pendulum
|
||||
import asyncio
|
||||
import textwrap
|
||||
import traceback
|
||||
import subprocess
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
|
||||
class Owner:
|
||||
"""Commands that can only be used by Phantom, bot management commands"""
|
||||
def get_syntax_error(e):
|
||||
if e.text is None:
|
||||
return '```py\n{0.__class__.__name__}: {0}\n```'.format(e)
|
||||
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__)
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
class Owner(commands.Cog):
|
||||
"""Commands that can only be used by the owner of the bot, bot management commands"""
|
||||
_last_result = None
|
||||
sessions = set()
|
||||
|
||||
async def cog_check(self, ctx):
|
||||
return await ctx.bot.is_owner(ctx.author)
|
||||
|
||||
@staticmethod
|
||||
def cleanup_code(content):
|
||||
"""Automatically removes code blocks from the code."""
|
||||
# remove ```py\n```
|
||||
if content.startswith('```') and content.endswith('```'):
|
||||
return '\n'.join(content.split('\n')[1:-1])
|
||||
|
||||
# remove `foo`
|
||||
return content.strip('` \n')
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def repl(self, ctx):
|
||||
msg = ctx.message
|
||||
|
||||
variables = {
|
||||
'ctx': ctx,
|
||||
'bot': ctx.bot,
|
||||
'message': msg,
|
||||
'guild': msg.guild,
|
||||
'server': msg.guild,
|
||||
'channel': msg.channel,
|
||||
'author': msg.author,
|
||||
'self': self,
|
||||
'_': None,
|
||||
}
|
||||
|
||||
if msg.channel.id in self.sessions:
|
||||
await ctx.send('Already running a REPL session in this channel. Exit it with `quit`.')
|
||||
return
|
||||
|
||||
self.sessions.add(msg.channel.id)
|
||||
await ctx.send('Enter code to execute or evaluate. `exit()` or `quit` to exit.')
|
||||
|
||||
def check(m):
|
||||
return m.author.id == msg.author.id and \
|
||||
m.channel.id == msg.channel.id and \
|
||||
m.content.startswith('`')
|
||||
|
||||
code = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
response = await ctx.bot.wait_for('message', check=check, timeout=10.0 * 60.0)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send('Exiting REPL session.')
|
||||
self.sessions.remove(msg.channel.id)
|
||||
break
|
||||
|
||||
cleaned = self.cleanup_code(response.content)
|
||||
|
||||
if cleaned in ('quit', 'exit', 'exit()'):
|
||||
await ctx.send('Exiting.')
|
||||
self.sessions.remove(msg.channel.id)
|
||||
return
|
||||
|
||||
executor = exec
|
||||
if cleaned.count('\n') == 0:
|
||||
# single statement, potentially 'eval'
|
||||
try:
|
||||
code = compile(cleaned, '<repl session>', 'eval')
|
||||
except SyntaxError:
|
||||
pass
|
||||
else:
|
||||
executor = eval
|
||||
|
||||
if executor is exec:
|
||||
try:
|
||||
code = compile(cleaned, '<repl session>', 'exec')
|
||||
except SyntaxError as e:
|
||||
await ctx.send(get_syntax_error(e))
|
||||
continue
|
||||
|
||||
variables['message'] = response
|
||||
|
||||
fmt = None
|
||||
stdout = io.StringIO()
|
||||
|
||||
try:
|
||||
with redirect_stdout(stdout):
|
||||
result = executor(code, variables)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
except Exception:
|
||||
value = stdout.getvalue()
|
||||
fmt = '```py\n{}{}\n```'.format(value, traceback.format_exc())
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
if result is not None:
|
||||
fmt = '```py\n{}{}\n```'.format(value, result)
|
||||
variables['_'] = result
|
||||
elif value:
|
||||
fmt = '```py\n{}\n```'.format(value)
|
||||
|
||||
try:
|
||||
if fmt is not None:
|
||||
if len(fmt) > 2000:
|
||||
await ctx.send('Content too big to be printed.')
|
||||
else:
|
||||
await ctx.send(fmt)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
except discord.HTTPException as e:
|
||||
await ctx.send('Unexpected error: `{}`'.format(e))
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def motd_push(self, *, message):
|
||||
"""Used to push a new message to the message of the day"""
|
||||
date = pendulum.utcnow().to_date_string()
|
||||
key = date
|
||||
entry = {'motd': message, 'date': date}
|
||||
# Try to add this, if there's an entry for that date, lets update it to make sure only one motd is sent a day
|
||||
# I should be managing this myself, more than one should not be sent in a day
|
||||
if await utils.add_content('motd', entry):
|
||||
await utils.update_content('motd', entry, key)
|
||||
await self.bot.say("New motd update for {}!".format(date))
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.check(utils.is_owner)
|
||||
async def debug(self, ctx, *, code : str):
|
||||
"""Evaluates code."""
|
||||
code = code.strip('` ')
|
||||
python = '```py\n{}\n```'
|
||||
result = None
|
||||
async def sendtochannel(self, ctx, cid: int, *, message):
|
||||
"""Sends a message to a provided channel, by ID"""
|
||||
channel = ctx.bot.get_channel(cid)
|
||||
await channel.send(message)
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
@commands.command()
|
||||
async def debug(self, ctx, *, body: str):
|
||||
env = {
|
||||
'bot': self.bot,
|
||||
'bot': ctx.bot,
|
||||
'ctx': ctx,
|
||||
'message': ctx.message,
|
||||
'server': ctx.message.server,
|
||||
'channel': ctx.message.channel,
|
||||
'author': ctx.message.author
|
||||
'author': ctx.message.author,
|
||||
'server': ctx.message.guild,
|
||||
'guild': ctx.message.guild,
|
||||
'message': ctx.message,
|
||||
'self': self,
|
||||
'_': self._last_result
|
||||
}
|
||||
|
||||
env.update(globals())
|
||||
|
||||
try:
|
||||
result = eval(code, env)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
except Exception as e:
|
||||
await self.bot.say(python.format(type(e).__name__ + ': ' + str(e)))
|
||||
return
|
||||
try:
|
||||
await self.bot.say(python.format(result))
|
||||
except discord.HTTPException:
|
||||
await self.bot.say("Result is too long for me to send")
|
||||
except:
|
||||
pass
|
||||
body = self.cleanup_code(body)
|
||||
stdout = io.StringIO()
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.check(utils.is_owner)
|
||||
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
|
||||
|
||||
try:
|
||||
exec(to_compile, env)
|
||||
except SyntaxError as e:
|
||||
return await ctx.send(get_syntax_error(e))
|
||||
|
||||
func = env['func']
|
||||
try:
|
||||
with redirect_stdout(stdout):
|
||||
ret = await func()
|
||||
except Exception:
|
||||
value = stdout.getvalue()
|
||||
await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```"[:2000])
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
try:
|
||||
await ctx.message.add_reaction('\u2705')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ret is None:
|
||||
if value:
|
||||
await ctx.send(f"```py\n{value}\n```"[:2000])
|
||||
else:
|
||||
self._last_result = ret
|
||||
await ctx.send(f"```py\n{value}{ret}\n```"[:2000])
|
||||
|
||||
@commands.command()
|
||||
async def bash(self, ctx, *, cmd: str):
|
||||
"""Runs a bash command"""
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT
|
||||
)
|
||||
stdout = (await proc.communicate())[0]
|
||||
if stdout:
|
||||
await ctx.send(f'[stdout]\n{stdout.decode()}')
|
||||
else:
|
||||
await ctx.send("Process finished, no output")
|
||||
|
||||
@commands.command()
|
||||
async def shutdown(self, ctx):
|
||||
"""Shuts the bot down"""
|
||||
fmt = 'Shutting down, I will miss you {0.author.name}'
|
||||
await self.bot.say(fmt.format(ctx.message))
|
||||
await self.bot.logout()
|
||||
await self.bot.close()
|
||||
await ctx.send(fmt.format(ctx.message))
|
||||
await ctx.bot.logout()
|
||||
await ctx.bot.close()
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def name(self, newNick: str):
|
||||
async def name(self, ctx, new_nick: str):
|
||||
"""Changes the bot's name"""
|
||||
await self.bot.edit_profile(username=newNick)
|
||||
await self.bot.say('Changed username to ' + newNick)
|
||||
await ctx.bot.user.edit(username=new_nick)
|
||||
await ctx.send('Changed username to ' + new_nick)
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def status(self, *, status: str):
|
||||
async def status(self, ctx, *, status: str):
|
||||
"""Changes the bot's 'playing' status"""
|
||||
await self.bot.change_status(discord.Game(name=status, type=0))
|
||||
await self.bot.say("Just changed my status to '{0}'!".format(status))
|
||||
await ctx.bot.change_presence(activity=discord.Game(name=status, type=0))
|
||||
await ctx.send("Just changed my status to '{}'!".format(status))
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def load(self, *, module: str):
|
||||
async def load(self, ctx, *, module: str):
|
||||
"""Loads a module"""
|
||||
|
||||
# Do this because I'm too lazy to type cogs.module
|
||||
|
@ -98,15 +230,14 @@ class Owner:
|
|||
|
||||
# This try catch will catch errors such as syntax errors in the module we are loading
|
||||
try:
|
||||
self.bot.load_extension(module)
|
||||
await self.bot.say("I have just loaded the {} module".format(module))
|
||||
ctx.bot.load_extension(module)
|
||||
await ctx.send("I have just loaded the {} module".format(module))
|
||||
except Exception as error:
|
||||
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
|
||||
await self.bot.say(fmt.format(type(error).__name__, error))
|
||||
await ctx.send(fmt.format(type(error).__name__, error))
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def unload(self, *, module: str):
|
||||
async def unload(self, ctx, *, module: str):
|
||||
"""Unloads a module"""
|
||||
|
||||
# Do this because I'm too lazy to type cogs.module
|
||||
|
@ -114,27 +245,26 @@ class Owner:
|
|||
if not module.startswith("cogs"):
|
||||
module = "cogs.{}".format(module)
|
||||
|
||||
self.bot.unload_extension(module)
|
||||
await self.bot.say("I have just unloaded the {} module".format(module))
|
||||
ctx.bot.unload_extension(module)
|
||||
await ctx.send("I have just unloaded the {} module".format(module))
|
||||
|
||||
@commands.command()
|
||||
@commands.check(utils.is_owner)
|
||||
async def reload(self, *, module: str):
|
||||
async def reload(self, ctx, *, module: str):
|
||||
"""Reloads a module"""
|
||||
|
||||
# Do this because I'm too lazy to type cogs.module
|
||||
module = module.lower()
|
||||
if not module.startswith("cogs"):
|
||||
module = "cogs.{}".format(module)
|
||||
self.bot.unload_extension(module)
|
||||
ctx.bot.unload_extension(module)
|
||||
|
||||
# This try block will catch errors such as syntax errors in the module we are loading
|
||||
try:
|
||||
self.bot.load_extension(module)
|
||||
await self.bot.say("I have just reloaded the {} module".format(module))
|
||||
ctx.bot.load_extension(module)
|
||||
await ctx.send("I have just reloaded the {} module".format(module))
|
||||
except Exception as error:
|
||||
fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```'
|
||||
await self.bot.say(fmt.format(type(error).__name__, error))
|
||||
await ctx.send(fmt.format(type(error).__name__, error))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
315
cogs/picarto.py
|
@ -1,242 +1,111 @@
|
|||
import aiohttp
|
||||
import asyncio
|
||||
import discord
|
||||
import re
|
||||
import rethinkdb as r
|
||||
import traceback
|
||||
import logging
|
||||
import utils
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
log = logging.getLogger()
|
||||
BASE_URL = 'https://ptvappapi.picarto.tv'
|
||||
|
||||
# This is a public key for use, I don't care if this is seen
|
||||
api_key = '03e26294-b793-11e5-9a41-005056984bd4'
|
||||
BASE_URL = 'https://api.picarto.tv/v1'
|
||||
|
||||
|
||||
class Picarto:
|
||||
def produce_embed(*channels):
|
||||
description = ""
|
||||
# Loop through each channel and produce the information that will go in the description
|
||||
for channel in channels:
|
||||
url = f"https://picarto.tv/{channel.get('name')}"
|
||||
description = f"""{description}\n\n**Title:** [{channel.get("title")}]({url})
|
||||
**Channel:** [{channel.get("name")}]({url})
|
||||
**Adult:** {"Yes" if channel.get("adult") else "No"}
|
||||
**Gaming:** {"Yes" if channel.get("gaming") else "No"}
|
||||
**Commissions:** {"Yes" if channel.get("commissions") else "No"}"""
|
||||
|
||||
return discord.Embed(title="Channels that have gone online!", description=description.strip())
|
||||
|
||||
|
||||
class Picarto(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.task = self.bot.loop.create_task(self.picarto_task())
|
||||
self.channel_info = {}
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
async def get_online_users(self):
|
||||
# This method is in place to just return all online users so we can compare against it
|
||||
url = BASE_URL + '/online/all'
|
||||
payload = {'key': api_key}
|
||||
self.online_channels = await utils.request(url, payload=payload)
|
||||
url = BASE_URL + '/online'
|
||||
payload = {
|
||||
'adult': 'true',
|
||||
'gaming': 'true'
|
||||
}
|
||||
channel_info = {}
|
||||
channels = await utils.request(url, payload=payload)
|
||||
if channels and isinstance(channels, (list, set, tuple)) and len(channels) > 0:
|
||||
for channel in channels:
|
||||
name = channel["name"]
|
||||
previous = self.channel_info.get(name)
|
||||
# There are three statuses, on, remained, and off
|
||||
# On means they were off previously, but are now online
|
||||
# Remained means they were on previous, and are still on
|
||||
# Off means they were on preivous, but are now offline
|
||||
# If they weren't included in the online channels...well they're off
|
||||
if previous is None:
|
||||
channel_info[name] = channel
|
||||
channel_info[name]["status"] = "on"
|
||||
elif previous["status"] in ["on", "remaining"]:
|
||||
channel_info[name] = channel
|
||||
channel_info[name]["status"] = "remaining"
|
||||
# After loop has finished successfully, we want to override the statuses of the channels
|
||||
self.channel_info = channel_info
|
||||
|
||||
def channel_online(self, channel):
|
||||
# Channel is the name we are checking against that
|
||||
# This creates a list of all users that match this channel name (should only ever be 1)
|
||||
# And returns True as long as it is more than 0
|
||||
channel = re.search("(?<=picarto.tv/)(.*)", channel).group(1)
|
||||
matches = [stream for stream in self.online_channels if stream['channel_name'].lower() == channel.lower()]
|
||||
return len(matches) > 0
|
||||
async def picarto_task(self):
|
||||
# The first time we setup this task, if we leave an empty dict as channel_info....it will announce anything
|
||||
# So what we want to do here, is get everyone who is online now before starting the actual check
|
||||
# Also wait a little before starting
|
||||
await self.bot.wait_until_ready()
|
||||
await self.get_online_users()
|
||||
await asyncio.sleep(30)
|
||||
# Now that we've done the initial setup, start the actual loop we'll use
|
||||
try:
|
||||
while not self.bot.is_closed():
|
||||
await self.check_channels()
|
||||
await asyncio.sleep(30)
|
||||
except Exception as error:
|
||||
with open("error_log", 'a') as f:
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error), file=f)
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def check_channels(self):
|
||||
await self.bot.wait_until_ready()
|
||||
# This is a loop that runs every 30 seconds, checking if anyone has gone online
|
||||
try:
|
||||
while not self.bot.is_closed:
|
||||
await self.get_online_users()
|
||||
picarto = await utils.filter_content('picarto', {'notifications_on': 1})
|
||||
for data in picarto:
|
||||
m_id = data['member_id']
|
||||
url = data['picarto_url']
|
||||
# Check if they are online
|
||||
online = self.channel_online(url)
|
||||
# If they're currently online, but saved as not then we'll send our notification
|
||||
if online and data['live'] == 0:
|
||||
for s_id in data['servers']:
|
||||
server = self.bot.get_server(s_id)
|
||||
if server is None:
|
||||
continue
|
||||
member = server.get_member(m_id)
|
||||
if member is None:
|
||||
continue
|
||||
server_settings = await utils.get_content('server_settings', s_id)
|
||||
if server_settings is not None:
|
||||
channel_id = server_settings.get('notification_channel', s_id)
|
||||
else:
|
||||
channel_id = s_id
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['picarto_url']))
|
||||
self.bot.loop.create_task(utils.update_content('picarto', {'live': 1}, m_id))
|
||||
elif not online and data['live'] == 1:
|
||||
for s_id in data['servers']:
|
||||
server = self.bot.get_server(s_id)
|
||||
if server is None:
|
||||
continue
|
||||
member = server.get_member(m_id)
|
||||
if member is None:
|
||||
continue
|
||||
server_settings = await utils.get_content('server_settings', s_id)
|
||||
if server_settings is not None:
|
||||
channel_id = server_settings.get('notification_channel', s_id)
|
||||
else:
|
||||
channel_id = s_id
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['picarto_url']))
|
||||
self.bot.loop.create_task(utils.update_content('picarto', {'live': 0}, m_id))
|
||||
await asyncio.sleep(30)
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e)
|
||||
log.error(fmt)
|
||||
|
||||
@commands.group(invoke_without_command=True, no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def picarto(self, ctx, member: discord.Member = None):
|
||||
"""This command can be used to view Picarto stats about a certain member
|
||||
|
||||
EXAMPLE: !picarto @otherPerson
|
||||
RESULT: Info about their picarto stream"""
|
||||
|
||||
# If member is not given, base information on the author
|
||||
member = member or ctx.message.author
|
||||
picarto_entry = await utils.get_content('picarto', member.id)
|
||||
if picarto_entry is None:
|
||||
await self.bot.say("That user does not have a picarto url setup!")
|
||||
return
|
||||
|
||||
member_url = picarto_entry['picarto_url']
|
||||
|
||||
# Use regex to get the actual username so that we can make a request to the API
|
||||
stream = re.search("(?<=picarto.tv/)(.*)", member_url).group(1)
|
||||
url = BASE_URL + '/channel/{}'.format(stream)
|
||||
payload = {'key': api_key}
|
||||
|
||||
data = await utils.request(url, payload=payload)
|
||||
if data is None:
|
||||
await self.bot.say("I couldn't connect to Picarto!")
|
||||
return
|
||||
|
||||
# Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it
|
||||
things_to_print = ['channel', 'commissions_enabled', 'is_nsfw', 'program', 'tablet', 'followers',
|
||||
'content_type']
|
||||
|
||||
embed = discord.Embed(title='{}\'s Picarto'.format(data['channel']), url=url)
|
||||
if data['avatar_url']:
|
||||
embed.set_thumbnail(url=data['avatar_url'])
|
||||
|
||||
for i, result in data.items():
|
||||
if i in things_to_print and str(result):
|
||||
i = i.title().replace('_', ' ')
|
||||
embed.add_field(name=i, value=str(result))
|
||||
|
||||
# Social URL's can be given if a user wants them to show
|
||||
# Print them if they exist, otherwise don't try to include them
|
||||
for i, result in data['social_urls'].items():
|
||||
embed.add_field(name=i.title(), value=result)
|
||||
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
@picarto.command(name='add', no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def add_picarto_url(self, ctx, url: str):
|
||||
"""Saves your user's picarto URL
|
||||
|
||||
EXAMPLE: !picarto add MyUsername
|
||||
RESULT: Your picarto stream is saved, and notifications should go to this server"""
|
||||
|
||||
# This uses a lookbehind to check if picarto.tv exists in the url given
|
||||
# If it does, it matches picarto.tv/user and sets the url as that
|
||||
# Then (in the else) add https://www. to that
|
||||
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
|
||||
# This means that the url was just given as a user (or something complete invalid)
|
||||
# So set URL as https://www.picarto.tv/[url]
|
||||
# Even if this was invalid such as https://www.picarto.tv/picarto.tv/user
|
||||
# For example, our next check handles that
|
||||
try:
|
||||
url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0)
|
||||
except AttributeError:
|
||||
url = "https://www.picarto.tv/{}".format(url)
|
||||
else:
|
||||
url = "https://www.{}".format(url)
|
||||
channel = re.search("https://www.picarto.tv/(.*)", url).group(1)
|
||||
api_url = BASE_URL + '/channel/{}'.format(channel)
|
||||
payload = {'key': api_key}
|
||||
|
||||
data = await utils.request(api_url, payload=payload)
|
||||
if not data:
|
||||
await self.bot.say("That Picarto user does not exist! What would be the point of adding a nonexistant "
|
||||
"Picarto user? Silly")
|
||||
return
|
||||
|
||||
key = ctx.message.author.id
|
||||
entry = {'picarto_url': url,
|
||||
'servers': [ctx.message.server.id],
|
||||
'notifications_on': 1,
|
||||
'live': 0,
|
||||
'member_id': key}
|
||||
if await utils.add_content('picarto', entry):
|
||||
await self.bot.say(
|
||||
"I have just saved your Picarto URL {}, this server will now be notified when you go live".format(
|
||||
ctx.message.author.mention))
|
||||
else:
|
||||
await utils.update_content('picarto', {'picarto_url': url}, key)
|
||||
await self.bot.say("I have just updated your Picarto URL")
|
||||
|
||||
@picarto.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def remove_picarto_url(self, ctx):
|
||||
"""Removes your picarto URL"""
|
||||
if await utils.remove_content('picarto', ctx.message.author.id):
|
||||
await self.bot.say("I am no longer saving your picarto URL {}".format(ctx.message.author.mention))
|
||||
else:
|
||||
await self.bot.say(
|
||||
"I do not have your picarto URL added {}. You can save your picarto url with {}picarto add".format(
|
||||
ctx.message.author.mention, ctx.prefix))
|
||||
|
||||
@picarto.group(no_pm=True, invoke_without_command=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify(self, ctx):
|
||||
"""This can be used to turn picarto notifications on or off
|
||||
Call this command by itself, to add this server to the list of servers to be notified
|
||||
|
||||
EXAMPLE: !picarto notify
|
||||
RESULT: This server will now be notified of you going live"""
|
||||
key = ctx.message.author.id
|
||||
result = await utils.get_content('picarto', key)
|
||||
# Check if this user is saved at all
|
||||
if result is None:
|
||||
await self.bot.say(
|
||||
"I do not have your Picarto URL added {}. You can save your Picarto url with !picarto add".format(
|
||||
ctx.message.author.mention))
|
||||
# Then check if this server is already added as one to notify in
|
||||
elif ctx.message.server.id in result['servers']:
|
||||
await self.bot.say("I am already set to notify in this server...")
|
||||
else:
|
||||
await utils.update_content('picarto', {'servers': r.row['servers'].append(ctx.message.server.id)}, key)
|
||||
|
||||
@notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify_on(self, ctx):
|
||||
"""Turns picarto notifications on
|
||||
|
||||
EXAMPLE: !picarto notify on
|
||||
RESULT: Notifications are sent when you go live"""
|
||||
await utils.update_content('picarto', {'notifications_on': 1}, ctx.message.author.id)
|
||||
await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format(
|
||||
ctx.message.author.mention))
|
||||
|
||||
@notify.command(name='off', aliases=['stop,no'], pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify_off(self, ctx):
|
||||
"""Turns picarto notifications off
|
||||
|
||||
EXAMPLE: !picarto notify off
|
||||
RESULT: No more notifications sent when you go live"""
|
||||
await utils.update_content('picarto', {'notifications_on': 0}, ctx.message.author.id)
|
||||
await self.bot.say(
|
||||
"I will not notify if you go live anymore {}, "
|
||||
"are you going to stream some lewd stuff you don't want people to see?~".format(
|
||||
ctx.message.author.mention))
|
||||
query = """
|
||||
SELECT
|
||||
id, followed_picarto_channels, COALESCE(picarto_alerts, default_alerts) AS channel
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
COALESCE(picarto_alerts, default_alerts) IS NOT NULL
|
||||
"""
|
||||
# Recheck who is currently online
|
||||
await self.get_online_users()
|
||||
# Now get all guilds and their picarto channels they follow and loop through them
|
||||
results = await self.bot.db.fetch(query) or []
|
||||
for result in results:
|
||||
# Get all the channels that have gone online
|
||||
gone_online = [
|
||||
self.channel_info.get(name)
|
||||
for name in result["followed_picarto_channels"]
|
||||
if self.channel_info.get(name, {}).get("status", "off") == "on"
|
||||
]
|
||||
# If they've gone online, produce the embed for them and send it
|
||||
if gone_online:
|
||||
embed = produce_embed(*gone_online)
|
||||
channel = self.bot.get_channel(result["channel"])
|
||||
if channel is not None:
|
||||
try:
|
||||
await channel.send(embed=embed)
|
||||
except (discord.Forbidden, discord.HTTPException, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def setup(bot):
|
||||
p = Picarto(bot)
|
||||
bot.loop.create_task(p.check_channels())
|
||||
bot.add_cog(Picarto(bot))
|
||||
bot.add_cog(Picarto(bot))
|
||||
|
|
102
cogs/polls.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
import discord
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
def to_keycap(c):
|
||||
return '\N{KEYCAP TEN}' if c == 10 else str(c) + '\u20e3'
|
||||
|
||||
|
||||
class Poll:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
return self.message.guild
|
||||
|
||||
@property
|
||||
def creator(self):
|
||||
return self.message.author
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self.message.reactions
|
||||
|
||||
async def update_message(self):
|
||||
self.message = await self.message.channel.fetch_message(self.message.id)
|
||||
|
||||
async def remove_other_reaction(self, reaction, member):
|
||||
"""Ensures that this is the only reaction set for this user"""
|
||||
for r in self.options:
|
||||
if r.emoji == reaction.emoji:
|
||||
continue
|
||||
users = await r.users().flatten()
|
||||
if member.id in [x.id for x in users]:
|
||||
try:
|
||||
await self.message.remove_reaction(r, member)
|
||||
except discord.Forbidden:
|
||||
return
|
||||
|
||||
|
||||
class Polls(commands.Cog):
|
||||
"""Create custom polls that can be tracked through reactions"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.polls = []
|
||||
|
||||
async def create_poll(self, ctx, content):
|
||||
question, *options = content.split("\n")
|
||||
if len(options) == 0 or len(options) > 10:
|
||||
await ctx.send("Please provide between 1-10 options")
|
||||
else:
|
||||
fmt = "{} asked: {}\n".format(ctx.message.author.display_name, question)
|
||||
fmt += "\n".join(
|
||||
"{}: {}".format(to_keycap(i + 1), opt)
|
||||
for i, opt in enumerate(options)
|
||||
)
|
||||
|
||||
msg = await ctx.send(fmt)
|
||||
for i, _ in enumerate(options):
|
||||
await msg.add_reaction(to_keycap(i + 1))
|
||||
p = Poll(msg)
|
||||
await p.update_message()
|
||||
self.polls.append(p)
|
||||
|
||||
def get_poll(self, message):
|
||||
for p in self.polls:
|
||||
if p.message.id == message.id:
|
||||
return p
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_reaction_add(self, reaction, user):
|
||||
if user.id == self.bot.user.id:
|
||||
return
|
||||
poll = self.get_poll(reaction.message)
|
||||
if poll:
|
||||
try:
|
||||
await poll.remove_other_reaction(reaction, user)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def poll(self, ctx, *, question):
|
||||
"""Sets up a poll based on the question that you have provided.
|
||||
Provide the question on the first line and the options on the following lines
|
||||
|
||||
EXAMPLE: !poll This is my poll
|
||||
Option 1
|
||||
Option 2
|
||||
Option 3
|
||||
RESULT: A new poll people can vote on"""
|
||||
await self.create_poll(ctx, question)
|
||||
await ctx.message.delete()
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Polls(bot))
|
324
cogs/raffle.py
|
@ -1,239 +1,191 @@
|
|||
from discord.ext import commands
|
||||
from . import utils
|
||||
from collections import defaultdict
|
||||
|
||||
import utils
|
||||
|
||||
import discord
|
||||
import random
|
||||
import pendulum
|
||||
import re
|
||||
import asyncio
|
||||
import traceback
|
||||
import random
|
||||
|
||||
|
||||
class Raffle:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.bot.loop.create_task(self.raffle_task())
|
||||
class Raffle(commands.Cog):
|
||||
"""Used to hold custom raffles"""
|
||||
raffles = defaultdict(list)
|
||||
|
||||
async def raffle_task(self):
|
||||
while True:
|
||||
try:
|
||||
await self.check_raffles()
|
||||
except Exception as error:
|
||||
with open("error_log", 'a') as f:
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error), file=f)
|
||||
await asyncio.sleep(900)
|
||||
def create_raffle(self, ctx, title, num):
|
||||
raffle = GuildRaffle(ctx, title, num)
|
||||
self.raffles[ctx.guild.id].append(raffle)
|
||||
raffle.start()
|
||||
|
||||
async def check_raffles(self):
|
||||
# This is used to periodically check the current raffles, and see if they have ended yet
|
||||
# If the raffle has ended, we'll pick a winner from the entrants
|
||||
raffles = await utils.get_content('raffles')
|
||||
|
||||
if raffles is None:
|
||||
return
|
||||
|
||||
for raffle in raffles:
|
||||
server = self.bot.get_server(raffle['server_id'])
|
||||
|
||||
# Check to see if this cog can find the server in question
|
||||
if server is None:
|
||||
continue
|
||||
|
||||
now = pendulum.utcnow()
|
||||
expires = pendulum.parse(raffle['expires'])
|
||||
|
||||
# Now lets compare and see if this raffle has ended, if not just continue
|
||||
if expires > now:
|
||||
continue
|
||||
|
||||
title = raffle['title']
|
||||
entrants = raffle['entrants']
|
||||
raffle_id = raffle['id']
|
||||
|
||||
# Make sure there are actually entrants
|
||||
if len(entrants) == 0:
|
||||
fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title)
|
||||
else:
|
||||
winner = None
|
||||
count = 0
|
||||
while winner is None:
|
||||
winner = server.get_member(random.SystemRandom().choice(entrants))
|
||||
|
||||
# Lets make sure we don't get caught in an infinite loop
|
||||
# Realistically having more than 25 random entrants found that aren't in the server anymore
|
||||
# Isn't something that should be an issue
|
||||
count += 1
|
||||
if count >= 25:
|
||||
break
|
||||
|
||||
if winner is None:
|
||||
fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format(title)
|
||||
else:
|
||||
fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name)
|
||||
|
||||
# No matter which one of these matches were met, the raffle has ended and we want to remove it
|
||||
# We don't have to wait for it however, so create a task for it
|
||||
self.bot.loop.create_task(utils.remove_content('raffles', raffle_id ))
|
||||
|
||||
server_settings = await utils.get_content('server_settings', str(server.id))
|
||||
channel_id = server_settings.get('notification_channel', server.id)
|
||||
channel = self.bot.get_channel(channel_id)
|
||||
try:
|
||||
await channel.send(fmt)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def raffles(self, ctx):
|
||||
@commands.command(name="raffles")
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def _raffles(self, ctx):
|
||||
"""Used to print the current running raffles on the server
|
||||
|
||||
EXAMPLE: !raffles
|
||||
RESULT: A list of the raffles setup on this server"""
|
||||
r_filter = {'server_id': ctx.message.server.id}
|
||||
raffles = await utils.filter_content('raffles', r_filter)
|
||||
if raffles is None:
|
||||
await self.bot.say("There are currently no raffles setup on this server!")
|
||||
raffles = self.raffles[ctx.guild.id]
|
||||
if len(raffles) == 0:
|
||||
await ctx.send("There are currently no raffles setup on this server!")
|
||||
return
|
||||
|
||||
fmt = "\n\n".join("**Raffle:** {}\n**Title:** {}\n**Total Entrants:** {}\n**Ends:** {} UTC".format(
|
||||
num + 1,
|
||||
raffle['title'],
|
||||
len(raffle['entrants']),
|
||||
raffle['expires']) for num, raffle in enumerate(raffles))
|
||||
await self.bot.say(fmt)
|
||||
embed = discord.Embed(title=f"Raffles in {ctx.guild.name}")
|
||||
|
||||
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def raffle(self, ctx, raffle_id: int = 0):
|
||||
for num, raffle in enumerate(raffles):
|
||||
embed.add_field(
|
||||
name=f"Raffle {num + 1}",
|
||||
value=f"Title: {raffle.title}\n"
|
||||
f"Total Entrants: {len(raffle.entrants)}\n"
|
||||
f"Ends in {raffle.remaining}",
|
||||
inline=False
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def raffle(self, ctx, raffle_id: int):
|
||||
"""Used to enter a raffle running on this server
|
||||
If there is more than one raffle running, provide an ID of the raffle you want to enter
|
||||
|
||||
EXAMPLE: !raffle 1
|
||||
RESULT: You've entered the first raffle!"""
|
||||
# Lets let people use 1 - (length of raffles) and handle 0 base ourselves
|
||||
raffle_id -= 1
|
||||
r_filter = {'server_id': ctx.message.server.id}
|
||||
author = ctx.message.author
|
||||
|
||||
raffles = await utils.filter_content('raffles', r_filter)
|
||||
if raffles is None:
|
||||
await self.bot.say("There are currently no raffles setup on this server!")
|
||||
return
|
||||
|
||||
raffle_count = len(raffles)
|
||||
|
||||
# There is only one raffle, so use the first's info
|
||||
if raffle_count == 1:
|
||||
entrants = raffles[0]['entrants']
|
||||
# Lets make sure that the user hasn't already entered the raffle
|
||||
if author.id in entrants:
|
||||
await self.bot.say("You have already entered this raffle!")
|
||||
return
|
||||
entrants.append(author.id)
|
||||
|
||||
# Since we have no good thing to filter things off of, lets use the internal rethinkdb id
|
||||
update = {'entrants': entrants}
|
||||
await utils.update_content('raffles', update, raffles[0]['id'])
|
||||
await self.bot.say("{} you have just entered the raffle!".format(author.mention))
|
||||
# Otherwise, make sure the author gave a valid raffle_id
|
||||
elif raffle_id in range(raffle_count - 1):
|
||||
entrants = raffles[raffle_id]['entrants']
|
||||
|
||||
# Lets make sure that the user hasn't already entered the raffle
|
||||
if author.id in entrants:
|
||||
await self.bot.say("You have already entered this raffle!")
|
||||
return
|
||||
entrants.append(author.id)
|
||||
|
||||
update = {'entrants': entrants}
|
||||
await utils.update_content('raffles', update, raffles[raffle_id]['id'])
|
||||
await self.bot.say("{} you have just entered the raffle!".format(author.mention))
|
||||
try:
|
||||
raffle = self.raffles[ctx.guild.id][raffle_id - 1]
|
||||
except IndexError:
|
||||
await ctx.send(f"I could not find a raffle for ID {raffle_id}")
|
||||
await self._raffles.invoke(ctx)
|
||||
else:
|
||||
fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \
|
||||
"There are currently `{}` raffles running, use {}raffles to view the current running raffles".format(
|
||||
raffle_count, ctx.prefix)
|
||||
await self.bot.say(fmt)
|
||||
if raffle.enter(ctx.author):
|
||||
await ctx.send(f"You have just joined the raffle {raffle['title']}")
|
||||
else:
|
||||
await ctx.send("You have already entered this raffle!")
|
||||
|
||||
@raffle.command(pass_context=True, no_pm=True, name='create', aliases=['start', 'begin', 'add'])
|
||||
@utils.custom_perms(kick_members=True)
|
||||
@raffle.command(name='create', aliases=['start', 'begin', 'add'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def raffle_create(self, ctx):
|
||||
"""This is used in order to create a new server raffle
|
||||
|
||||
EXAMPLE: !raffle create
|
||||
RESULT: A follow-along for setting up a new raffle"""
|
||||
|
||||
author = ctx.message.author
|
||||
server = ctx.message.server
|
||||
channel = ctx.message.channel
|
||||
now = pendulum.utcnow()
|
||||
author = ctx.author
|
||||
channel = ctx.channel
|
||||
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
"Ready to start a new raffle! Please respond with the title you would like to use for this raffle!")
|
||||
|
||||
msg = await self.bot.wait_for_message(author=author, channel=channel, timeout=120)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long! >:c")
|
||||
check = lambda m: m.author == author and m.channel == channel
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=120)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long! >:c")
|
||||
return
|
||||
|
||||
title = msg.content
|
||||
|
||||
fmt = "Alright, your new raffle will be titled:\n\n{}\n\nHow long would you like this raffle to run for? " \
|
||||
"The format should be [number] [length] for example, `2 days` or `1 hour` or `30 minutes` etc. " \
|
||||
"The minimum for this is 10 minutes, and the maximum is 3 months"
|
||||
await self.bot.say(fmt.format(title))
|
||||
"The minimum for this is 10 minutes, and the maximum is 3 days"
|
||||
await ctx.send(fmt.format(title))
|
||||
|
||||
# Our check to ensure that a proper length of time was passed
|
||||
check = lambda m: re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", m.content.lower()) is not None
|
||||
msg = await self.bot.wait_for_message(author=author, channel=channel, timeout=120, check=check)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long! >:c")
|
||||
def check(m):
|
||||
if m.author == author and m.channel == channel:
|
||||
return re.search("\d+ (minutes?|hours?|days?)", m.content.lower()) is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=120, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long! >:c")
|
||||
return
|
||||
|
||||
# Lets get the length provided, based on the number and type passed
|
||||
num, term = re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", msg.content.lower()).group(0).split(' ')
|
||||
num, term = re.search("(\d+) (minutes?|hours?|days?)", msg.content.lower()).groups()
|
||||
# This should be safe to convert, we already made sure with our check earlier this would match
|
||||
num = int(num)
|
||||
|
||||
# Now lets ensure this meets our min/max
|
||||
if "minute" in term and (num < 10 or num > 129600):
|
||||
await self.bot.say(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
|
||||
return
|
||||
elif "hour" in term and num > 2160:
|
||||
await self.bot.say(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
|
||||
return
|
||||
elif "day" in term and num > 90:
|
||||
await self.bot.say(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
|
||||
return
|
||||
elif "week" in term and num > 12:
|
||||
await self.bot.say(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
|
||||
return
|
||||
elif "month" in term and num > 3:
|
||||
await self.bot.say(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
|
||||
if "minute" in term:
|
||||
num = num * 60
|
||||
elif "hour" in term:
|
||||
num = num * 60 * 60
|
||||
elif "day" in term:
|
||||
num = num * 24 * 60 * 60
|
||||
|
||||
if 60 < num < 259200:
|
||||
await ctx.send(
|
||||
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 days")
|
||||
return
|
||||
|
||||
# Pendulum only accepts the plural version of terms, lets make sure this is added
|
||||
term = term if term.endswith('s') else '{}s'.format(term)
|
||||
# If we're in the range, lets just pack this in a dictionary we can pass to set the time we want, then set that
|
||||
payload = {term: num}
|
||||
expires = now.add(**payload)
|
||||
|
||||
# Now we're ready to add this as a new raffle
|
||||
entry = {'title': title,
|
||||
'expires': expires.to_datetime_string(),
|
||||
'entrants': [],
|
||||
'author': author.id,
|
||||
'server_id': server.id}
|
||||
|
||||
# We don't want to pass a filter to this, because we can have multiple raffles per server
|
||||
await utils.add_content('raffles', entry)
|
||||
await self.bot.say("I have just saved your new raffle!")
|
||||
self.create_raffle(ctx, title, num)
|
||||
await ctx.send("I have just saved your new raffle!")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Raffle(bot))
|
||||
|
||||
|
||||
class GuildRaffle:
|
||||
|
||||
def __init__(self, ctx, title, expires):
|
||||
self._ctx = ctx
|
||||
self.title = title
|
||||
self.expires = expires
|
||||
self.entrants = set()
|
||||
self.task = None
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
return self._ctx.guild
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self._ctx.bot.db
|
||||
|
||||
def start(self):
|
||||
self.task = self._ctx.bot.loop.call_later(self.expires, self.end_raffle())
|
||||
|
||||
@property
|
||||
def remaining(self):
|
||||
minutes, seconds = divmod(self.task.when(), 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
return f"{days} days, {hours} hours, {minutes} minutes, {seconds} seconds"
|
||||
|
||||
def enter(self, entrant):
|
||||
self.entrants.add(entrant)
|
||||
|
||||
async def end_raffle(self):
|
||||
entrants = {e for e in self.entrants if self.guild.get_member(e.id)}
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
COALESCE(raffle_alerts, default_alerts) AS channel,
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
id = $1
|
||||
AND
|
||||
COALESCE(raffle_alerts, default_alerts) IS NOT NULL
|
||||
"""
|
||||
channel = None
|
||||
result = await self.db.fetch(query, self.guild.id)
|
||||
|
||||
if result:
|
||||
channel = self.guild.get_channel(result['channel'])
|
||||
if channel is None:
|
||||
return
|
||||
|
||||
if entrants:
|
||||
winner = random.SystemRandom().choice(self.entrants)
|
||||
await channel.send(f"The winner of the raffle `{self.title}` is {winner.mention}! Congratulations!")
|
||||
else:
|
||||
await channel.send(
|
||||
f"There were no entrants to the raffle `{self.title}`, who are in this server currently!"
|
||||
)
|
||||
|
|
385
cogs/roles.py
|
@ -1,65 +1,146 @@
|
|||
from discord.ext import commands
|
||||
import discord
|
||||
from .utils import checks
|
||||
|
||||
import utils
|
||||
|
||||
import re
|
||||
import asyncio
|
||||
|
||||
|
||||
class Roles:
|
||||
class Roles(commands.Cog):
|
||||
"""Class to handle management of roles on the server"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
@commands.command(aliases=['color'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def colour(self, ctx, role_colour: discord.Colour):
|
||||
"""Used to give yourself a role matching the colour given.
|
||||
If the role doesn't exist, it will be created. Names such as red, blue, yellow, etc. can be used.
|
||||
Additionally, hex codes can be used as well
|
||||
|
||||
@commands.group(aliases=['roles'], invoke_without_command=True, no_pm=True, pass_context=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def role(self, ctx):
|
||||
EXAMPLE: !colour red
|
||||
RESULT: A role that matches red (#e74c3c) will be given to you"""
|
||||
result = await ctx.bot.db.fetchrow("SELECT colour_roles FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
if result and not result["colour_roles"]:
|
||||
await ctx.send("Colour roles not allowed on this server! "
|
||||
"The command `allowcolours` must be ran to enable them!")
|
||||
return
|
||||
|
||||
if not ctx.me.guild_permissions.manage_roles:
|
||||
await ctx.send("Error: I need manage_roles to be able to use this command")
|
||||
return
|
||||
|
||||
# The convention we'll use for the name
|
||||
name = "Bonfire {}".format(role_colour)
|
||||
|
||||
# Try to find a role that matches our convention, Name #000000 with the colour matching
|
||||
role = discord.utils.get(ctx.guild.roles, name=name, colour=role_colour)
|
||||
|
||||
# The colour roles they currently have, we need to remove them if they want a new colour
|
||||
old_roles = [r for r in ctx.author.roles if re.match(r'Bonfire #[0-9a-zA-Z]+', r.name)]
|
||||
if old_roles:
|
||||
await ctx.author.remove_roles(*old_roles)
|
||||
|
||||
# If the role doesn't exist, we need to create it
|
||||
if not role:
|
||||
opts = {
|
||||
"name": name,
|
||||
"colour": role_colour
|
||||
}
|
||||
try:
|
||||
role = await ctx.guild.create_role(**opts)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("{} is not a valid colour".format(role_colour))
|
||||
return
|
||||
# Now add the role
|
||||
await ctx.author.add_roles(role)
|
||||
|
||||
await ctx.send("I have just given you your requested colour!")
|
||||
|
||||
@commands.group(aliases=['roles'], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def role(self, ctx, *, role: discord.Role=None):
|
||||
"""This command can be used to modify the roles on the server.
|
||||
Pass no subcommands and this will print the roles currently available on this server
|
||||
If you give a role as the argument then it will give some information about that role
|
||||
|
||||
EXAMPLE: !role
|
||||
RESULT: A list of all your roles"""
|
||||
# Simply get a list of all roles in this server and send them
|
||||
server_roles = [role.name for role in ctx.message.server.roles if not role.is_everyone]
|
||||
await self.bot.say("Your server's roles are: ```\n{}```".format("\n".join(server_roles)))
|
||||
|
||||
@role.command(name='remove', pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(manage_roles=True)
|
||||
if role:
|
||||
# Create the embed object
|
||||
opts = {
|
||||
"title": role.name,
|
||||
"colour": role.colour,
|
||||
}
|
||||
if role.managed:
|
||||
opts["description"] = "This role is managed by a third party service"
|
||||
embed = discord.Embed(**opts)
|
||||
# Add details to it
|
||||
embed.add_field(name="Created", value=role.created_at.date())
|
||||
embed.add_field(name="Mentionable", value="Yes" if role.mentionable else "No")
|
||||
total_members = len(role.members)
|
||||
embed.add_field(name="Total members", value=str(total_members))
|
||||
# If there are only a few members in this role, display them
|
||||
if 5 >= total_members > 0:
|
||||
embed.add_field(name="Members", value="\n".join(m.display_name for m in role.members))
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
# Don't include the colour roles
|
||||
colour_role = re.compile("Bonfire #.+")
|
||||
# Simply get a list of all roles in this server and send them
|
||||
entries = [r.name for r in ctx.guild.roles[1:] if not colour_role.match(r.name)]
|
||||
if len(entries) == 0:
|
||||
await ctx.send("You do not have any roles setup on this server, other than the default role!")
|
||||
return
|
||||
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
|
||||
@role.command(name='remove')
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_roles=True)
|
||||
async def remove_role(self, ctx):
|
||||
"""Use this to remove roles from a number of members
|
||||
|
||||
EXAMPLE: !role remove @Jim @Bot @Joe
|
||||
RESULT: A follow-along to remove the role(s) you want to, from these 3 members"""
|
||||
# No use in running through everything if the bot cannot manage roles
|
||||
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
|
||||
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await ctx.send("I can't manage roles in this server, do you not trust me? :c")
|
||||
return
|
||||
check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel
|
||||
|
||||
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
|
||||
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
|
||||
# First get the list of all mentioned users
|
||||
members = ctx.message.mentions
|
||||
# If no users are mentioned, ask the author for a list of the members they want to remove the role from
|
||||
if len(members) == 0:
|
||||
await self.bot.say("Please provide the list of members you want to remove a role from")
|
||||
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Please provide the list of members you want to remove a role from")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
if len(msg.mentions) == 0:
|
||||
await self.bot.say("I cannot remove a role from someone if you don't provide someone...")
|
||||
await ctx.send("I cannot remove a role from someone if you don't provide someone...")
|
||||
return
|
||||
# Override members if everything has gone alright, and then continue
|
||||
members = msg.mentions
|
||||
|
||||
# This allows the user to remove multiple roles from the list of users, if they want.
|
||||
await self.bot.say("Alright, please provide the roles you would like to remove from this member. "
|
||||
"Make sure the roles, if more than one is provided, are separate by commas. "
|
||||
"Here is a list of this server's roles:"
|
||||
"```\n{}```".format("\n".join([r.name for r in server_roles])))
|
||||
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Alright, please provide the roles you would like to remove from this member. "
|
||||
"Make sure the roles, if more than one is provided, are separate by commas. "
|
||||
"Here is a list of this server's roles:"
|
||||
"```\n{}```".format("\n".join([r.name for r in server_roles])))
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
|
||||
# Split the content based on commas, using regex so we can split if a space was not provided or if it was
|
||||
|
@ -73,17 +154,18 @@ class Roles:
|
|||
|
||||
# If no valid roles were given, let them know that and return
|
||||
if len(roles) == 0:
|
||||
await self.bot.say("Please provide a valid role next time!")
|
||||
await ctx.send("Please provide a valid role next time!")
|
||||
return
|
||||
|
||||
# Otherwise, remove the roles from each member given
|
||||
for member in members:
|
||||
await self.bot.remove_roles(member, *roles)
|
||||
await self.bot.say("I have just removed the following roles:```\n{}``` from the following members:"
|
||||
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
|
||||
await member.remove_roles(*roles)
|
||||
await ctx.send("I have just removed the following roles:```\n{}``` from the following members:"
|
||||
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
|
||||
|
||||
@role.command(name='add', pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(manage_roles=True)
|
||||
@role.command(name='add', aliases=['give', 'assign'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_roles=True)
|
||||
async def add_role(self, ctx):
|
||||
"""Use this to add a role to multiple members.
|
||||
Provide the list of members, and I'll ask for the role
|
||||
|
@ -92,32 +174,34 @@ class Roles:
|
|||
EXAMPLE: !role add @Bob @Joe @jim
|
||||
RESULT: A follow along to add the roles you want to these 3"""
|
||||
# No use in running through everything if the bot cannot manage roles
|
||||
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await self.bot.say("I can't manage roles in this server, do you not trust me? :c")
|
||||
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await ctx.send("I can't manage roles in this server, do you not trust me? :c")
|
||||
return
|
||||
check = lambda m: m.author == ctx.message.author and m.channel == ctx.message.channel
|
||||
|
||||
# This is exactly the same as removing roles, except we call add_roles instead.
|
||||
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
|
||||
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
|
||||
members = ctx.message.mentions
|
||||
if len(members) == 0:
|
||||
await self.bot.say("Please provide the list of members you want to add a role to")
|
||||
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Please provide the list of members you want to add a role to")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
if len(msg.mentions) == 0:
|
||||
await self.bot.say("I cannot add a role to someone if you don't provide someone...")
|
||||
await ctx.send("I cannot add a role to someone if you don't provide someone...")
|
||||
return
|
||||
members = msg.mentions
|
||||
|
||||
await self.bot.say("Alright, please provide the roles you would like to add to this member. "
|
||||
"Make sure the roles, if more than one is provided, are separate by commas. "
|
||||
"Here is a list of this server's roles:"
|
||||
"```\n{}```".format("\n".join([r.name for r in server_roles])))
|
||||
|
||||
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Alright, please provide the roles you would like to add to this member. "
|
||||
"Make sure the roles, if more than one is provided, are separate by commas. "
|
||||
"Here is a list of this server's roles:"
|
||||
"```\n{}```".format("\n".join([r.name for r in server_roles])))
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
role_names = re.split(', ?', msg.content)
|
||||
roles = []
|
||||
|
@ -127,51 +211,59 @@ class Roles:
|
|||
roles.append(_role)
|
||||
|
||||
if len(roles) == 0:
|
||||
await self.bot.say("Please provide a valid role next time!")
|
||||
await ctx.send("Please provide a valid role next time!")
|
||||
return
|
||||
|
||||
for member in members:
|
||||
await self.bot.add_roles(member, *roles)
|
||||
await self.bot.say("I have just added the following roles:```\n{}``` to the following members:"
|
||||
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
|
||||
await member.add_roles(*roles)
|
||||
await ctx.send("I have just added the following roles:```\n{}``` to the following members:"
|
||||
"```\n{}```".format("\n".join(role_names), "\n".join([m.display_name for m in members])))
|
||||
|
||||
@role.command(name='delete', pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(manage_roles=True)
|
||||
@role.command(name='delete')
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_roles=True)
|
||||
async def delete_role(self, ctx, *, role: discord.Role = None):
|
||||
"""This command can be used to delete one of the roles from the server
|
||||
|
||||
EXAMPLE: !role delete StupidRole
|
||||
RESULT: No more role called StupidRole"""
|
||||
# No use in running through everything if the bot cannot manage roles
|
||||
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await self.bot.say("I can't delete roles in this server, do you not trust me? :c")
|
||||
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await ctx.send("I can't delete roles in this server, do you not trust me? :c")
|
||||
return
|
||||
|
||||
# If no role was given, get the current roles on the server and ask which ones they'd like to remove
|
||||
if role is None:
|
||||
server_roles = [role for role in ctx.message.server.roles if not role.is_everyone]
|
||||
server_roles = [role for role in ctx.message.guild.roles if not role.is_default()]
|
||||
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
"Which role would you like to remove from the server? Here is a list of this server's roles:"
|
||||
"```\n{}```".format("\n".join([r.name for r in server_roles])))
|
||||
|
||||
# For this method we're only going to delete one role at a time
|
||||
# This check attempts to find a role based on the content provided, if it can't find one it returns None
|
||||
# We can use that fact to simply use just that as our check
|
||||
check = lambda m: discord.utils.get(server_roles, name=m.content)
|
||||
msg = await self.bot.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, check=check)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
def check(m):
|
||||
if m.author == ctx.message.author and m.channel == ctx.message.channel:
|
||||
return discord.utils.get(server_roles, name=m.content) is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=60, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
# If we have gotten here, based on our previous check, we know that the content provided is a valid role.
|
||||
# Due to that, no need for any error checking here
|
||||
role = discord.utils.get(server_roles, name=msg.content)
|
||||
|
||||
await self.bot.delete_role(ctx.message.server, role)
|
||||
await self.bot.say("I have just removed the role {} from this server".format(role.name))
|
||||
await role.delete()
|
||||
await ctx.send("I have just removed the role {} from this server".format(role.name))
|
||||
|
||||
@role.command(name='create', pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(manage_roles=True)
|
||||
@role.command(name='create')
|
||||
@commands.guild_only()
|
||||
@utils.can_run(manage_roles=True)
|
||||
async def create_role(self, ctx):
|
||||
"""This command can be used to create a new role for this server
|
||||
A prompt will follow asking what settings you would like for this new role
|
||||
|
@ -180,58 +272,78 @@ class Roles:
|
|||
EXAMPLE: !role create
|
||||
RESULT: A follow along in order to create a new role"""
|
||||
# No use in running through everything if the bot cannot create the role
|
||||
if not ctx.message.server.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await self.bot.say("I can't create roles in this server, do you not trust me? :c")
|
||||
if not ctx.message.guild.me.permissions_in(ctx.message.channel).manage_roles:
|
||||
await ctx.send("I can't create roles in this server, do you not trust me? :c")
|
||||
return
|
||||
|
||||
# Save a couple variables that will be used repeatedly
|
||||
author = ctx.message.author
|
||||
server = ctx.message.server
|
||||
server = ctx.message.guild
|
||||
channel = ctx.message.channel
|
||||
|
||||
# A couple checks that will be used in the wait_for_message's
|
||||
num_separated_check = lambda m: re.search("(\d(, ?| )?|[nN]one)", m.content) is not None
|
||||
yes_no_check = lambda m: re.search("(yes|no)", m.content.lower()) is not None
|
||||
members_check = lambda m: len(m.mentions) > 0
|
||||
def num_seperated_check(m):
|
||||
if m.author == author and m.channel == channel:
|
||||
return re.search("(\d(, ?| )?|[nN]one)", m.content) is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
def yes_no_check(m):
|
||||
if m.author == author and m.channel == channel:
|
||||
return re.search("(yes|no)", m.content.lower()) is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
def members_check(m):
|
||||
if m.author == author and m.channel == channel:
|
||||
return len(m.mentions) > 0
|
||||
else:
|
||||
return False
|
||||
|
||||
author_check = lambda m: m.author == author and m.channel == channel
|
||||
|
||||
# Start the checks for the role, get the name of the role first
|
||||
await self.bot.say(
|
||||
await ctx.send(
|
||||
"Alright! I'm ready to create a new role, please respond with the name of the role you want to create")
|
||||
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=60.0, check=author_check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
name = msg.content
|
||||
|
||||
# Print a list of all the permissions available, then ask for which ones need to be active on this new role
|
||||
all_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
|
||||
fmt = "\n".join("{}) {}".format(i, perm) for i, perm in enumerate(all_perms))
|
||||
await self.bot.say("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
|
||||
"the numbers, seperated by commas, of the permissions you want this role to have.\n"
|
||||
"```\n{}```".format(fmt))
|
||||
await ctx.send("Sounds fancy! Here is a list of all the permissions available. Please respond with just "
|
||||
"the numbers, seperated by commas, of the permissions you want this role to have.\n"
|
||||
"```\n{}```".format(fmt))
|
||||
# For this we're going to give a couple extra minutes before we timeout
|
||||
# as it might take a bit to figure out which permissions they want
|
||||
msg = await self.bot.wait_for_message(timeout=180.0, author=author, channel=channel, check=num_separated_check)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=180.0, check=num_seperated_check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
|
||||
# Check if any integer's were provided that are within the length of the list of permissions
|
||||
num_permissions = [int(i) for i in re.split(' ?,?', msg.content) if i.isdigit() and int(i) < len(all_perms)]
|
||||
|
||||
# Check if this role should be in a separate section on the sidebard, i.e. hoisted
|
||||
await self.bot.say("Do you want this role to be in a separate section on the sidebar? (yes or no)")
|
||||
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=yes_no_check)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Do you want this role to be in a separate section on the sidebar? (yes or no)")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=60.0, check=yes_no_check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
hoist = True if msg.content.lower() == "yes" else False
|
||||
|
||||
# Check if this role should be able to be mentioned
|
||||
await self.bot.say("Do you want this role to be mentionable? (yes or no)")
|
||||
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=yes_no_check)
|
||||
if msg is None:
|
||||
await self.bot.say("You took too long. I'm impatient, don't make me wait")
|
||||
await ctx.send("Do you want this role to be mentionable? (yes or no)")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=60.0, check=yes_no_check)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long. I'm impatient, don't make me wait")
|
||||
return
|
||||
mentionable = True if msg.content.lower() == "yes" else False
|
||||
|
||||
|
@ -248,21 +360,96 @@ class Roles:
|
|||
'mentionable': mentionable
|
||||
}
|
||||
# Create the role, and wait a second, sometimes it goes too quickly and we get a role with 'new role' to print
|
||||
role = await self.bot.create_role(server, **payload)
|
||||
role = await server.create_role(**payload)
|
||||
await asyncio.sleep(1)
|
||||
await self.bot.say("We did it! You just created the new role {}\nIf you want to add this role"
|
||||
" to some people, mention them now".format(role.name))
|
||||
msg = await self.bot.wait_for_message(timeout=60.0, author=author, channel=channel, check=members_check)
|
||||
# There's no need to mention the users, so don't send a failure message if they didn't, just return
|
||||
if msg is None:
|
||||
await ctx.send("We did it! You just created the new role {}\nIf you want to add this role"
|
||||
" to some people, mention them now".format(role.name))
|
||||
try:
|
||||
msg = await ctx.bot.wait_for('message', timeout=60.0, check=members_check)
|
||||
except asyncio.TimeoutError:
|
||||
# There's no need to mention the users, so don't send a failure message if they didn't, just return
|
||||
return
|
||||
|
||||
# Otherwise members were mentioned, add the new role to them now
|
||||
for member in msg.mentions:
|
||||
await self.bot.add_roles(member, role)
|
||||
await member.add_roles(role)
|
||||
|
||||
fmt = "\n".join(m.display_name for m in msg.mentions)
|
||||
await self.bot.say("I have just added the role {} to: ```\n{}```".format(name, fmt))
|
||||
await ctx.send("I have just added the role {} to: ```\n{}```".format(name, fmt))
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def assign(self, ctx, *role: discord.Role):
|
||||
"""Assigns the provided role(s) to you, if they can be assigned
|
||||
|
||||
EXAMPLE: !assign me Member
|
||||
RESULT: You now have the Member role"""
|
||||
if not ctx.message.guild.me.guild_permissions.manage_roles:
|
||||
await ctx.send("I need to have manage roles permissions to assign roles")
|
||||
return
|
||||
|
||||
author = ctx.message.author
|
||||
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result is None:
|
||||
await ctx.send("There are no self-assignable roles on this server")
|
||||
return
|
||||
self_assignable_roles = result["assignable_roles"]
|
||||
|
||||
if len(self_assignable_roles) == 0:
|
||||
await ctx.send("There are no self-assignable roles on this server")
|
||||
return
|
||||
|
||||
fmt = ""
|
||||
roles = [r for r in role if r.id in self_assignable_roles]
|
||||
fmt += "\n".join(["Successfully added {}".format(r.name)
|
||||
if r.id in self_assignable_roles else
|
||||
"{} is not available to be self-assigned".format(r.name)
|
||||
for r in role])
|
||||
|
||||
try:
|
||||
await author.add_roles(*roles)
|
||||
await ctx.send(fmt)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("I cannot assign roles to you {}".format(author.mention))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def unassign(self, ctx, *role: discord.Role):
|
||||
"""Unassigns the provided role(s) to you, if they can be assigned
|
||||
|
||||
EXAMPLE: !unassign Member
|
||||
RESULT: You now no longer have the Member role"""
|
||||
if not ctx.message.guild.me.guild_permissions.manage_roles:
|
||||
await ctx.send("I need to have manage roles permissions to assign roles")
|
||||
return
|
||||
|
||||
author = ctx.message.author
|
||||
result = await ctx.bot.db.fetchrow("SELECT assignable_roles FROM guilds WHERE id = $1", ctx.guild.id)
|
||||
|
||||
if result is None:
|
||||
await ctx.send("There are no self-assignable roles on this server")
|
||||
return
|
||||
self_assignable_roles = result["assignable_roles"]
|
||||
|
||||
if len(self_assignable_roles) == 0:
|
||||
await ctx.send("There are no self-assignable roles on this server")
|
||||
return
|
||||
|
||||
fmt = ""
|
||||
roles = [r for r in role if str(r.id) in self_assignable_roles]
|
||||
fmt += "\n".join(["Successfully removed {}".format(r.name)
|
||||
if str(r.id) in self_assignable_roles else
|
||||
"{} is not available to be self-assigned".format(r.name)
|
||||
for r in role])
|
||||
|
||||
try:
|
||||
await author.remove_roles(*roles)
|
||||
await ctx.send(fmt)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("I cannot remove roles from you {}".format(author.mention))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
123
cogs/roulette.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import discord
|
||||
import random
|
||||
import pendulum
|
||||
import asyncio
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
class Roulette(commands.Cog):
|
||||
"""A fun game that ends in someone getting kicked!"""
|
||||
roulettes = []
|
||||
|
||||
def get_game(self, server):
|
||||
for x in self.roulettes:
|
||||
if x.server == server:
|
||||
return x
|
||||
|
||||
def start_game(self, server, time):
|
||||
game = self.get_game(server)
|
||||
if game:
|
||||
return False
|
||||
else:
|
||||
game = Game(server, time)
|
||||
self.roulettes.append(game)
|
||||
return game
|
||||
|
||||
def end_game(self, server):
|
||||
game = self.get_game(server)
|
||||
member = game.choose()
|
||||
self.roulettes.remove(game)
|
||||
return member
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def roulette(self, ctx):
|
||||
"""Joins the current running roulette
|
||||
|
||||
EXAMPLE: !roulette
|
||||
RESULT: You're probably going to get kicked..."""
|
||||
r = self.get_game(ctx.message.guild)
|
||||
if not r:
|
||||
await ctx.send("There is no roulette game running on this server!")
|
||||
else:
|
||||
result = r.join(ctx.message.author)
|
||||
time_left = r.time_left
|
||||
if result:
|
||||
await ctx.send("You have joined this roulette game! Good luck~ This roulette will end in " + time_left)
|
||||
else:
|
||||
await ctx.send("This roulette will end in " + time_left)
|
||||
|
||||
@roulette.command(name='start', aliases=['create'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def roulette_start(self, ctx, time: int=5):
|
||||
"""Starts a roulette, that will end in one of the entrants being kicked from the server
|
||||
By default, the roulette will end in 5 minutes; provide a number (up to 30)
|
||||
to change how many minutes until it ends
|
||||
|
||||
EXAMPLE: !roulette start
|
||||
RESULT: A new roulette game!"""
|
||||
if time < 1 or time > 30:
|
||||
await ctx.send("Invalid time! The roulette must be set to run between 1 and 30 minutes")
|
||||
return
|
||||
else:
|
||||
game = self.start_game(ctx.message.guild, time)
|
||||
if game:
|
||||
await ctx.send("A new roulette game has just started! A random entrant will be kicked in {} minutes."
|
||||
" Type {}roulette to join this roulette...good luck~".format(game.time_left, ctx.prefix))
|
||||
else:
|
||||
await ctx.send("There is already a roulette game running on this server!")
|
||||
return
|
||||
|
||||
await asyncio.sleep(time * 60)
|
||||
member = self.end_game(ctx.message.guild)
|
||||
|
||||
if member is None:
|
||||
await ctx.send("Well no one joined the roulette. That was boring.")
|
||||
return
|
||||
|
||||
try:
|
||||
fmt = "The unlucky member to be kicked is {}; hopefully someone invites them back :)".format(
|
||||
member.display_name
|
||||
)
|
||||
await member.kick()
|
||||
except discord.Forbidden:
|
||||
fmt = "Well, the unlucky member chosen was {} but I can't kick you...so kick yourself please?".format(
|
||||
member.display_name
|
||||
)
|
||||
|
||||
await ctx.send(fmt)
|
||||
|
||||
|
||||
class Game:
|
||||
|
||||
def __init__(self, guild, time):
|
||||
self.entrants = []
|
||||
self.server = guild
|
||||
self.end_time = pendulum.now(tz="UTC").add(minutes=time)
|
||||
|
||||
@property
|
||||
def time_left(self):
|
||||
return (self.end_time - pendulum.now(tz="UTC")).in_words()
|
||||
|
||||
def join(self, member):
|
||||
"""Adds a member to the list of entrants"""
|
||||
if member in self.entrants:
|
||||
return False
|
||||
else:
|
||||
self.entrants.append(member)
|
||||
return True
|
||||
|
||||
def choose(self):
|
||||
try:
|
||||
return random.SystemRandom().choice(self.entrants)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Roulette(bot))
|
487
cogs/spades.py
Normal file
|
@ -0,0 +1,487 @@
|
|||
import asyncio
|
||||
import discord
|
||||
|
||||
import utils
|
||||
from utils import Deck, Suit, Face
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
card_map = {
|
||||
"2": "two",
|
||||
"3": "three",
|
||||
"4": "four",
|
||||
"5": "five",
|
||||
"6": "six",
|
||||
"7": "seven",
|
||||
"8": "eight",
|
||||
"9": "nine",
|
||||
"10": "ten"
|
||||
}
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, member, game):
|
||||
self.discord_member = member
|
||||
self.game = game
|
||||
self.channel = member.dm_channel
|
||||
if self.channel is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self.set_channel())
|
||||
self.hand_message = None
|
||||
self.table_message = None
|
||||
self.hand = Deck(prefill=False, spades_high=True)
|
||||
self.bid = 0
|
||||
self.points = 0
|
||||
self.tricks = 0
|
||||
|
||||
self.played_card = None
|
||||
|
||||
self._messages_to_clean = []
|
||||
|
||||
@property
|
||||
def bid_num(self):
|
||||
if self.bid == "moon":
|
||||
return 13
|
||||
elif self.bid == "nil":
|
||||
return 0
|
||||
return self.bid
|
||||
|
||||
async def send_message(self, content=None, embed=None, delete=True):
|
||||
"""A convenience method to send the message to the player, then add it to the list of messages to delete"""
|
||||
_msg = await self.discord_member.send(content, embed=embed)
|
||||
if delete:
|
||||
self._messages_to_clean.append(_msg)
|
||||
return _msg
|
||||
|
||||
async def set_channel(self):
|
||||
self.channel = await self.discord_member.create_dm()
|
||||
|
||||
async def show_hand(self):
|
||||
embed = discord.Embed(title="Hand")
|
||||
diamonds = []
|
||||
hearts = []
|
||||
clubs = []
|
||||
spades = []
|
||||
|
||||
for card in sorted(self.hand):
|
||||
if card.suit == Suit.diamonds:
|
||||
diamonds.append(str(card.face_short))
|
||||
if card.suit == Suit.hearts:
|
||||
hearts.append(str(card.face_short))
|
||||
if card.suit == Suit.clubs:
|
||||
clubs.append(str(card.face_short))
|
||||
if card.suit == Suit.spades:
|
||||
spades.append(str(card.face_short))
|
||||
|
||||
if diamonds:
|
||||
embed.add_field(name="Diamonds", value=", ".join(diamonds), inline=False)
|
||||
if hearts:
|
||||
embed.add_field(name="Hearts", value=", ".join(hearts), inline=False)
|
||||
if clubs:
|
||||
embed.add_field(name="Clubs", value=", ".join(clubs), inline=False)
|
||||
if spades:
|
||||
embed.add_field(name="Spades", value=", ".join(spades), inline=False)
|
||||
|
||||
if self.hand_message:
|
||||
await self.hand_message.edit(embed=embed)
|
||||
else:
|
||||
self.hand_message = await self.discord_member.send(embed=embed)
|
||||
|
||||
async def show_table(self):
|
||||
|
||||
embed = discord.Embed(title="Table")
|
||||
|
||||
if self.game.round.suit:
|
||||
embed.add_field(name="Round suit", value=self.game.round.suit.name, inline=False)
|
||||
else:
|
||||
embed.add_field(name="Round suit", value=self.game.round.suit, inline=False)
|
||||
|
||||
winning_card = self.game.round.winning_card
|
||||
if winning_card:
|
||||
embed.add_field(name="Winning card", value=str(winning_card), inline=False)
|
||||
|
||||
for num, p in enumerate(self.game.players):
|
||||
fmt = "{} ({}/{} tricks): {}".format(
|
||||
p.discord_member.display_name,
|
||||
p.tricks,
|
||||
p.bid_num,
|
||||
p.played_card
|
||||
)
|
||||
embed.add_field(name="Player {}".format(num + 1), value=fmt, inline=False)
|
||||
|
||||
if self.table_message:
|
||||
await self.table_message.edit(embed=embed)
|
||||
else:
|
||||
self.table_message = await self.discord_member.send(embed=embed)
|
||||
|
||||
async def get_bid(self):
|
||||
await self.send_message(
|
||||
content="It is your turn to bid. Please provide 1-12, nil, or moon depending on the bid you want. "
|
||||
"Please note you have 3 minutes to bid, any longer and you will be removed from the game.")
|
||||
self.bid = 0
|
||||
|
||||
def check(m):
|
||||
possible = ['nil', 'moon', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13']
|
||||
return m.channel == self.channel \
|
||||
and m.author.id == self.discord_member.id \
|
||||
and m.content.strip().lower() in possible
|
||||
|
||||
msg = await self.game.bot.wait_for('message', check=check)
|
||||
content = msg.content.strip().lower()
|
||||
if content == '0':
|
||||
self.bid = 'nil'
|
||||
elif content == '13':
|
||||
self.bid = 'moon'
|
||||
elif content.isdigit():
|
||||
self.bid = int(content)
|
||||
else:
|
||||
self.bid = content
|
||||
await self.send_message(content="Thank you for your bid! Please wait while I get the other players' bids...")
|
||||
return self.bid
|
||||
|
||||
async def play(self):
|
||||
fmt = "It is your turn to play, provide your response in the form '[value] of [face]' such as Ace of Spades. "
|
||||
fmt += "Your hand can be found above when you bid earlier."
|
||||
|
||||
await self.send_message(content=fmt)
|
||||
await self.game.bot.wait_for('message', check=self.play_check)
|
||||
await self.send_message(content="You have played...please wait for the other players")
|
||||
|
||||
async def clean_messages(self):
|
||||
for msg in self._messages_to_clean:
|
||||
await msg.delete()
|
||||
|
||||
self._messages_to_clean = []
|
||||
|
||||
def play_check(self, message):
|
||||
if message.channel != self.channel or message.author.id != message.author.id:
|
||||
return False
|
||||
if " of " not in message.content:
|
||||
return False
|
||||
try:
|
||||
parts = message.content.partition('of')
|
||||
face = parts[0].split()[-1].lower()
|
||||
suit = parts[2].split()[0].lower()
|
||||
face = card_map.get(face, face)
|
||||
|
||||
suit = getattr(Suit, suit)
|
||||
face = getattr(Face, face)
|
||||
|
||||
card = self.hand.get_card(suit, face)
|
||||
if card is not None and self.game.round.can_play(self, card):
|
||||
self.played_card = card
|
||||
self.hand.pluck(card=card)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except (IndexError, AttributeError):
|
||||
return False
|
||||
|
||||
def score(self):
|
||||
if self.bid == 'nil':
|
||||
if self.tricks == 0:
|
||||
self.points += 100
|
||||
else:
|
||||
self.points -= 100
|
||||
elif self.bid == 'moon':
|
||||
if self.tricks == 13:
|
||||
self.points += 200
|
||||
else:
|
||||
self.points -= 200
|
||||
else:
|
||||
if self.tricks >= self.bid:
|
||||
self.points += self.bid * 10
|
||||
self.points += self.tricks - self.bid
|
||||
else:
|
||||
self.points -= self.bid * 10
|
||||
|
||||
self.bid = 0
|
||||
self.tricks = 0
|
||||
|
||||
def has_suit(self, face):
|
||||
for card in self.hand:
|
||||
if face == card.suit:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_only_spades(self):
|
||||
for card in self.hand:
|
||||
if card.suit != Suit.spades:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Round:
|
||||
def __init__(self):
|
||||
self.spades_broken = False
|
||||
self.cards = Deck(prefill=False, spades_high=True)
|
||||
self.suit = None
|
||||
|
||||
def can_play(self, player, card):
|
||||
if self.cards.count == 0:
|
||||
if card.suit != Suit.spades:
|
||||
return True
|
||||
else:
|
||||
return self.spades_broken or player.has_only_spades()
|
||||
else:
|
||||
if card.suit == self.suit:
|
||||
return True
|
||||
else:
|
||||
return not player.has_suit(self.suit)
|
||||
|
||||
def play(self, card):
|
||||
# Set the suit
|
||||
if self.cards.count == 0:
|
||||
self.suit = card.suit
|
||||
# This will override the deck, and set it to the round's deck (this is what we want)
|
||||
card.deck = self.cards
|
||||
self.cards.insert(card)
|
||||
if card.suit == Suit.spades:
|
||||
self.spades_broken = True
|
||||
|
||||
@property
|
||||
def winning_card(self):
|
||||
cards = sorted([
|
||||
card
|
||||
for card in self.cards
|
||||
if card.suit == self.suit or card.suit == Suit.spades], reverse=True
|
||||
)
|
||||
try:
|
||||
return cards[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def reset(self):
|
||||
list(self.cards.draw(count=self.cards.count))
|
||||
self.suit = None
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.players = []
|
||||
self.deck = Deck(spades_high=True)
|
||||
self.deck.shuffle()
|
||||
self.round = Round()
|
||||
self.started = False
|
||||
self.card_count = 13
|
||||
self.score_limit = 250
|
||||
|
||||
async def start(self):
|
||||
self.started = True
|
||||
special_bids = ['nil', 'moon', 'misdeal']
|
||||
fmt = "All 4 players have joined, and your game of Spades has started!\n"
|
||||
fmt += "Rules for this game can be found here: https://www.pagat.com/boston/spades.html. "
|
||||
fmt += "Special actions/bids allowed are: `{}`\n".format(", ".join(special_bids))
|
||||
fmt += "Players are:\n" + "\n".join(p.discord_member.display_name for p in self.players)
|
||||
fmt += "\n\nPlease wait while all players bid...then the first round will begin"
|
||||
for p in self.players:
|
||||
await p.discord_member.send(fmt)
|
||||
|
||||
await self.game_task()
|
||||
|
||||
async def game_task(self):
|
||||
winner = None
|
||||
# some while loop, while no one has won yet
|
||||
while winner is None:
|
||||
await self.play_round()
|
||||
winner = self.get_winner()
|
||||
await self.new_round()
|
||||
|
||||
async def play_round(self):
|
||||
# For clarities sake, I want to send when it starts, immediately
|
||||
# then follow through with the hand and betting when it's their turn
|
||||
self.deal()
|
||||
for p in self.players:
|
||||
await p.show_hand()
|
||||
await p.show_table()
|
||||
await p.get_bid()
|
||||
|
||||
self.order_turns(self.get_highest_bidder())
|
||||
|
||||
# Bids are complete, time to start the game
|
||||
await self.clean_messages()
|
||||
|
||||
fmt = "Alright, everyone has bid, the bids are:\n{}".format(
|
||||
"\n".join("{}: {}".format(p.discord_member.display_name, p.bid) for p in self.players))
|
||||
for p in self.players:
|
||||
await p.send_message(content=fmt)
|
||||
|
||||
# Once bids are done, we can play the actual round
|
||||
for i in range(self.card_count):
|
||||
# Wait for each player to play
|
||||
for p in self.players:
|
||||
await p.play()
|
||||
self.round.play(p.played_card)
|
||||
# Update everyone's table once each person has finished
|
||||
await self.update_table()
|
||||
# Get the winner after the round, increase their tricks
|
||||
winner = self.get_round_winner()
|
||||
winning_card = winner.played_card
|
||||
winner.tricks += 1
|
||||
# Order players based off the winner
|
||||
self.order_turns(winner)
|
||||
|
||||
# Reset the round
|
||||
await self.reset_round()
|
||||
fmt = "{} won with a {}".format(winner.discord_member.display_name, winning_card)
|
||||
for p in self.players:
|
||||
await p.send_message(content=fmt)
|
||||
|
||||
async def update_table(self):
|
||||
for p in self.players:
|
||||
await p.show_table()
|
||||
|
||||
async def clean_messages(self):
|
||||
for p in self.players:
|
||||
await p.clean_messages()
|
||||
|
||||
async def reset_round(self):
|
||||
self.round.reset()
|
||||
await self.clean_messages()
|
||||
# First loop through to set everyone's card to None
|
||||
for p in self.players:
|
||||
p.played_card = None
|
||||
# Now we can show the table correctly, since everyone's card is set correctly
|
||||
for p in self.players:
|
||||
await p.show_hand()
|
||||
await p.show_table()
|
||||
|
||||
def get_highest_bidder(self):
|
||||
highest_bid = -1
|
||||
highest_player = None
|
||||
for player in self.players:
|
||||
print(player.bid_num, player.discord_member.display_name)
|
||||
if player.bid_num > highest_bid:
|
||||
highest_player = player
|
||||
|
||||
print(highest_player.discord_member.display_name)
|
||||
|
||||
return highest_player
|
||||
|
||||
def order_turns(self, player):
|
||||
index = self.players.index(player)
|
||||
self.players = self.players[index:] + self.players[:index]
|
||||
|
||||
def get_round_winner(self):
|
||||
winning_card = self.round.winning_card
|
||||
for p in self.players:
|
||||
if winning_card == p.played_card:
|
||||
return p
|
||||
|
||||
def get_winner(self):
|
||||
for p in self.players:
|
||||
if p.points >= self.score_limit:
|
||||
return p
|
||||
|
||||
async def new_round(self):
|
||||
score_msg = discord.Embed(title="Table scores")
|
||||
for p in self.players:
|
||||
p.score()
|
||||
p.played_card = None
|
||||
p.hand_message = None
|
||||
p.table_message = None
|
||||
score_msg.add_field(
|
||||
name="Player {}".format(p.discord_member.display_name),
|
||||
value="{}/{}".format(p.points, self.score_limit),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# We should do this after scoring, so a separate loop is needed here for that
|
||||
for p in self.players:
|
||||
await p.send_message(embed=score_msg)
|
||||
|
||||
# Round the round's reset information (this is the one run after each round of cards...this won't do everything)
|
||||
self.round.reset()
|
||||
# This is the only extra thing needed to fully reset the round itself
|
||||
self.round.spades_broken = False
|
||||
# Set the deck back and shuffle it
|
||||
self.deck.refresh()
|
||||
self.deck.shuffle()
|
||||
# This is all we want to do here, this is just preparing for the new round...not actually starting it
|
||||
|
||||
def deal(self):
|
||||
for _ in range(self.card_count):
|
||||
for p in self.players:
|
||||
card = list(self.deck.draw())
|
||||
p.hand.insert(card)
|
||||
|
||||
def join(self, member):
|
||||
p = Player(member, self)
|
||||
self.players.append(p)
|
||||
|
||||
|
||||
class Spades(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.pending_game = None
|
||||
self.games = []
|
||||
|
||||
def get_game(self, member):
|
||||
# Simply loop through each game's players, find the one that matches and return it
|
||||
for g, _ in self.games:
|
||||
for p in g.players:
|
||||
if member.id == p.discord_member.id:
|
||||
return g
|
||||
if self.pending_game:
|
||||
for p in self.pending_game.players:
|
||||
if member.id == p.discord_member.id:
|
||||
return self.pending_game
|
||||
|
||||
def join_game(self, author):
|
||||
# First check if there's a pending game
|
||||
if self.pending_game:
|
||||
# If so add the player to it
|
||||
self.pending_game.join(author)
|
||||
# If we've hit 4 players, we want to start the game, add it to our list of games, and wipe our pending game
|
||||
if len(self.pending_game.players) == 2:
|
||||
task = self.bot.loop.create_task(self.pending_game.start())
|
||||
self.games.append((self.pending_game, task))
|
||||
self.pending_game = None
|
||||
# If there's no pending game, start a pending game
|
||||
else:
|
||||
g = Game(self.bot)
|
||||
g.join(author)
|
||||
self.pending_game = g
|
||||
|
||||
def cog_unload(self):
|
||||
# Simply cancel every task
|
||||
for _, task in self.games:
|
||||
task.cancel()
|
||||
|
||||
@commands.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def spades(self, ctx):
|
||||
"""Used to join a spades games. This can be used in servers, or in PM, however it will be handled purely via PM.
|
||||
There are no teams in this version of spades, and blind nil/moon bids are not allowed. The way this is handled
|
||||
is for each person joining, there is a pending game ready to start...once 4 people have joined the "lobby" the
|
||||
game will start.
|
||||
|
||||
EXAMPLE: !spades
|
||||
RESULT: You've joined the spades lobby!"""
|
||||
author = ctx.message.author
|
||||
game = self.get_game(author)
|
||||
if game:
|
||||
if game.started:
|
||||
await ctx.send("You are already in a game! Check your PM's if you are confused")
|
||||
else:
|
||||
await ctx.send("There are {} players in your lobby".format(len(game.players)))
|
||||
return
|
||||
# Before we add the player to the game, we need to ensure we can PM this user
|
||||
# So lets do this backwards, confirm the user has joined the game, *then* join the game
|
||||
try:
|
||||
await author.send("You have joined a spades lobby! Please wait for more people to join, "
|
||||
"before the game can start")
|
||||
if ctx.guild:
|
||||
await ctx.send("Check your PM's {}. I have sent you information about your spades lobby".format(
|
||||
author.display_name))
|
||||
self.join_game(author)
|
||||
except discord.Forbidden:
|
||||
await ctx.send("This game is ran through PM's only! "
|
||||
"Please enable your PM's on this server if you want to play!")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Spades(bot))
|
88
cogs/spotify.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import asyncio
|
||||
import aiohttp
|
||||
import traceback
|
||||
|
||||
from discord.ext import commands
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
class Spotify(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self._token = None
|
||||
self._client_id = utils.spotify_id or ""
|
||||
self._client_secret = utils.spotify_secret or ""
|
||||
|
||||
self._authorization = "{}:{}".format(self._client_id, self._client_secret)
|
||||
self.headers = {
|
||||
"Authorization": "Basic {}".format(
|
||||
urlsafe_b64encode(self._authorization.encode()).decode()
|
||||
)
|
||||
}
|
||||
self.task = self.bot.loop.create_task(self.api_token_task())
|
||||
|
||||
async def api_token_task(self):
|
||||
while True:
|
||||
delay = 2400
|
||||
try:
|
||||
delay = await self.get_api_token()
|
||||
except Exception as error:
|
||||
with open("error_log", 'a') as f:
|
||||
traceback.print_tb(error.__traceback__, file=f)
|
||||
print('{0.__class__.__name__}: {0}'.format(error), file=f)
|
||||
finally:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
async def get_api_token(self):
|
||||
url = "https://accounts.spotify.com/api/token"
|
||||
opts = {"grant_type": "client_credentials"}
|
||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
||||
response = await session.post(url, data=opts)
|
||||
data = await response.json()
|
||||
self._token = data.get("access_token")
|
||||
return data.get("expires_in")
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@utils.can_run(send_messages=True)
|
||||
async def spotify(self, ctx, *, query):
|
||||
"""Searches Spotify for a song, giving you the link you can use to listen in. Give the query to search for
|
||||
and it will search by title/artist for the best match
|
||||
|
||||
EXAMPLE: !spotify Eminem
|
||||
RESULT: Some Eminem song"""
|
||||
|
||||
# Setup the headers with the token that should be here
|
||||
headers = {"Authorization": "Bearer {}".format(self._token)}
|
||||
opts = {"q": query, "type": "track"}
|
||||
url = "https://api.spotify.com/v1/search"
|
||||
response = await utils.request(url, headers=headers, payload=opts)
|
||||
try:
|
||||
await ctx.send(response.get("tracks").get("items")[0].get("external_urls").get("spotify"))
|
||||
except (KeyError, AttributeError, IndexError):
|
||||
await ctx.send("Couldn't find a song for:\n{}".format(query))
|
||||
|
||||
@spotify.command()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def playlist(self, ctx, *, query):
|
||||
"""Searches Spotify for a playlist, giving you the link you can use to listen in. Give the query to search for
|
||||
and it will search for the best match
|
||||
|
||||
EXAMPLE: !spotify Eminem
|
||||
RESULT: Some Eminem song"""
|
||||
# Setup the headers with the token that should be here
|
||||
headers = {"Authorization": "Bearer {}".format(self._token)}
|
||||
opts = {"q": query, "type": "playlist"}
|
||||
url = "https://api.spotify.com/v1/search"
|
||||
response = await utils.request(url, headers=headers, payload=opts)
|
||||
try:
|
||||
await ctx.send(response.get("playlists").get("items")[0].get("external_urls").get("spotify"))
|
||||
except (KeyError, AttributeError, IndexError):
|
||||
await ctx.send("Couldn't find a song for:\n{}".format(query))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Spotify(bot))
|
442
cogs/stats.py
|
@ -1,45 +1,88 @@
|
|||
import re
|
||||
import utils
|
||||
import discord
|
||||
import datetime
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class Stats:
|
||||
class Stats(commands.Cog):
|
||||
"""Leaderboard/stats related commands"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
def find_command(self, command):
|
||||
cmd = None
|
||||
async def _get_guild_usage(self, guild):
|
||||
embed = discord.Embed(title="Server Command Usage")
|
||||
count = await self.bot.db.fetchrow("SELECT COUNT(*), MIN(executed) FROM command_usage WHERE guild=$1", guild.id)
|
||||
|
||||
for part in command.split():
|
||||
try:
|
||||
if cmd is None:
|
||||
cmd = self.bot.commands.get(part)
|
||||
else:
|
||||
cmd = cmd.commands.get(part)
|
||||
except AttributeError:
|
||||
cmd = None
|
||||
break
|
||||
embed.description = f"{count[0]} total commands used"
|
||||
embed.set_footer(text='Tracking command usage since').timestamp = count[1] or datetime.datetime.utcnow()
|
||||
|
||||
return cmd
|
||||
query = """
|
||||
SELECT
|
||||
command, COUNT(*) as uses
|
||||
FROM
|
||||
command_usage
|
||||
WHERE
|
||||
guild = $1
|
||||
GROUP BY
|
||||
command
|
||||
ORDER BY
|
||||
"uses" DESC
|
||||
LIMIT 5
|
||||
"""
|
||||
|
||||
@commands.command(no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
results = await self.bot.db.fetch(query, guild.id)
|
||||
value = "\n".join(f"{command} ({uses} uses)" for command, uses in results or "No Commands")
|
||||
embed.add_field(name='Top Commands', value=value)
|
||||
|
||||
return embed
|
||||
|
||||
async def _get_member_usage(self, member):
|
||||
embed = discord.Embed(title=f"{member.display_name}'s command usage")
|
||||
count = await self.bot.db.fetchrow(
|
||||
"SELECT COUNT(*), MIN(executed) FROM command_usage WHERE author=$1",
|
||||
member.id
|
||||
)
|
||||
|
||||
embed.description = f"{count[0]} total commands used"
|
||||
embed.set_footer(text='Tracking command usage since').timestamp = count[1] or datetime.datetime.utcnow()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
command, COUNT(*) as uses
|
||||
FROM
|
||||
command_usage
|
||||
WHERE
|
||||
author = $1
|
||||
GROUP BY
|
||||
command
|
||||
ORDER BY
|
||||
"uses" DESC
|
||||
LIMIT 5
|
||||
"""
|
||||
|
||||
results = await self.bot.db.fetch(query, member.id)
|
||||
value = "\n".join(f"{command} ({uses} uses)" for command, uses in results or "No Commands")
|
||||
embed.add_field(name='Top Commands', value=value)
|
||||
|
||||
return embed
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def serverinfo(self, ctx):
|
||||
"""Provides information about the server
|
||||
|
||||
EXAMPLE: !serverinfo
|
||||
RESULT: Information about your server!"""
|
||||
server = ctx.message.server
|
||||
server = ctx.message.guild
|
||||
# Create our embed that we'll use for the information
|
||||
embed = discord.Embed(title=server.name, description="Created on: {}".format(server.created_at.date()))
|
||||
|
||||
# Make sure we only set the icon url if it has been set
|
||||
if server.icon_url != "":
|
||||
if server.icon_url:
|
||||
embed.set_thumbnail(url=server.icon_url)
|
||||
|
||||
# Add our fields, these are self-explanatory
|
||||
|
@ -52,237 +95,216 @@ class Stats:
|
|||
embed.add_field(name='Roles', value=len(server.roles))
|
||||
|
||||
# Split channels into voice and text channels
|
||||
voice_channels = [c for c in server.channels if str(c.type) == 'voice']
|
||||
text_channels = [c for c in server.channels if str(c.type) == 'text']
|
||||
voice_channels = [c for c in server.channels if type(c) is discord.VoiceChannel]
|
||||
text_channels = [c for c in server.channels if type(c) is discord.TextChannel]
|
||||
embed.add_field(name='Channels', value='{} text, {} voice'.format(len(text_channels), len(voice_channels)))
|
||||
embed.add_field(name='Owner', value=server.owner.display_name)
|
||||
|
||||
# Add the shard ID
|
||||
embed.set_footer(text="Server is on shard: {}/{}".format(self.bot.shard_id+1, self.bot.shard_count))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
await self.bot.say(embed=embed)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def userinfo(self, ctx, *, user: discord.Member = None):
|
||||
"""Provides information about a provided member
|
||||
|
||||
@commands.group(no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def command(self):
|
||||
EXAMPLE: !userinfo
|
||||
RESULT: Information about yourself!"""
|
||||
if user is None:
|
||||
user = ctx.message.author
|
||||
|
||||
embed = discord.Embed(colour=user.colour)
|
||||
fmt = "{} ({})".format(str(user), user.id)
|
||||
embed.set_author(name=fmt, icon_url=user.avatar_url)
|
||||
|
||||
embed.add_field(name='Joined this server', value=user.joined_at.date(), inline=False)
|
||||
embed.add_field(name='Joined Discord', value=user.created_at.date(), inline=False)
|
||||
|
||||
# Sort them based on the hierarchy, but don't include @everyone
|
||||
roles = sorted([x for x in user.roles if not x.is_default()], reverse=True)
|
||||
# I only want the top 5 roles for this purpose
|
||||
roles = ", ".join("{}".format(x.name) for x in roles[:5])
|
||||
# If there are no roles, then just say this
|
||||
roles = roles or "No roles added"
|
||||
embed.add_field(name='Top 5 roles', value=roles, inline=False)
|
||||
|
||||
# Add the activity if there is one
|
||||
act = user.activity
|
||||
if isinstance(act, discord.activity.Spotify):
|
||||
embed.add_field(name="Listening to", value=act.title, inline=False)
|
||||
elif isinstance(act, discord.activity.Game):
|
||||
embed.add_field(name='Playing', value=act.name, inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def command(self, ctx):
|
||||
pass
|
||||
|
||||
@command.command(no_pm=True, pass_context=True, name="stats")
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def command_stats(self, ctx, *, command):
|
||||
"""This command can be used to view some usage stats about a specific command
|
||||
@command.command(name="stats")
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def command_stats(self, ctx, *, member: discord.Member = None):
|
||||
"""This command can be used to view some usage stats about commands from either a user, or the server. Provide
|
||||
a member if you want to view their usage, provide no one and the server's usage will be looked up"""
|
||||
|
||||
EXAMPLE: !command stats play
|
||||
RESULT: The realization that this is the only reason people use me ;-;"""
|
||||
cmd = self.bot.get_command(command)
|
||||
if cmd is None:
|
||||
await self.bot.say("`{}` is not a valid command".format(command))
|
||||
return
|
||||
|
||||
command_stats = await utils.get_content('command_usage', cmd.qualified_name)
|
||||
if command_stats is None:
|
||||
await self.bot.say("That command has never been used! You know I worked hard on that! :c")
|
||||
return
|
||||
|
||||
total_usage = command_stats['total_usage']
|
||||
member_usage = command_stats['member_usage'].get(ctx.message.author.id, 0)
|
||||
server_usage = command_stats['server_usage'].get(ctx.message.server.id, 0)
|
||||
|
||||
try:
|
||||
data = [("Command Name", cmd.qualified_name),
|
||||
("Total Usage", total_usage),
|
||||
("Your Usage", member_usage),
|
||||
("This Server's Usage", server_usage)]
|
||||
banner = await utils.create_banner(ctx.message.author, "Command Stats", data)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
fmt = "The command {} has been used a total of {} times\n" \
|
||||
"{} times on this server\n" \
|
||||
"It has been ran by you, {}, {} times".format(cmd.qualified_name, total_usage, server_usage,
|
||||
ctx.message.author.display_name, member_usage)
|
||||
|
||||
await self.bot.say(fmt)
|
||||
|
||||
@command.command(no_pm=True, pass_context=True, name="leaderboard")
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def command_leaderboard(self, ctx, option="server"):
|
||||
"""This command can be used to print a leaderboard of commands
|
||||
Provide 'server' to print a leaderboard for this server
|
||||
Provide 'me' to print a leaderboard for your own usage
|
||||
|
||||
EXAMPLE: !command leaderboard me
|
||||
RESULT: The realization of how little of a life you have"""
|
||||
if re.search('(author|me)', option):
|
||||
author = ctx.message.author
|
||||
# First lets get all the command usage
|
||||
command_stats = await utils.get_content('command_usage')
|
||||
# Now use a dictionary comprehension to get just the command name, and usage
|
||||
# Based on the author's usage of the command
|
||||
stats = {data['command']: data['member_usage'].get(author.id) for data in command_stats
|
||||
if data['member_usage'].get(author.id, 0) > 0}
|
||||
# Now sort it by the amount of times used
|
||||
sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Create a string, each command on it's own line, based on the top 5 used commands
|
||||
# I'm letting it use the length of the sorted_stats[:5]
|
||||
# As this can include, for example, all 3 if there are only 3 entries
|
||||
try:
|
||||
top_5 = [(data[0], data[1]) for data in sorted_stats[:5]]
|
||||
banner = await utils.create_banner(ctx.message.author, "Your command usage", top_5)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
top_5 = "\n".join("{}: {}".format(data[0], data[1]) for data in sorted_stats[:5])
|
||||
await self.bot.say(
|
||||
"Your top {} most used commands are:\n```\n{}```".format(len(sorted_stats[:5]), top_5))
|
||||
elif re.search('server', option):
|
||||
# This is exactly the same as above, except server usage instead of member usage
|
||||
server = ctx.message.server
|
||||
command_stats = await utils.get_content('command_usage')
|
||||
stats = {data['command']: data['server_usage'].get(server.id) for data in command_stats
|
||||
if data['server_usage'].get(server.id, 0) > 0}
|
||||
sorted_stats = sorted(stats.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
top_5 = "\n".join("{}: {}".format(data[0], data[1]) for data in sorted_stats[:5])
|
||||
await self.bot.say(
|
||||
"This server's top {} most used commands are:\n```\n{}```".format(len(sorted_stats[:5]), top_5))
|
||||
if member is None:
|
||||
embed = await self._get_guild_usage(ctx.guild)
|
||||
else:
|
||||
await self.bot.say("That is not a valid option, valid options are: `server` or `me`")
|
||||
embed = await self._get_member_usage(member)
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def mostboops(self, ctx):
|
||||
"""Shows the person you have 'booped' the most, as well as how many times
|
||||
|
||||
EXAMPLE: !mostboops
|
||||
RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!"""
|
||||
boops = await utils.get_content('boops', ctx.message.author.id)
|
||||
if boops is None:
|
||||
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
|
||||
return
|
||||
query = """
|
||||
SELECT
|
||||
boopee, amount
|
||||
FROM
|
||||
boops
|
||||
WHERE
|
||||
booper=$1
|
||||
AND
|
||||
boopee = ANY($2)
|
||||
ORDER BY
|
||||
amount DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
members = [m.id for m in ctx.guild.members]
|
||||
most = await ctx.bot.db.fetchrow(query, ctx.author.id, members)
|
||||
|
||||
# Just to make this easier, just pay attention to the boops data, now that we have the right entry
|
||||
boops = boops['boops']
|
||||
if most is None or len(most) == 0:
|
||||
await ctx.send(f"You have not booped anyone in this server {ctx.author.mention}")
|
||||
else:
|
||||
member = ctx.guild.get_member(most['boopee'])
|
||||
await ctx.send(
|
||||
f"{ctx.author.mention} you have booped {member.display_name} the most amount of times, "
|
||||
f"coming in at {most['amount']} times"
|
||||
)
|
||||
|
||||
# First get a list of the ID's of all members in this server, for use in list comprehension
|
||||
server_member_ids = [member.id for member in ctx.message.server.members]
|
||||
# Then get a sorted list, based on the amount of times they've booped the member
|
||||
# Reverse needs to be true, as we want it to go from highest to lowest
|
||||
sorted_boops = sorted(boops.items(), key=lambda x: x[1], reverse=True)
|
||||
# Then override the same list, checking if the member they've booped is in this server
|
||||
sorted_boops = [x for x in sorted_boops if x[0] in server_member_ids]
|
||||
|
||||
# Since this is sorted, we just need to get the following information on the first user in the list
|
||||
most_id, most_boops = sorted_boops[0]
|
||||
|
||||
member = discord.utils.find(lambda m: m.id == most_id, self.bot.get_all_members())
|
||||
await self.bot.say("{0} you have booped {1} the most amount of times, coming in at {2} times".format(
|
||||
ctx.message.author.mention, member.display_name, most_boops))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def listboops(self, ctx):
|
||||
"""Lists all the users you have booped and the amount of times
|
||||
|
||||
EXAMPLE: !listboops
|
||||
RESULT: The list of your booped members!"""
|
||||
boops = await utils.get_content('boops', ctx.message.author.id)
|
||||
if boops is None:
|
||||
await self.bot.say("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
|
||||
return
|
||||
|
||||
# Just to make this easier, just pay attention to the boops data, now that we have the right entry
|
||||
boops = boops['boops']
|
||||
query = """
|
||||
SELECT
|
||||
boopee, amount
|
||||
FROM
|
||||
boops
|
||||
WHERE
|
||||
booper=$1
|
||||
AND
|
||||
boopee = ANY($2)
|
||||
ORDER BY
|
||||
amount DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
|
||||
# Same concept as the mostboops method
|
||||
server_member_ids = [member.id for member in ctx.message.server.members]
|
||||
booped_members = {m_id: amt for m_id, amt in boops.items() if m_id in server_member_ids}
|
||||
sorted_booped_members = sorted(booped_members.items(), key=lambda k: k[1], reverse=True)
|
||||
# Now we only want the first 10 members, so splice this list
|
||||
sorted_booped_members = sorted_booped_members[:10]
|
||||
members = [m.id for m in ctx.guild.members]
|
||||
most = await ctx.bot.db.fetch(query, ctx.author.id, members)
|
||||
|
||||
try:
|
||||
output = [("{0.display_name}".format(ctx.message.server.get_member(m_id)), amt)
|
||||
for m_id, amt in sorted_booped_members]
|
||||
banner = await utils.create_banner(ctx.message.author, "Your booped victims", output)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
output = "\n".join(
|
||||
"{0.display_name}: {1} times".format(ctx.message.server.get_member(m_id), amt) for
|
||||
m_id, amt in sorted_booped_members)
|
||||
await self.bot.say("You have booped:```\n{}```".format(output))
|
||||
if len(most) != 0:
|
||||
embed = discord.Embed(title="Your booped victims", colour=ctx.author.colour)
|
||||
embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url)
|
||||
for row in most:
|
||||
member = ctx.guild.get_member(row['boopee'])
|
||||
embed.add_field(name=member.display_name, value=row['amount'])
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
await ctx.send("You haven't booped anyone in this server!")
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def leaderboard(self, ctx):
|
||||
"""Prints a leaderboard of everyone in the server's battling record
|
||||
|
||||
EXAMPLE: !leaderboard
|
||||
RESULT: A leaderboard of this server's battle records"""
|
||||
# Create a list of the ID's of all members in this server, for comparison to the records saved
|
||||
server_member_ids = [member.id for member in ctx.message.server.members]
|
||||
battles = await utils.get_content('battle_records')
|
||||
battles = [battle for battle in battles if battle['member_id'] in server_member_ids]
|
||||
|
||||
# Sort the members based on their rating
|
||||
sorted_members = sorted(battles, key=lambda k: k['rating'], reverse=True)
|
||||
query = """
|
||||
SELECT
|
||||
id, battle_rating
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = any($1::bigint[])
|
||||
ORDER BY
|
||||
battle_rating DESC
|
||||
"""
|
||||
|
||||
output = []
|
||||
for x in sorted_members:
|
||||
member_id = x['member_id']
|
||||
rating = x['rating']
|
||||
member = ctx.message.server.get_member(member_id)
|
||||
output.append("{} (Rating: {})".format(member.display_name, rating))
|
||||
results = await ctx.bot.db.fetch(query, [m.id for m in ctx.guild.members])
|
||||
|
||||
try:
|
||||
pages = utils.Pages(self.bot, message=ctx.message, entries=output)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await self.bot.say(str(e))
|
||||
if results is None or len(results) == 0:
|
||||
await ctx.send("No one has battled on this server!")
|
||||
else:
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def stats(self, ctx, member: discord.Member = None):
|
||||
output = []
|
||||
for row in results:
|
||||
member = ctx.guild.get_member(row['id'])
|
||||
output.append(f"{member.display_name} (Rating: {row['battle_rating']})")
|
||||
|
||||
try:
|
||||
pages = utils.Pages(ctx, entries=output)
|
||||
await pages.paginate()
|
||||
except utils.CannotPaginate as e:
|
||||
await ctx.send(str(e))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def battlestats(self, ctx, member: discord.Member = None):
|
||||
"""Prints the battling stats for you, or the user provided
|
||||
|
||||
EXAMPLE: !stats @OtherPerson
|
||||
RESULT: How good they are at winning a completely luck based game"""
|
||||
member = member or ctx.message.author
|
||||
# Get the different data that we'll display
|
||||
query = """
|
||||
SELECT id, rank, battle_rating, battle_wins, battle_losses
|
||||
FROM
|
||||
(SELECT
|
||||
id,
|
||||
ROW_NUMBER () OVER (ORDER BY battle_rating DESC) as "rank",
|
||||
battle_rating,
|
||||
battle_wins,
|
||||
battle_losses
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = any($1::bigint[]) AND
|
||||
battle_rating IS NOT NULL
|
||||
) AS sub
|
||||
WHERE id = $2
|
||||
"""
|
||||
member_list = [m.id for m in ctx.guild.members]
|
||||
result = await ctx.bot.db.fetchrow(query, member_list, member.id)
|
||||
if result is None:
|
||||
return await ctx.send("You have not battled!")
|
||||
server_rank = result["rank"]
|
||||
# overall_rank = "{}/{}".format(*ctx.bot.br.get_rank(member))
|
||||
rating = result["battle_rating"]
|
||||
record = f"{result['battle_wins']} - {result['battle_losses']}"
|
||||
|
||||
# For this one, we don't want to pass a filter, as we do need all battle records
|
||||
# We need this because we want to make a comparison for overall rank
|
||||
all_members = await utils.get_content('battle_records')
|
||||
embed = discord.Embed(title="Battling stats for {}".format(ctx.author.display_name), colour=ctx.author.colour)
|
||||
embed.set_author(name=str(member), icon_url=member.avatar_url)
|
||||
embed.add_field(name="Record", value=record, inline=False)
|
||||
embed.add_field(name="Server Rank", value=server_rank, inline=False)
|
||||
# embed.add_field(name="Overall Rank", value=overall_rank, inline=False)
|
||||
embed.add_field(name="Rating", value=rating, inline=False)
|
||||
|
||||
# Make a list comprehension to just check if the user has battled
|
||||
if len([entry for entry in all_members if entry['member_id'] == member.id]) == 0:
|
||||
await self.bot.say("That user has not battled yet!")
|
||||
return
|
||||
|
||||
# Same concept as the leaderboard
|
||||
server_member_ids = [member.id for member in ctx.message.server.members]
|
||||
server_members = [stats for stats in all_members if stats['member_id'] in server_member_ids]
|
||||
sorted_server_members = sorted(server_members, key=lambda x: x['rating'], reverse=True)
|
||||
sorted_all_members = sorted(all_members, key=lambda x: x['rating'], reverse=True)
|
||||
|
||||
# Enumurate the list so that we can go through, find the user's place in the list
|
||||
# and get just that for the rank
|
||||
server_rank = [i for i, x in enumerate(sorted_server_members) if x['member_id'] == member.id][0] + 1
|
||||
total_rank = [i for i, x in enumerate(sorted_all_members) if x['member_id'] == member.id][0] + 1
|
||||
# The rest of this is straight forward, just formatting
|
||||
|
||||
entry = [m for m in server_members if m['member_id'] == member.id][0]
|
||||
rating = entry['rating']
|
||||
record = "{}-{}".format(entry['wins'], entry['losses'])
|
||||
try:
|
||||
title = 'Stats for {}'.format(member.display_name)
|
||||
fmt = [('Record', record), ('Server Rank', '{}/{}'.format(server_rank, len(server_members))),
|
||||
('Overall Rank', '{}/{}'.format(total_rank, len(all_members))), ('Rating', rating)]
|
||||
banner = await utils.create_banner(member, title, fmt)
|
||||
await self.bot.upload(banner)
|
||||
except (FileNotFoundError, discord.Forbidden):
|
||||
fmt = 'Stats for {}:\n\tRecord: {}\n\tServer Rank: {}/{}\n\tOverall Rank: {}/{}\n\tRating: {}'
|
||||
fmt = fmt.format(member.display_name, record, server_rank, len(server_members), total_rank,
|
||||
len(all_members), rating)
|
||||
await self.bot.say('```\n{}```'.format(fmt))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
from discord.ext import commands
|
||||
import discord
|
||||
|
||||
from .utils import config
|
||||
from .utils import checks
|
||||
|
||||
import aiohttp
|
||||
import re
|
||||
import json
|
||||
import pendulum
|
||||
import rethinkdb as r
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Strawpoll(bot))
|
||||
|
||||
|
||||
getter = re.compile(r'`(?!`)(.*?)`')
|
||||
multi = re.compile(r'```(.*?)```', re.DOTALL)
|
||||
|
||||
|
||||
class Strawpoll:
|
||||
"""This class is used to create new strawpoll """
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.url = 'https://strawpoll.me/api/v2/polls'
|
||||
# In this class we'll only be sending POST requests when creating a poll
|
||||
# Strawpoll requires the content-type, so just add that to the default headers
|
||||
self.headers = {'User-Agent': 'Bonfire/1.0.0',
|
||||
'Content-Type': 'application/json'}
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
@commands.group(aliases=['strawpoll', 'poll', 'polls'], pass_context=True, invoke_without_command=True, no_pm=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
async def strawpolls(self, ctx, poll_id: str = None):
|
||||
"""This command can be used to show a strawpoll setup on this server
|
||||
|
||||
EXAMPLE: !strawpolls
|
||||
RESULT: A list of all polls setup on this server"""
|
||||
# Strawpolls cannot be 'deleted' so to handle whether a poll is running or not on a server
|
||||
# Just save the poll, which can then be removed when it should not be "running" anymore
|
||||
polls = await config.get_content('strawpolls', ctx.message.server.id)
|
||||
# Check if there are any polls setup on this server
|
||||
try:
|
||||
polls = polls[0]['polls']
|
||||
except TypeError:
|
||||
await self.bot.say("There are currently no strawpolls running on this server!")
|
||||
return
|
||||
# Print all polls on this server if poll_id was not provided
|
||||
if poll_id is None:
|
||||
fmt = "\n".join(
|
||||
"{}: https://strawpoll.me/{}".format(data['title'], data['poll_id']) for data in polls)
|
||||
await self.bot.say("```\n{}```".format(fmt))
|
||||
else:
|
||||
# Since strawpoll should never allow us to have more than one poll with the same ID
|
||||
# It's safe to assume there's only one result
|
||||
try:
|
||||
poll = [p for p in polls if p['poll_id'] == poll_id][0]
|
||||
except IndexError:
|
||||
await self.bot.say("That poll does not exist on this server!")
|
||||
return
|
||||
|
||||
async with self.session.get("{}/{}".format(self.url, poll_id),
|
||||
headers={'User-Agent': 'Bonfire/1.0.0'}) as response:
|
||||
data = await response.json()
|
||||
|
||||
# The response for votes and options is provided as two separate lists
|
||||
# We are enumarting the list of options, to print r (the option)
|
||||
# And the votes to match it, based on the index of the option
|
||||
# The rest is simple formatting
|
||||
fmt_options = "\n\t".join(
|
||||
"{}: {}".format(result, data['votes'][i]) for i, result in enumerate(data['options']))
|
||||
author = discord.utils.get(ctx.message.server.members, id=poll['author'])
|
||||
created_ago = (pendulum.utcnow() - pendulum.parse(poll['date'])).in_words()
|
||||
link = "https://strawpoll.me/{}".format(poll_id)
|
||||
fmt = "Link: {}\nTitle: {}\nAuthor: {}\nCreated: {} ago\nOptions:\n\t{}".format(link, data['title'],
|
||||
author.display_name,
|
||||
created_ago, fmt_options)
|
||||
await self.bot.say("```\n{}```".format(fmt))
|
||||
|
||||
@strawpolls.command(name='create', aliases=['setup', 'add'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def create_strawpoll(self, ctx, title, *, options):
|
||||
"""This command is used to setup a new strawpoll
|
||||
The format needs to be: poll create "title here" all options here
|
||||
Options need to be separated by using either one ` around each option
|
||||
Or use a code block (3 ` around the options), each option on it's own line"""
|
||||
# The following should use regex to search for the options inside of the two types of code blocks with `
|
||||
# We're using this instead of other things, to allow most used puncation inside the options
|
||||
match_single = getter.findall(options)
|
||||
match_multi = multi.findall(options)
|
||||
# Since match_single is already going to be a list, we just set
|
||||
# The options to match_single and remove any blank entries
|
||||
if match_single:
|
||||
options = match_single
|
||||
options = [option for option in options if option]
|
||||
# Otherwise, options need to be set based on the list, split by lines.
|
||||
# Then remove blank entries like the last one
|
||||
elif match_multi:
|
||||
options = match_multi[0].splitlines()
|
||||
options = [option for option in options if option]
|
||||
# If neither is found, then error out and let them know to use the help command, since this one is a bit finicky
|
||||
else:
|
||||
await self.bot.say(
|
||||
"Please provide options for a new strawpoll! Use {}help {} if you do not know the format".format(
|
||||
ctx.prefix, ctx.command.qualified_name))
|
||||
return
|
||||
# Make the post request to strawpoll, creating the poll, and returning the ID
|
||||
# The ID is all we really need from the returned data, as the rest we already sent/are not going to use ever
|
||||
payload = {'title': title,
|
||||
'options': options}
|
||||
try:
|
||||
async with self.session.post(self.url, data=json.dumps(payload), headers=self.headers) as response:
|
||||
data = await response.json()
|
||||
except json.JSONDecodeError:
|
||||
await self.bot.say("Sorry, I couldn't connect to strawpoll at the moment. Please try again later")
|
||||
return
|
||||
|
||||
# Save this strawpoll in the list of running strawpolls for a server
|
||||
poll_id = str(data['id'])
|
||||
|
||||
r_filter = {'server_id': ctx.message.server.id}
|
||||
sub_entry = {'poll_id': poll_id,
|
||||
'author': ctx.message.author.id,
|
||||
'date': str(pendulum.utcnow()),
|
||||
'title': title}
|
||||
|
||||
entry = {'server_id': ctx.message.server.id,
|
||||
'polls': [sub_entry]}
|
||||
update = {'polls': r.row['polls'].append(sub_entry)}
|
||||
if not await config.update_content('strawpolls', update, r_filter):
|
||||
await config.add_content('strawpolls', entry, {'poll_id': poll_id})
|
||||
await self.bot.say("Link for your new strawpoll: https://strawpoll.me/{}".format(poll_id))
|
||||
|
||||
@strawpolls.command(name='delete', aliases=['remove', 'stop'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def remove_strawpoll(self, ctx, poll_id):
|
||||
"""This command can be used to delete one of the existing strawpolls
|
||||
|
||||
EXAMPLE: !strawpoll remove 5
|
||||
RESULT: No more strawpoll 5~"""
|
||||
r_filter = {'server_id': ctx.message.server.id}
|
||||
content = await config.get_content('strawpolls', r_filter)
|
||||
try:
|
||||
content = content[0]['polls']
|
||||
except TypeError:
|
||||
await self.bot.say("There are no strawpolls setup on this server!")
|
||||
return
|
||||
|
||||
polls = [poll for poll in content if poll['poll_id'] != poll_id]
|
||||
|
||||
update = {'polls': polls}
|
||||
# Try to remove the poll based on the ID, if it doesn't exist, this will return false
|
||||
if await config.update_content('strawpolls', update, r_filter):
|
||||
await self.bot.say("I have just removed the poll with the ID {}".format(poll_id))
|
||||
else:
|
||||
await self.bot.say("There is no poll setup with that ID!")
|
265
cogs/tags.py
|
@ -1,98 +1,229 @@
|
|||
from discord.ext import commands
|
||||
from .utils import config
|
||||
from .utils import checks
|
||||
import re
|
||||
import discord
|
||||
|
||||
import utils
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class Tags:
|
||||
class Tags(commands.Cog):
|
||||
"""This class contains all the commands for custom tags"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def tags(self, ctx):
|
||||
"""Prints all the custom tags that this server currently has
|
||||
|
||||
EXAMPLE: !tags
|
||||
RESULT: All tags setup on this server"""
|
||||
tags = await config.get_content('tags', ctx.message.server.id)
|
||||
# Simple generator that adds a tag to the list to print, if the tag is for this server
|
||||
try:
|
||||
fmt = "\n".join("{}".format(tag['tag']) for tag in tags)
|
||||
await self.bot.say('```\n{}```'.format(fmt))
|
||||
except TypeError:
|
||||
await self.bot.say("There are not tags setup on this server!")
|
||||
tags = await ctx.bot.db.fetch("SELECT trigger FROM tags WHERE guild=$1", ctx.guild.id)
|
||||
|
||||
@commands.group(pass_context=True, invoke_without_command=True, no_pm=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
if len(tags) > 0:
|
||||
entries = [t['trigger'] for t in tags]
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
else:
|
||||
await ctx.send("There are no tags setup on this server!")
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def mytags(self, ctx):
|
||||
"""Prints all the custom tags that this server that you own
|
||||
|
||||
EXAMPLE: !mytags
|
||||
RESULT: All your tags setup on this server"""
|
||||
tags = await ctx.bot.db.fetch(
|
||||
"SELECT trigger FROM tags WHERE guild=$1 AND creator=$2",
|
||||
ctx.guild.id,
|
||||
ctx.author.id
|
||||
)
|
||||
|
||||
if len(tags) > 0:
|
||||
entries = [t['trigger'] for t in tags]
|
||||
pages = utils.Pages(ctx, entries=entries)
|
||||
await pages.paginate()
|
||||
else:
|
||||
await ctx.send("You have no tags on this server!")
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def tag(self, ctx, *, tag: str):
|
||||
"""This can be used to call custom tags
|
||||
The format to call a custom tag is !tag <tag>
|
||||
The format to call a custom tag is !tag <tag>
|
||||
|
||||
EXAMPLE: !tag butts
|
||||
RESULT: Whatever you setup for the butts tag!!"""
|
||||
r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag)
|
||||
tags = await config.filter_content('tags', r_filter)
|
||||
if tags is None:
|
||||
await self.bot.say('That tag does not exist!')
|
||||
return
|
||||
# We shouldn't ever have two tags of the same name, so just get the first result
|
||||
await self.bot.say("\u200B{}".format(tags[0]['result']))
|
||||
EXAMPLE: !tag butts
|
||||
RESULT: Whatever you setup for the butts tag!!"""
|
||||
tag = await ctx.bot.db.fetchrow(
|
||||
"SELECT id, result FROM tags WHERE guild=$1 AND trigger=$2",
|
||||
ctx.guild.id,
|
||||
tag.lower().strip()
|
||||
)
|
||||
|
||||
@tag.command(name='add', aliases=['create', 'start'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def add_tag(self, ctx, *, result: str):
|
||||
if tag:
|
||||
await ctx.send("\u200B{}".format(tag['result']))
|
||||
await ctx.bot.db.execute("UPDATE tags SET uses = uses + 1 WHERE id = $1", tag['id'])
|
||||
else:
|
||||
await ctx.send("There is no tag called {}".format(tag))
|
||||
|
||||
@tag.command(name='add', aliases=['create', 'setup'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def add_tag(self, ctx):
|
||||
"""Use this to add a new tag that can be used in this server
|
||||
Format to add a tag is !tag add <tag> - <result>
|
||||
|
||||
EXAMPLE: !tag add this is my new tag - This is what it will be
|
||||
RESULT: A tag that can be called by '!tag this is my new tag' and will output 'This is what it will be'"""
|
||||
EXAMPLE: !tag add
|
||||
RESULT: A follow-along in order to create a new tag"""
|
||||
|
||||
def check(m):
|
||||
return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0
|
||||
|
||||
my_msg = await ctx.send("Ready to setup a new tag! What do you want the trigger for the tag to be?")
|
||||
|
||||
try:
|
||||
# Use regex to get the matche for everything before and after a -
|
||||
match = re.search("(.*) - (.*)", result)
|
||||
tag = match.group(1).strip()
|
||||
tag_result = match.group(2).strip()
|
||||
# Next two checks are just to ensure there was a valid match found
|
||||
except AttributeError:
|
||||
await self.bot.say(
|
||||
"Please provide the format for the tag in: {}tag add <tag> - <result>".format(ctx.prefix))
|
||||
return
|
||||
# If our regex failed to find the content (aka they provided the wrong format)
|
||||
if len(tag) == 0 or len(tag_result) == 0:
|
||||
await self.bot.say(
|
||||
"Please provide the format for the tag in: {}tag add <tag> - <result>".format(ctx.prefix))
|
||||
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long!")
|
||||
return
|
||||
|
||||
# Make sure the tag created does not mention everyone/here
|
||||
if '@everyone' in tag_result or '@here' in tag_result:
|
||||
await self.bot.say("You cannot create a tag that mentions everyone!")
|
||||
trigger = msg.content.lower().strip()
|
||||
forbidden_tags = ['add', 'create', 'setup', 'edit', 'info', 'delete', 'remove', 'stop']
|
||||
if len(trigger) > 100:
|
||||
await ctx.send("Please keep tag triggers under 100 characters")
|
||||
return
|
||||
elif trigger.lower() in forbidden_tags:
|
||||
await ctx.send(
|
||||
"Sorry, but your tag trigger was detected to be forbidden. "
|
||||
"Current forbidden tag triggers are: \n{}".format("\n".join(forbidden_tags)))
|
||||
return
|
||||
entry = {'server_id': ctx.message.server.id, 'tag': tag, 'result': tag_result}
|
||||
r_filter = lambda row: (row['server_id'] == ctx.message.server.id) & (row['tag'] == tag)
|
||||
# Try to create new entry first, if that fails (it already exists) then we update it
|
||||
if not await config.update_content('tags', entry, r_filter):
|
||||
await config.add_content('tags', entry)
|
||||
await self.bot.say(
|
||||
"I have just updated the tag `{0}`! You can call this tag by entering !tag {0}".format(tag))
|
||||
|
||||
@tag.command(name='delete', aliases=['remove', 'stop'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
async def del_tag(self, ctx, *, tag: str):
|
||||
tag = await ctx.bot.db.fetchrow(
|
||||
"SELECT result FROM tags WHERE guild=$1 AND trigger=$2",
|
||||
ctx.guild.id,
|
||||
trigger.lower().strip()
|
||||
)
|
||||
if tag:
|
||||
await ctx.send("There is already a tag setup called {}!".format(trigger))
|
||||
return
|
||||
|
||||
try:
|
||||
await my_msg.delete()
|
||||
await msg.delete()
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
|
||||
my_msg = await ctx.send(
|
||||
"Alright, your new tag can be called with {}!\n\nWhat do you want to be displayed with this tag?".format(
|
||||
trigger))
|
||||
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long!")
|
||||
return
|
||||
|
||||
result = msg.content
|
||||
try:
|
||||
await my_msg.delete()
|
||||
await msg.delete()
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
|
||||
await ctx.send("I have just setup a new tag for this server! You can call your tag with {}".format(trigger))
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO tags(guild, creator, trigger, result) VALUES ($1, $2, $3, $4)",
|
||||
ctx.guild.id,
|
||||
ctx.author.id,
|
||||
trigger,
|
||||
result
|
||||
)
|
||||
|
||||
@tag.command(name='edit')
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def edit_tag(self, ctx, *, trigger: str):
|
||||
"""This will allow you to edit a tag that you have created
|
||||
EXAMPLE: !tag edit this tag
|
||||
RESULT: I'll ask what you want the new result to be"""
|
||||
def check(m):
|
||||
return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0
|
||||
|
||||
tag = await ctx.bot.db.fetchrow(
|
||||
"SELECT id, trigger FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
|
||||
ctx.guild.id,
|
||||
ctx.author.id,
|
||||
trigger
|
||||
)
|
||||
|
||||
if tag:
|
||||
my_msg = await ctx.send(f"Alright, what do you want the new result for the tag {tag} to be")
|
||||
try:
|
||||
msg = await ctx.bot.wait_for("message", check=check, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("You took too long!")
|
||||
return
|
||||
|
||||
new_result = msg.content
|
||||
|
||||
try:
|
||||
await my_msg.delete()
|
||||
await msg.delete()
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
|
||||
await ctx.send(f"Alright, the tag {trigger} has been updated")
|
||||
await ctx.bot.db.execute("UPDATE tags SET result=$1 WHERE id=$2", new_result, tag['id'])
|
||||
else:
|
||||
await ctx.send(f"You do not have a tag called {trigger} on this server!")
|
||||
|
||||
@tag.command(name='delete', aliases=['remove', 'stop'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def del_tag(self, ctx, *, trigger: str):
|
||||
"""Use this to remove a tag from use for this server
|
||||
Format to delete a tag is !tag delete <tag>
|
||||
|
||||
EXAMPLE: !tag delete stupid_tag
|
||||
RESULT: Deletes that stupid tag"""
|
||||
await self.bot.say("Temporarily disabled")
|
||||
# TODO: Fix tags, this will inherently fix this method
|
||||
"""r_filter = lambda row: (row['server_id'] == ctx.message.guild.id) & (row['tag'] == tag)
|
||||
if await utils.remove_content('tags', r_filter):
|
||||
await ctx.send('I have just removed the tag `{}`'.format(tag))
|
||||
|
||||
tag = await ctx.bot.db.fetchrow(
|
||||
"SELECT id FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
|
||||
ctx.guild.id,
|
||||
ctx.author.id,
|
||||
trigger
|
||||
)
|
||||
|
||||
if tag:
|
||||
await ctx.send(f"I have just deleted the tag {trigger}")
|
||||
await ctx.bot.db.execute("DELETE FROM tags WHERE id=$1", tag['id'])
|
||||
else:
|
||||
await ctx.send(
|
||||
"The tag {} does not exist! You can't remove something if it doesn't exist...".format(tag))"""
|
||||
await ctx.send(f"You do not own a tag called {trigger} on this server!")
|
||||
|
||||
@tag.command(name="info")
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def info_tag(self, ctx, *, trigger: str):
|
||||
"""Shows some information a bout the tag given"""
|
||||
|
||||
tag = await ctx.bot.db.fetchrow(
|
||||
"SELECT creator, uses, trigger FROM tags WHERE guild=$1 AND trigger=$2",
|
||||
ctx.guild.id,
|
||||
trigger
|
||||
)
|
||||
|
||||
if tag is not None:
|
||||
embed = discord.Embed(title=tag['trigger'])
|
||||
creator = ctx.guild.get_member(tag['creator'])
|
||||
if creator:
|
||||
embed.set_author(name=creator.display_name, url=creator.avatar_url)
|
||||
embed.add_field(name="Uses", value=tag['uses'])
|
||||
embed.add_field(name="Owner", value=creator.mention)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
await ctx.send(f"I cannot find a tag named '{trigger}'")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from discord.ext import commands
|
||||
import discord
|
||||
|
||||
from .utils import config
|
||||
from .utils import checks
|
||||
from .utils import utilities
|
||||
import utils
|
||||
|
||||
import re
|
||||
import random
|
||||
|
@ -99,10 +97,9 @@ class Board:
|
|||
return "```\n{}```".format(_board)
|
||||
|
||||
|
||||
class TicTacToe:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.boards = {}
|
||||
class TicTacToe(commands.Cog):
|
||||
"""Pretty self-explanatory"""
|
||||
boards = {}
|
||||
|
||||
def create(self, server_id, player1, player2):
|
||||
self.boards[server_id] = Board(player1, player2)
|
||||
|
@ -110,8 +107,9 @@ class TicTacToe:
|
|||
# Return whoever is x's so that we know who is going first
|
||||
return self.boards[server_id].challengers['x']
|
||||
|
||||
@commands.group(pass_context=True, aliases=['tic', 'tac', 'toe'], no_pm=True, invoke_without_command=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@commands.group(aliases=['tic', 'tac', 'toe'], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def tictactoe(self, ctx, *, option: str):
|
||||
"""Updates the current server's tic-tac-toe board
|
||||
You obviously need to be one of the players to use this
|
||||
|
@ -121,15 +119,15 @@ class TicTacToe:
|
|||
EXAMPLE: !tictactoe middle top
|
||||
RESULT: Your piece is placed in the very top space, in the middle"""
|
||||
player = ctx.message.author
|
||||
board = self.boards.get(ctx.message.server.id)
|
||||
board = self.boards.get(ctx.message.guild.id)
|
||||
# Need to make sure the board exists before allowing someone to play
|
||||
if not board:
|
||||
await self.bot.say("There are currently no Tic-Tac-Toe games setup!")
|
||||
await ctx.send("There are currently no Tic-Tac-Toe games setup!")
|
||||
return
|
||||
# Now just make sure the person can play, this will fail if o's are up and x tries to play
|
||||
# Or if someone else entirely tries to play
|
||||
if not board.can_play(player):
|
||||
await self.bot.say("You cannot play right now!")
|
||||
await ctx.send("You cannot play right now!")
|
||||
return
|
||||
|
||||
# Search for the positions in the option given, the actual match doesn't matter, just need to check if it exists
|
||||
|
@ -141,14 +139,14 @@ class TicTacToe:
|
|||
|
||||
# Just a bit of logic to ensure nothing that doesn't make sense is given
|
||||
if top and bottom:
|
||||
await self.bot.say("That is not a valid location! Use some logic, come on!")
|
||||
await ctx.send("That is not a valid location! Use some logic, come on!")
|
||||
return
|
||||
if left and right:
|
||||
await self.bot.say("That is not a valid location! Use some logic, come on!")
|
||||
await ctx.send("That is not a valid location! Use some logic, come on!")
|
||||
return
|
||||
# Make sure at least something was given
|
||||
if not top and not bottom and not left and not right and not middle:
|
||||
await self.bot.say("Please provide a valid location to play!")
|
||||
await ctx.send("Please provide a valid location to play!")
|
||||
return
|
||||
|
||||
x = 0
|
||||
|
@ -181,7 +179,7 @@ class TicTacToe:
|
|||
# board.update will handle which letter is placed
|
||||
# If it returns false however, then someone has already played in that spot and nothing was updated
|
||||
if not board.update(x, y):
|
||||
await self.bot.say("Someone has already played there!")
|
||||
await ctx.send("Someone has already played there!")
|
||||
return
|
||||
# Next check if there's a winner
|
||||
winner = board.check()
|
||||
|
@ -189,25 +187,32 @@ class TicTacToe:
|
|||
# Get the loser based on whether or not the winner is x's
|
||||
# If the winner is x's, the loser is o's...obviously, and vice-versa
|
||||
loser = board.challengers['x'] if board.challengers['x'] != winner else board.challengers['o']
|
||||
await self.bot.say("{} has won this game of TicTacToe, better luck next time {}".format(winner.display_name,
|
||||
loser.display_name))
|
||||
await ctx.send("{} has won this game of TicTacToe, better luck next time {}".format(winner.display_name,
|
||||
loser.display_name))
|
||||
# Handle updating ratings based on the winner and loser
|
||||
await utilities.update_records('tictactoe', winner, loser)
|
||||
await utils.update_records('tictactoe', ctx.bot.db, winner, loser)
|
||||
# This game has ended, delete it so another one can be made
|
||||
del self.boards[ctx.message.server.id]
|
||||
try:
|
||||
del self.boards[ctx.message.guild.id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# If no one has won, make sure the game is not full. If it has, delete the board and say it was a tie
|
||||
if board.full():
|
||||
await self.bot.say("This game has ended in a tie!")
|
||||
del self.boards[ctx.message.server.id]
|
||||
await ctx.send("This game has ended in a tie!")
|
||||
try:
|
||||
del self.boards[ctx.message.guild.id]
|
||||
except KeyError:
|
||||
pass
|
||||
# If no one has won, and the game has not ended in a tie, print the new updated board
|
||||
else:
|
||||
player_turn = board.challengers.get('x') if board.X_turn else board.challengers.get('o')
|
||||
fmt = str(board) + "\n{} It is now your turn to play!".format(player_turn.display_name)
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@tictactoe.command(name='start', aliases=['challenge', 'create'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(send_messages=True)
|
||||
@tictactoe.command(name='start', aliases=['challenge', 'create'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(send_messages=True)
|
||||
async def start_game(self, ctx, player2: discord.Member):
|
||||
"""Starts a game of tictactoe with another player
|
||||
|
||||
|
@ -216,32 +221,33 @@ class TicTacToe:
|
|||
player1 = ctx.message.author
|
||||
# For simplicities sake, only allow one game on a server at a time.
|
||||
# Things can easily get confusing (on the server's end) if we allow more than one
|
||||
if self.boards.get(ctx.message.server.id) is not None:
|
||||
await self.bot.say("Sorry but only one Tic-Tac-Toe game can be running per server!")
|
||||
if self.boards.get(ctx.message.guild.id) is not None:
|
||||
await ctx.send("Sorry but only one Tic-Tac-Toe game can be running per server!")
|
||||
return
|
||||
# Make sure we're not being challenged, I always win anyway
|
||||
if player2 == ctx.message.server.me:
|
||||
await self.bot.say("You want to play? Alright lets play.\n\nI win, so quick you didn't even notice it.")
|
||||
if player2 == ctx.message.guild.me:
|
||||
await ctx.send("You want to play? Alright lets play.\n\nI win, so quick you didn't even notice it.")
|
||||
return
|
||||
if player2 == player1:
|
||||
await self.bot.say("You can't play yourself, I won't allow it. Go find some friends")
|
||||
await ctx.send("You can't play yourself, I won't allow it. Go find some friends")
|
||||
return
|
||||
|
||||
# Create the board and return who has been decided to go first
|
||||
x_player = self.create(ctx.message.server.id, player1, player2)
|
||||
fmt = "A tictactoe game has just started between {} and {}".format(player1.display_name, player2.display_name)
|
||||
x_player = self.create(ctx.message.guild.id, player1, player2)
|
||||
fmt = "A tictactoe game has just started between {} and {}\n".format(player1.display_name, player2.display_name)
|
||||
# Print the board too just because
|
||||
fmt += str(self.boards[ctx.message.server.id])
|
||||
fmt += str(self.boards[ctx.message.guild.id])
|
||||
|
||||
# We don't need to do anything weird with assigning x_player to something
|
||||
# it is already a member object, just use it
|
||||
fmt += "I have decided at random, and {} is going to be x's this game. It is your turn first! " \
|
||||
"Use the {}tictactoe command, and a position, to choose where you want to play"\
|
||||
"Use the {}tictactoe command, and a position, to choose where you want to play" \
|
||||
.format(x_player.display_name, ctx.prefix)
|
||||
await self.bot.say(fmt)
|
||||
await ctx.send(fmt)
|
||||
|
||||
@tictactoe.command(name='delete', aliases=['stop', 'remove', 'end'], pass_context=True, no_pm=True)
|
||||
@checks.custom_perms(kick_members=True)
|
||||
@tictactoe.command(name='delete', aliases=['stop', 'remove', 'end'])
|
||||
@commands.guild_only()
|
||||
@utils.can_run(kick_members=True)
|
||||
async def stop_game(self, ctx):
|
||||
"""Force stops a game of tictactoe
|
||||
This should realistically only be used in a situation like one player leaves
|
||||
|
@ -249,12 +255,12 @@ class TicTacToe:
|
|||
|
||||
EXAMPLE: !tictactoe stop
|
||||
RESULT: No more tictactoe!"""
|
||||
if self.boards.get(ctx.message.server.id) is None:
|
||||
await self.bot.say("There are no tictactoe games running on this server!")
|
||||
if self.boards.get(ctx.message.guild.id) is None:
|
||||
await ctx.send("There are no tictactoe games running on this server!")
|
||||
return
|
||||
|
||||
del self.boards[ctx.message.server.id]
|
||||
await self.bot.say("I have just stopped the game of TicTacToe, a new should be able to be started now!")
|
||||
del self.boards[ctx.message.guild.id]
|
||||
await ctx.send("I have just stopped the game of TicTacToe, a new should be able to be started now!")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
|
124
cogs/tutorial.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from discord.ext import commands
|
||||
|
||||
import utils
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class Tutorial(commands.Cog):
|
||||
@commands.command()
|
||||
# @utils.can_run(send_messages=True)
|
||||
async def tutorial(self, ctx, *, cmd_or_cog = None):
|
||||
# The message we'll use to send
|
||||
output = ""
|
||||
|
||||
# The list of commands we need to run through
|
||||
commands = []
|
||||
if cmd_or_cog:
|
||||
cmd = ctx.bot.get_command(cmd_or_cog.lower())
|
||||
# This should be a cog
|
||||
if cmd is None:
|
||||
cog = ctx.bot.get_cog(cmd_or_cog.title())
|
||||
if cog is None:
|
||||
await ctx.send("Could not find a command or a cog for {}".format(cmd_or_cog))
|
||||
return
|
||||
|
||||
commands = set([
|
||||
c
|
||||
for c in ctx.bot.walk_commands()
|
||||
if c.cog_name == cmd_or_cog.title()
|
||||
])
|
||||
# Specific command
|
||||
else:
|
||||
commands = [cmd]
|
||||
# Use all commands
|
||||
else:
|
||||
commands = set(ctx.bot.walk_commands())
|
||||
|
||||
# Loop through all the commands that we want to use
|
||||
for command in commands:
|
||||
embed = self.generate_embed(command)
|
||||
# await ctx.author.send(embed=embed)
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
def generate_embed(self, command):
|
||||
# Create the embed object
|
||||
opts = {
|
||||
"title": "`{}` command tutorial:\n\n".format(command.qualified_name),
|
||||
"colour": discord.Colour.green()
|
||||
}
|
||||
embed = discord.Embed(**opts)
|
||||
|
||||
if command.help is not None:
|
||||
# Split into examples, results, and the description itself based on the string
|
||||
description, _, rest = command.help.partition('EXAMPLE:')
|
||||
example, _, rest = rest.partition('RESULT:')
|
||||
result, _, gif = rest.partition("GIF:")
|
||||
else:
|
||||
example = None
|
||||
result = None
|
||||
gif = None
|
||||
|
||||
# Add a field for the aliases
|
||||
if command.aliases:
|
||||
embed.add_field(
|
||||
name="Aliases",
|
||||
value="\n".join(["\t{}".format(alias) for alias in command.aliases]),
|
||||
inline=False
|
||||
)
|
||||
# Add any paramaters needed
|
||||
if command.clean_params:
|
||||
params = []
|
||||
for key, value in command.clean_params.items():
|
||||
# Get the parameter type, as well as the default value if it exists
|
||||
param_type, has_default, default_value = str(value).partition("=")
|
||||
try:
|
||||
# We want everything after the :
|
||||
param_type = param_type.split(":")[1]
|
||||
# Now we want to split based on . (for possible deep level types IE discord.member.Member) then get the last value
|
||||
param_type = param_type.split(".")
|
||||
param_type = param_type[len(param_type) - 1]
|
||||
# This could mean something like *param was provided as the parameter
|
||||
except IndexError:
|
||||
param_type = "str"
|
||||
|
||||
# Start the string that we'll use as the param's info
|
||||
string = "{} (Type: {}".format(key, param_type)
|
||||
|
||||
if default_value:
|
||||
string += ", Default: {}".format(default_value)
|
||||
|
||||
# This is the = from the partition, if it exists, then there's a default...hence the name
|
||||
if has_default:
|
||||
string += ", optional)"
|
||||
else:
|
||||
string += ", required)"
|
||||
|
||||
# Now push our string to the list of params
|
||||
params.append(string)
|
||||
name = "Paramaters"
|
||||
embed.add_field(name=name, value="\n".join(params), inline=False)
|
||||
# Set the description of the embed to the description
|
||||
if description:
|
||||
embed.description = description
|
||||
# Add these two in one embed
|
||||
if example and result:
|
||||
embed.add_field(
|
||||
name="Example",
|
||||
value="{}\n{}".format(example.strip(), result.strip()),
|
||||
inline=False
|
||||
)
|
||||
try:
|
||||
can_run = [func for func in command.checks if "can_run" in func.__qualname__][0]
|
||||
perms = ",".join(attribute for attribute, setting in can_run.perms.items() if setting)
|
||||
embed.set_footer(text="Permissions required: {}".format(perms))
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Tutorial(bot))
|
240
cogs/twitch.py
|
@ -1,240 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from . import utils
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import discord
|
||||
import re
|
||||
import rethinkdb as r
|
||||
import traceback
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
class Twitch:
|
||||
"""Class for some twitch integration
|
||||
You can add or remove your twitch stream for your user
|
||||
I will then notify the server when you have gone live or offline"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.key = utils.twitch_key
|
||||
self.params = {'client_id': self.key}
|
||||
|
||||
async def channel_online(self, twitch_url: str):
|
||||
# Check a specific channel's data, and get the response in text format
|
||||
channel = re.search("(?<=twitch.tv/)(.*)", twitch_url).group(1)
|
||||
url = "https://api.twitch.tv/kraken/streams/{}".format(channel)
|
||||
|
||||
response = await utils.request(url, payload=self.params)
|
||||
|
||||
# For some reason Twitch's API call is not reliable, sometimes it returns stream as None
|
||||
# That is what we're checking specifically, sometimes it doesn't exist in the returned JSON at all
|
||||
# Sometimes it returns something that cannot be decoded with JSON (which means we'll get None back)
|
||||
# In either error case, just assume they're offline, the next check will most likely work
|
||||
try:
|
||||
return response['stream'] is not None
|
||||
except (KeyError, TypeError):
|
||||
return False
|
||||
|
||||
async def check_channels(self):
|
||||
await self.bot.wait_until_ready()
|
||||
# Loop through as long as the bot is connected
|
||||
try:
|
||||
while not self.bot.is_closed:
|
||||
twitch = await utils.filter_content('twitch', {'notifications_on': 1})
|
||||
for data in twitch:
|
||||
m_id = data['member_id']
|
||||
url = data['twitch_url']
|
||||
# Check if they are online
|
||||
online = await self.channel_online(url)
|
||||
# If they're currently online, but saved as not then we'll send our notification
|
||||
if online and data['live'] == 0:
|
||||
for s_id in data['servers']:
|
||||
server = self.bot.get_server(s_id)
|
||||
if server is None:
|
||||
continue
|
||||
member = server.get_member(m_id)
|
||||
if member is None:
|
||||
continue
|
||||
server_settings = await utils.get_content('server_settings', s_id)
|
||||
if server_settings is not None:
|
||||
channel_id = server_settings.get('notification_channel', s_id)
|
||||
else:
|
||||
channel_id = s_id
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "{} has just gone live! View their stream at <{}>".format(member.display_name, data['twitch_url']))
|
||||
self.bot.loop.create_task(utils.update_content('twitch', {'live': 1}, m_id))
|
||||
elif not online and data['live'] == 1:
|
||||
for s_id in data['servers']:
|
||||
server = self.bot.get_server(s_id)
|
||||
if server is None:
|
||||
continue
|
||||
member = server.get_member(m_id)
|
||||
if member is None:
|
||||
continue
|
||||
server_settings = await utils.get_content('server_settings', s_id)
|
||||
if server_settings is not None:
|
||||
channel_id = server_settings.get('notification_channel', s_id)
|
||||
else:
|
||||
channel_id = s_id
|
||||
channel = server.get_channel(channel_id)
|
||||
await self.bot.send_message(channel, "{} has just gone offline! View their stream next time at <{}>".format(member.display_name, data['twitch_url']))
|
||||
self.bot.loop.create_task(utils.update_content('twitch', {'live': 0}, m_id))
|
||||
await asyncio.sleep(30)
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
fmt = "{1}\n{0.__class__.__name__}: {0}".format(tb, e)
|
||||
log.error(fmt)
|
||||
|
||||
@commands.group(no_pm=True, invoke_without_command=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def twitch(self, ctx, *, member: discord.Member = None):
|
||||
"""Use this command to check the twitch info of a user
|
||||
|
||||
EXAMPLE: !twitch @OtherPerson
|
||||
RESULT: Information about their twitch URL"""
|
||||
await ctx.message.channel.trigger_typing()
|
||||
|
||||
if member is None:
|
||||
member = ctx.message.author
|
||||
|
||||
result = await utils.get_content('twitch', member.id)
|
||||
if result is None:
|
||||
await self.bot.say("{} has not saved their twitch URL yet!".format(member.name))
|
||||
return
|
||||
|
||||
url = result['twitch_url']
|
||||
user = re.search("(?<=twitch.tv/)(.*)", url).group(1)
|
||||
twitch_url = "https://api.twitch.tv/kraken/channels/{}".format(user)
|
||||
payload = {'client_id': self.key}
|
||||
data = await utils.request(twitch_url, payload=payload)
|
||||
|
||||
embed = discord.Embed(title=data['display_name'], url=url)
|
||||
if data['logo']:
|
||||
embed.set_thumbnail(url=data['logo'])
|
||||
|
||||
embed.add_field(name='Title', value=data['status'])
|
||||
embed.add_field(name='Followers', value=data['followers'])
|
||||
embed.add_field(name='Views', value=data['views'])
|
||||
if data['game']:
|
||||
embed.add_field(name='Game', value=data['game'])
|
||||
embed.add_field(name='Language', value=data['broadcaster_language'])
|
||||
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
@twitch.command(name='add', no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def add_twitch_url(self, ctx, url: str):
|
||||
"""Saves your user's twitch URL
|
||||
|
||||
EXAMPLE: !twitch add MyTwitchName
|
||||
RESULT: Saves your twitch URL; notifications will be sent to this server when you go live"""
|
||||
await ctx.message.channel.trigger_typing()
|
||||
|
||||
# This uses a lookbehind to check if twitch.tv exists in the url given
|
||||
# If it does, it matches twitch.tv/user and sets the url as that
|
||||
# Then (in the else) add https://www. to that
|
||||
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
|
||||
# This means that the url was just given as a user (or something complete invalid)
|
||||
# So set URL as https://www.twitch.tv/[url]
|
||||
# Even if this was invalid such as https://www.twitch.tv/google.com/
|
||||
# For example, our next check handles that
|
||||
try:
|
||||
url = re.search("((?<=://)?twitch.tv/)+(.*)", url).group(0)
|
||||
except AttributeError:
|
||||
url = "https://www.twitch.tv/{}".format(url)
|
||||
else:
|
||||
url = "https://www.{}".format(url)
|
||||
|
||||
# Try to find the channel provided, we'll get a 404 response if it does not exist
|
||||
status = await utils.request(url, attr='status')
|
||||
if not status == 200:
|
||||
await self.bot.say("That twitch user does not exist! "
|
||||
"What would be the point of adding a nonexistant twitch user? Silly")
|
||||
return
|
||||
|
||||
key = ctx.message.author.id
|
||||
entry = {'twitch_url': url,
|
||||
'servers': [ctx.message.server.id],
|
||||
'notifications_on': 1,
|
||||
'live': 0,
|
||||
'member_id': key}
|
||||
update = {'twitch_url': url}
|
||||
|
||||
# Check to see if this user has already saved a twitch URL
|
||||
# If they have, update the URL, otherwise create a new entry
|
||||
# Assuming they're not live, and notifications should be on
|
||||
if not await utils.add_content('twitch', entry):
|
||||
await utils.update_content('twitch', update, key)
|
||||
await self.bot.say("I have just saved your twitch url {}".format(ctx.message.author.mention))
|
||||
|
||||
@twitch.command(name='remove', aliases=['delete'], no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def remove_twitch_url(self, ctx):
|
||||
"""Removes your twitch URL
|
||||
|
||||
EXAMPLE: !twitch remove
|
||||
RESULT: I stop saving your twitch URL"""
|
||||
# Just try to remove it, if it doesn't exist, nothing is going to happen
|
||||
await utils.remove_content('twitch', ctx.message.author.id)
|
||||
await self.bot.say("I am no longer saving your twitch URL {}".format(ctx.message.author.mention))
|
||||
|
||||
@twitch.group(no_pm=True, invoke_without_command=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify(self, ctx):
|
||||
"""This can be used to modify notification settings for your twitch user
|
||||
Call this command by itself to add 'this' server as one that will be notified when you on/offline
|
||||
|
||||
EXAMPLE: !twitch notify
|
||||
RESULT: This server will now be notified when you go live"""
|
||||
key = ctx.message.author.id
|
||||
result = await utils.get_content('twitch', key)
|
||||
# Check if this user is saved at all
|
||||
if result is None:
|
||||
await self.bot.say(
|
||||
"I do not have your twitch URL added {}. You can save your twitch url with !twitch add".format(
|
||||
ctx.message.author.mention))
|
||||
# Then check if this server is already added as one to notify in
|
||||
elif ctx.message.server.id in result['servers']:
|
||||
await self.bot.say("I am already set to notify in this server...")
|
||||
else:
|
||||
await utils.update_content('twitch', {'servers': r.row['servers'].append(ctx.message.server.id)}, key)
|
||||
await self.bot.say("This server will now be notified if you go live")
|
||||
|
||||
@notify.command(name='on', aliases=['start,yes'], no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify_on(self, ctx):
|
||||
"""Turns twitch notifications on
|
||||
|
||||
EXAMPLE: !twitch notify on
|
||||
RESULT: Notifications will be sent when you go live"""
|
||||
if await utils.update_content('twitch', {"notifications_on": 1}, ctx.message.author.id):
|
||||
await self.bot.say("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format(
|
||||
ctx.message.author.mention))
|
||||
else:
|
||||
await self.bot.say("I can't notify if you go live if I don't know your twitch URL yet!")
|
||||
|
||||
@notify.command(name='off', aliases=['stop,no'], no_pm=True, pass_context=True)
|
||||
@utils.custom_perms(send_messages=True)
|
||||
async def notify_off(self, ctx):
|
||||
"""Turns twitch notifications off
|
||||
|
||||
EXAMPLE: !twitch notify off
|
||||
RESULT: Notifications will not be sent when you go live"""
|
||||
if await utils.update_content('twitch', {"notifications_on": 0}, ctx.message.author.id):
|
||||
await self.bot.say(
|
||||
"I will not notify if you go live anymore {}, "
|
||||
"are you going to stream some lewd stuff you don't want people to see?~".format(
|
||||
ctx.message.author.mention))
|
||||
else:
|
||||
await self.bot.say(
|
||||
"I mean, I'm already not going to notify anyone, because I don't have your twitch URL saved...")
|
||||
|
||||
|
||||
def setup(bot):
|
||||
t = Twitch(bot)
|
||||
bot.loop.create_task(t.check_channels())
|
||||
bot.add_cog(Twitch(bot))
|
|
@ -1,6 +0,0 @@
|
|||
from .cards import Deck
|
||||
from .checks import is_owner, custom_perms, db_check
|
||||
from .config import *
|
||||
from .utilities import *
|
||||
from .images import create_banner
|
||||
from .paginator import Pages, CannotPaginate
|
|
@ -1,43 +0,0 @@
|
|||
import itertools
|
||||
import random
|
||||
|
||||
suits = ['S', 'C', 'H', 'D']
|
||||
faces = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
|
||||
|
||||
class Deck:
|
||||
def __init__(self, prefill=True):
|
||||
# itertools.product creates us a tuple based on every output of our faces and suits
|
||||
# This is EXACTLY what a deck of normal playing cards is, so it's perfect
|
||||
if prefill:
|
||||
self.deck = list(itertools.product(suits, faces))
|
||||
else:
|
||||
self.deck = []
|
||||
|
||||
def __iter__(self):
|
||||
for card in self.deck:
|
||||
yield card
|
||||
@property
|
||||
def count(self):
|
||||
"""A property to provide how many cards are currently in the deck"""
|
||||
return len(self.deck)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
"""A property to determine whether or not the deck has cards in it"""
|
||||
return len(self.deck) == 0
|
||||
|
||||
def draw(self, count=1):
|
||||
"""Generator to draw from the deck"""
|
||||
try:
|
||||
for i in range(count):
|
||||
yield self.deck.pop()
|
||||
except IndexError:
|
||||
yield None
|
||||
|
||||
def insert(self, cards):
|
||||
"""Adds the provided cards to the end of the deck"""
|
||||
self.deck.extend(cards)
|
||||
|
||||
def shuffle(self):
|
||||
"""Shuffles the deck in place"""
|
||||
random.SystemRandom().shuffle(self.deck)
|
|
@ -1,94 +0,0 @@
|
|||
import asyncio
|
||||
import rethinkdb as r
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
from . import config
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# The tables needed for the database, as well as their primary keys
|
||||
required_tables = {
|
||||
'battle_records': 'member_id',
|
||||
'boops': 'member_id',
|
||||
'command_usage': 'command',
|
||||
'motd': 'date',
|
||||
'overwatch': 'member_id',
|
||||
'picarto': 'member_id',
|
||||
'server_settings': 'server_id',
|
||||
'raffles': 'id',
|
||||
'strawpolls': 'server_id',
|
||||
'osu': 'member_id',
|
||||
'tags': 'server_id',
|
||||
'tictactoe': 'member_id',
|
||||
'twitch': 'member_id'
|
||||
}
|
||||
|
||||
|
||||
async def db_check():
|
||||
"""Used to check if the required database/tables are setup"""
|
||||
db_opts = config.db_opts
|
||||
|
||||
r.set_loop_type('asyncio')
|
||||
# First try to connect, and see if the correct information was provided
|
||||
try:
|
||||
conn = await r.connect(**db_opts)
|
||||
except r.errors.ReqlDriverError:
|
||||
print("Cannot connect to the RethinkDB instance with the following information: {}".format(db_opts))
|
||||
|
||||
print("The RethinkDB instance you have setup may be down, otherwise please ensure you setup a"
|
||||
" RethinkDB instance, and you have provided the correct database information in config.yml")
|
||||
quit()
|
||||
return
|
||||
|
||||
# Get the current databases and check if the one we need is there
|
||||
dbs = await r.db_list().run(conn)
|
||||
if db_opts['db'] not in dbs:
|
||||
# If not, we want to create it
|
||||
print('Couldn\'t find database {}...creating now'.format(db_opts['db']))
|
||||
await r.db_create(db_opts['db']).run(conn)
|
||||
# Then add all the tables
|
||||
for table, key in required_tables.items():
|
||||
print("Creating table {}...".format(table))
|
||||
await r.table_create(table, primary_key=key).run(conn)
|
||||
print("Done!")
|
||||
else:
|
||||
# Otherwise, if the database is setup, make sure all the required tables are there
|
||||
tables = await r.table_list().run(conn)
|
||||
for table, key in required_tables.items():
|
||||
if table not in tables:
|
||||
print("Creating table {}...".format(table))
|
||||
await r.table_create(table, primary_key=key).run(conn)
|
||||
print("Done checking tables!")
|
||||
|
||||
|
||||
def is_owner(ctx):
|
||||
return ctx.message.author.id in config.owner_ids
|
||||
|
||||
|
||||
def custom_perms(**perms):
|
||||
def predicate(ctx):
|
||||
# Return true if this is a private channel, we'll handle that in the registering of the command
|
||||
if ctx.message.channel.is_private:
|
||||
return True
|
||||
|
||||
# Get the member permissions so that we can compare
|
||||
member_perms = ctx.message.author.permissions_in(ctx.message.channel)
|
||||
# Next, set the default permissions if one is not used, based on what was passed
|
||||
# This will be overriden later, if we have custom permissions
|
||||
required_perm = discord.Permissions.none()
|
||||
for perm, setting in perms.items():
|
||||
setattr(required_perm, perm, setting)
|
||||
|
||||
try:
|
||||
server_settings = config.cache.get('server_settings').values
|
||||
required_perm_value = [x for x in server_settings if x['server_id'] == ctx.message.server.id][0]['permissions'][ctx.command.qualified_name]
|
||||
required_perm = discord.Permissions(required_perm_value)
|
||||
except (TypeError, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
# Now just check if the person running the command has these permissions
|
||||
return member_perms >= required_perm
|
||||
|
||||
predicate.perms = perms
|
||||
return commands.check(predicate)
|
|
@ -1,243 +0,0 @@
|
|||
import ruamel.yaml as yaml
|
||||
import asyncio
|
||||
import rethinkdb as r
|
||||
import pendulum
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
global_config = {}
|
||||
|
||||
# Ensure that the required config.yml file actually exists
|
||||
try:
|
||||
with open("config.yml", "r") as f:
|
||||
global_config = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
print("You have no config file setup! Please use config.yml.sample to setup a valid config file")
|
||||
quit()
|
||||
|
||||
try:
|
||||
bot_token = global_config["bot_token"]
|
||||
except KeyError:
|
||||
print("You have no bot_token saved, this is a requirement for running a bot.")
|
||||
print("Please use config.yml.sample to setup a valid config file")
|
||||
quit()
|
||||
|
||||
try:
|
||||
owner_ids = global_config["owner_id"]
|
||||
except KeyError:
|
||||
print("You have no owner_id saved! You're not going to be able to run certain commands without this.")
|
||||
print("Please use config.yml.sample to setup a valid config file")
|
||||
quit()
|
||||
|
||||
|
||||
# This is a simple class for the cache concept, all it holds is it's own key and the values
|
||||
# With a method that gets content based on it's key
|
||||
class Cache:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.values = {}
|
||||
self.refreshed = pendulum.utcnow()
|
||||
loop.create_task(self.update())
|
||||
|
||||
async def update(self):
|
||||
self.values = await get_content(self.key)
|
||||
self.refreshed = pendulum.utcnow()
|
||||
|
||||
|
||||
# Default bot's description
|
||||
bot_description = global_config.get("description")
|
||||
# Bot's default prefix for commands
|
||||
default_prefix = global_config.get("command_prefix", "!")
|
||||
# The sharding information
|
||||
shard_count = global_config.get("shard_count", 1)
|
||||
shard_id = global_config.get("shard_id", 1)
|
||||
# The key for bots.discord.pw and carbonitex
|
||||
discord_bots_key = global_config.get('discord_bots_key', "")
|
||||
carbon_key = global_config.get('carbon_key', "")
|
||||
# The client ID for twitch requsets
|
||||
twitch_key = global_config.get('twitch_key', "")
|
||||
# The steam API key
|
||||
steam_key = global_config.get("steam_key", "")
|
||||
# The key for youtube API calls
|
||||
youtube_key = global_config.get("youtube_key", "")
|
||||
# The key for Osu API calls
|
||||
osu_key = global_config.get('osu_key', '')
|
||||
# The key for League of Legends API calls
|
||||
lol_key = global_config.get('lol_key', '')
|
||||
# The keys needed for deviant art calls
|
||||
da_id = global_config.get("da_id", "")
|
||||
da_secret = global_config.get("da_secret", "")
|
||||
# The invite link for the server made for the bot
|
||||
dev_server = global_config.get("dev_server", "")
|
||||
# The User-Agent that we'll use for most requests
|
||||
user_agent = global_config.get('user_agent', "")
|
||||
# The extensions to load
|
||||
extensions = global_config.get('extensions', [])
|
||||
|
||||
# The default status the bot will use
|
||||
default_status = global_config.get("default_status", None)
|
||||
# The URL that will be used to link to for the help command
|
||||
help_url = global_config.get("help_url", "")
|
||||
# The rethinkdb hostname
|
||||
db_host = global_config.get('db_host', 'localhost')
|
||||
# The rethinkdb database name
|
||||
db_name = global_config.get('db_name', 'Discord_Bot')
|
||||
# The rethinkdb certification
|
||||
db_cert = global_config.get('db_cert', '')
|
||||
# The rethinkdb port
|
||||
db_port = global_config.get('db_port', 28015)
|
||||
# The user and password assigned
|
||||
db_user = global_config.get('db_user', 'admin')
|
||||
db_pass = global_config.get('db_pass', '')
|
||||
# We've set all the options we need to be able to connect
|
||||
# so create a dictionary that we can use to unload to connect
|
||||
# db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'ssl':
|
||||
# {'ca_certs': db_cert}, 'user': db_user, 'password': db_pass}
|
||||
db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
|
||||
|
||||
# This will be a dictionary that holds the cache object, based on the key that is saved
|
||||
cache = {}
|
||||
|
||||
# Populate cache with each object
|
||||
# With the new saving method, we're not going to be able to cache the way that I was before
|
||||
# This is on standby until I rethink how to do this, because I do still want to cache data
|
||||
"""for k in possible_keys:
|
||||
ca che[k] = Cache(k)"""
|
||||
|
||||
# We still need 'cache' for prefixes and custom permissions however, so for now, just include that
|
||||
cache['server_settings'] = Cache('server_settings')
|
||||
|
||||
async def update_cache():
|
||||
for value in cache.values():
|
||||
await value.update()
|
||||
|
||||
|
||||
def command_prefix(bot, message):
|
||||
# We do not want to make a query for every message that is sent
|
||||
# So assume it's in cache, or it doesn't exist
|
||||
# If the prefix does exist in the database and isn't in our cache; too bad, something has messed up
|
||||
# But it is not worth a query for every single message the bot detects, to fix
|
||||
try:
|
||||
prefixes = cache['server_settings'].values
|
||||
prefix = [x for x in prefixes if x['server_id'] == message.server.id][0]['prefix']
|
||||
return prefix or default_prefix
|
||||
except (KeyError, TypeError, IndexError, AttributeError):
|
||||
return default_prefix
|
||||
|
||||
|
||||
async def add_content(table, content):
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
# First we need to make sure that this entry doesn't exist
|
||||
# For all rethinkDB cares, multiple entries can exist with the same content
|
||||
# For our purposes however, we do not want this
|
||||
try:
|
||||
result = await r.table(table).insert(content).run(conn)
|
||||
except r.ReqlOpFailedError:
|
||||
# This means the table does not exist
|
||||
await r.table_create(table).run(conn)
|
||||
await r.table(table).insert(content).run(conn)
|
||||
result = {}
|
||||
|
||||
await conn.close()
|
||||
if table == 'server_settings':
|
||||
loop.create_task(cache[table].update())
|
||||
return result.get('inserted', 0) > 0
|
||||
|
||||
|
||||
async def remove_content(table, key):
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
try:
|
||||
result = await r.table(table).get(key).delete().run(conn)
|
||||
except r.ReqlOpFailedError:
|
||||
result = {}
|
||||
pass
|
||||
|
||||
await conn.close()
|
||||
if table == 'server_settings':
|
||||
loop.create_task(cache[table].update())
|
||||
return result.get('deleted', 0) > 0
|
||||
|
||||
|
||||
async def update_content(table, content, key):
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
# This method is only for updating content, so if we find that it doesn't exist, just return false
|
||||
try:
|
||||
# Update based on the content and filter passed to us
|
||||
# rethinkdb allows you to do many many things inside of update
|
||||
# This is why we're accepting a variable and using it, whatever it may be, as the query
|
||||
result = await r.table(table).get(key).update(content).run(conn)
|
||||
except r.ReqlOpFailedError:
|
||||
result = {}
|
||||
|
||||
await conn.close()
|
||||
if table == 'server_settings':
|
||||
loop.create_task(cache[table].update())
|
||||
return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0
|
||||
|
||||
|
||||
async def replace_content(table, content, key):
|
||||
# This method is here because .replace and .update can have some different functionalities
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
try:
|
||||
result = await r.table(table).get(key).replace(content).run(conn)
|
||||
except r.ReqlOpFailedError:
|
||||
result = {}
|
||||
|
||||
await conn.close()
|
||||
if table == 'server_settings':
|
||||
loop.create_task(cache[table].update())
|
||||
return result.get('replaced', 0) > 0 or result.get('unchanged', 0) > 0
|
||||
|
||||
|
||||
async def get_content(table, key=None):
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
|
||||
try:
|
||||
if key:
|
||||
cursor = await r.table(table).get(key).run(conn)
|
||||
else:
|
||||
cursor = await r.table(table).run(conn)
|
||||
if cursor is None:
|
||||
content = None
|
||||
elif type(cursor) is not dict:
|
||||
content = await _convert_to_list(cursor)
|
||||
if len(content) == 0:
|
||||
content = None
|
||||
else:
|
||||
content = cursor
|
||||
except (IndexError, r.ReqlOpFailedError):
|
||||
content = None
|
||||
|
||||
await conn.close()
|
||||
return content
|
||||
|
||||
async def filter_content(table: str, r_filter):
|
||||
r.set_loop_type("asyncio")
|
||||
conn = await r.connect(**db_opts)
|
||||
try:
|
||||
cursor = await r.table(table).filter(r_filter).run(conn)
|
||||
content = await _convert_to_list(cursor)
|
||||
if len(content) == 0:
|
||||
content = None
|
||||
except (IndexError, r.ReqlOpFailedError):
|
||||
content = None
|
||||
|
||||
await conn.close()
|
||||
return content
|
||||
|
||||
|
||||
async def _convert_to_list(cursor):
|
||||
# This method is here because atm, AsyncioCursor is not iterable
|
||||
# For our purposes, we want a list, so we need to do this manually
|
||||
cursor_list = []
|
||||
while True:
|
||||
try:
|
||||
val = await cursor.next()
|
||||
cursor_list.append(val)
|
||||
except r.ReqlCursorEmpty:
|
||||
break
|
||||
return cursor_list
|
|
@ -1,195 +0,0 @@
|
|||
import asyncio
|
||||
import discord
|
||||
|
||||
class CannotPaginate(Exception):
|
||||
pass
|
||||
|
||||
class Pages:
|
||||
"""Implements a paginator that queries the user for the
|
||||
pagination interface.
|
||||
|
||||
Pages are 1-index based, not 0-index based.
|
||||
|
||||
If the user does not reply within 2 minutes, the pagination
|
||||
interface exits automatically.
|
||||
"""
|
||||
def __init__(self, bot, *, message, entries, per_page=10):
|
||||
self.bot = bot
|
||||
self.entries = entries
|
||||
self.message = message
|
||||
self.author = message.author
|
||||
self.per_page = per_page
|
||||
pages, left_over = divmod(len(self.entries), self.per_page)
|
||||
if left_over:
|
||||
pages += 1
|
||||
self.maximum_pages = pages
|
||||
self.embed = discord.Embed()
|
||||
self.paginating = len(entries) > per_page
|
||||
self.reaction_emojis = [
|
||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
|
||||
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
|
||||
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
|
||||
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
|
||||
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
|
||||
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
|
||||
('\N{INFORMATION SOURCE}', self.show_help),
|
||||
]
|
||||
|
||||
server = self.message.server
|
||||
if server is not None:
|
||||
self.permissions = self.message.channel.permissions_for(server.me)
|
||||
else:
|
||||
self.permissions = self.message.channel.permissions_for(self.bot.user)
|
||||
|
||||
if not self.permissions.embed_links:
|
||||
raise CannotPaginate('Bot does not have embed links permission.')
|
||||
|
||||
def get_page(self, page):
|
||||
base = (page - 1) * self.per_page
|
||||
return self.entries[base:base + self.per_page]
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
p = []
|
||||
for t in enumerate(entries, 1 + ((page - 1) * self.per_page)):
|
||||
p.append('%s. %s' % t)
|
||||
|
||||
self.embed.set_footer(text='Page %s/%s (%s entries)' % (page, self.maximum_pages, len(self.entries)))
|
||||
|
||||
if not self.paginating:
|
||||
self.embed.description = '\n'.join(p)
|
||||
return await self.bot.send_message(self.message.channel, embed=self.embed)
|
||||
|
||||
if not first:
|
||||
self.embed.description = '\n'.join(p)
|
||||
await self.bot.edit_message(self.message, embed=self.embed)
|
||||
return
|
||||
|
||||
# verify we can actually use the pagination session
|
||||
if not self.permissions.add_reactions:
|
||||
raise CannotPaginate('Bot does not have add reactions permission.')
|
||||
|
||||
if not self.permissions.read_message_history:
|
||||
raise CannotPaginate('Bot does not have Read Message History permission.')
|
||||
|
||||
p.append('')
|
||||
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
|
||||
self.embed.description = '\n'.join(p)
|
||||
self.message = await self.bot.send_message(self.message.channel, embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
try:
|
||||
await self.bot.add_reaction(self.message, reaction)
|
||||
except discord.NotFound:
|
||||
# If the message isn't found, we don't care about clearing anything
|
||||
return
|
||||
|
||||
async def checked_show_page(self, page):
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
|
||||
async def first_page(self):
|
||||
"""goes to the first page"""
|
||||
await self.show_page(1)
|
||||
|
||||
async def last_page(self):
|
||||
"""goes to the last page"""
|
||||
await self.show_page(self.maximum_pages)
|
||||
|
||||
async def next_page(self):
|
||||
"""goes to the next page"""
|
||||
await self.checked_show_page(self.current_page + 1)
|
||||
|
||||
async def previous_page(self):
|
||||
"""goes to the previous page"""
|
||||
await self.checked_show_page(self.current_page - 1)
|
||||
|
||||
async def show_current_page(self):
|
||||
if self.paginating:
|
||||
await self.show_page(self.current_page)
|
||||
|
||||
async def numbered_page(self):
|
||||
"""lets you type a page number to go to"""
|
||||
to_delete = []
|
||||
to_delete.append(await self.bot.send_message(self.message.channel, 'What page do you want to go to?'))
|
||||
msg = await self.bot.wait_for_message(author=self.author, channel=self.message.channel,
|
||||
check=lambda m: m.content.isdigit(), timeout=30.0)
|
||||
if msg is not None:
|
||||
page = int(msg.content)
|
||||
to_delete.append(msg)
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
else:
|
||||
to_delete.append(await self.bot.say('Invalid page given. (%s/%s)' % (page, self.maximum_pages)))
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
to_delete.append(await self.bot.send_message(self.message.channel, 'Took too long.'))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
try:
|
||||
await self.bot.delete_messages(to_delete)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
e = discord.Embed()
|
||||
messages = ['Welcome to the interactive paginator!\n']
|
||||
messages.append('This interactively allows you to see pages of text by navigating with ' \
|
||||
'reactions. They are as follows:\n')
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
messages.append('%s %s' % (emoji, func.__doc__))
|
||||
|
||||
e.description = '\n'.join(messages)
|
||||
e.colour = 0x738bd7 # blurple
|
||||
e.set_footer(text='We were on page %s before this message.' % self.current_page)
|
||||
await self.bot.edit_message(self.message, embed=e)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(60.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def stop_pages(self):
|
||||
"""stops the interactive pagination session"""
|
||||
await self.bot.delete_message(self.message)
|
||||
self.paginating = False
|
||||
|
||||
def react_check(self, reaction, user):
|
||||
if user is None or user.id != self.author.id:
|
||||
return False
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
if reaction.emoji == emoji:
|
||||
self.match = func
|
||||
return True
|
||||
return False
|
||||
|
||||
async def paginate(self, start_page=1):
|
||||
"""Actually paginate the entries and run the interactive loop if necessary."""
|
||||
await self.show_page(start_page, first=True)
|
||||
|
||||
while self.paginating:
|
||||
react = await self.bot.wait_for_reaction(message=self.message, check=self.react_check, timeout=120.0)
|
||||
if react is None:
|
||||
self.paginating = False
|
||||
try:
|
||||
await self.bot.clear_reactions(self.message)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
try:
|
||||
await self.bot.remove_reaction(self.message, react.reaction.emoji, react.user)
|
||||
except:
|
||||
pass # can't remove it so don't bother doing so
|
||||
|
||||
await self.match()
|
|
@ -1,191 +0,0 @@
|
|||
import aiohttp
|
||||
from io import BytesIO
|
||||
import inspect
|
||||
|
||||
from . import config
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def convert_to_jpeg(pfile):
|
||||
# Open the file given
|
||||
img = Image.open(pfile)
|
||||
# Create the BytesIO object we'll use as our new "file"
|
||||
new_file = BytesIO()
|
||||
# Save to this file as jpeg
|
||||
img.save(new_file, format='JPEG')
|
||||
# In order to use the file, we need to seek back to the 0th position
|
||||
new_file.seek(0)
|
||||
return new_file
|
||||
|
||||
|
||||
def get_all_commands(bot):
|
||||
"""Returns a list of all command names for the bot"""
|
||||
# First lets create a set of all the parent names
|
||||
parent_command_names = set(cmd.qualified_name for cmd in bot.commands.values())
|
||||
all_commands = []
|
||||
|
||||
# Now lets loop through and get all the child commands for each command
|
||||
# Only the command itself will be yielded if there are no children
|
||||
for cmd_name in parent_command_names:
|
||||
cmd = bot.commands.get(cmd_name)
|
||||
for child_cmd in get_subcommands(cmd):
|
||||
all_commands.append(child_cmd)
|
||||
|
||||
return all_commands
|
||||
|
||||
|
||||
def get_subcommands(command):
|
||||
yield command.qualified_name
|
||||
try:
|
||||
non_aliases = set(cmd.name for cmd in command.commands.values())
|
||||
for cmd_name in non_aliases:
|
||||
yield from get_subcommands(command.commands[cmd_name])
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
async def channel_is_nsfw(channel):
|
||||
server = channel.server.id
|
||||
channel = channel.id
|
||||
|
||||
server_settings = await config.get_content('server_settings', server)
|
||||
|
||||
try:
|
||||
return channel in server_settings['nsfw_channels']
|
||||
except (TypeError, IndexError, KeyError):
|
||||
return False
|
||||
|
||||
|
||||
def find_command(bot, command):
|
||||
"""Finds a command (be it parent or sub command) based on string given"""
|
||||
# This method ensures the command given is valid. We need to loop through commands
|
||||
# As bot.commands only includes parent commands
|
||||
# So we are splitting the command in parts, looping through the commands
|
||||
# And getting the subcommand based on the next part
|
||||
# If we try to access commands of a command that isn't a group
|
||||
# We'll hit an AttributeError, meaning an invalid command was given
|
||||
# If we loop through and don't find anything, cmd will still be None
|
||||
# And we'll report an invalid was given as well
|
||||
cmd = None
|
||||
|
||||
for part in command.split():
|
||||
try:
|
||||
if cmd is None:
|
||||
cmd = bot.commands.get(part)
|
||||
else:
|
||||
cmd = cmd.commands.get(part)
|
||||
except AttributeError:
|
||||
cmd = None
|
||||
break
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
async def download_image(url):
|
||||
"""Returns a file-like object based on the URL provided"""
|
||||
headers = {'User-Agent': config.user_agent}
|
||||
# Simply read the image, to get the bytes
|
||||
bts = await request(url, attr='read')
|
||||
if bts is None:
|
||||
return None
|
||||
|
||||
# Then wrap it in a BytesIO object, to be used like an actual file
|
||||
image = BytesIO(bts)
|
||||
return image
|
||||
|
||||
|
||||
async def request(url, *, headers=None, payload=None, method='GET', attr='json'):
|
||||
# Make sure our User Agent is what's set, and ensure it's sent even if no headers are passed
|
||||
if headers == None:
|
||||
headers = {}
|
||||
headers['User-Agent'] = config.user_agent
|
||||
|
||||
# Try 5 times
|
||||
for i in range(5):
|
||||
try:
|
||||
# Create the session with our headeres
|
||||
with aiohttp.ClientSession(headers=headers) as session:
|
||||
# Make the request, based on the method, url, and paramaters given
|
||||
async with session.request(method, url, params=payload) as response:
|
||||
# If the request wasn't successful, re-attempt
|
||||
if response.status != 200:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Get the attribute requested
|
||||
return_value = getattr(response, attr)
|
||||
# Next check if this can be called
|
||||
if callable(return_value):
|
||||
return_value = return_value()
|
||||
# If this is awaitable, await it
|
||||
if inspect.isawaitable(return_value):
|
||||
return_value = await return_value
|
||||
|
||||
# Then return it
|
||||
return return_value
|
||||
except AttributeError:
|
||||
# If an invalid attribute was requested, return None
|
||||
return None
|
||||
# If an error was hit other than the one we want to catch, try again
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
async def update_records(key, winner, loser):
|
||||
# We're using the Harkness scale to rate
|
||||
# http://opnetchessclub.wikidot.com/harkness-rating-system
|
||||
r_filter = lambda row: (row['member_id'] == str(winner.id)) | (row['member_id'] == str(loser.id))
|
||||
matches = await config.filter_content(key, r_filter)
|
||||
|
||||
winner_stats = {}
|
||||
loser_stats = {}
|
||||
try:
|
||||
for stat in matches:
|
||||
if stat.get('member_id') == winner.id:
|
||||
winner_stats = stat
|
||||
elif stat.get('member_id') == loser.id:
|
||||
loser_stats = stat
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
winner_rating = winner_stats.get('rating') or 1000
|
||||
loser_rating = loser_stats.get('rating') or 1000
|
||||
|
||||
# The scale is based off of increments of 25, increasing the change by 1 for each increment
|
||||
# That is all this loop does, increment the "change" for every increment of 25
|
||||
# The change caps off at 300 however, so break once we are over that limit
|
||||
difference = abs(winner_rating - loser_rating)
|
||||
rating_change = 0
|
||||
count = 25
|
||||
while count <= difference:
|
||||
if count > 300:
|
||||
break
|
||||
rating_change += 1
|
||||
count += 25
|
||||
|
||||
# 16 is the base change, increased or decreased based on whoever has the higher current rating
|
||||
if winner_rating > loser_rating:
|
||||
winner_rating += 16 - rating_change
|
||||
loser_rating -= 16 - rating_change
|
||||
else:
|
||||
winner_rating += 16 + rating_change
|
||||
loser_rating -= 16 + rating_change
|
||||
|
||||
# Just increase wins/losses for each person, making sure it's at least 0
|
||||
winner_wins = winner_stats.get('wins', 0)
|
||||
winner_losses = winner_stats.get('losses', 0)
|
||||
loser_wins = loser_stats.get('wins', 0)
|
||||
loser_losses = loser_stats.get('losses', 0)
|
||||
winner_wins += 1
|
||||
loser_losses += 1
|
||||
|
||||
# Now save the new wins, losses, and ratings
|
||||
winner_stats = {'wins': winner_wins, 'losses': winner_losses, 'rating': winner_rating}
|
||||
loser_stats = {'wins': loser_wins, 'losses': loser_losses, 'rating': loser_rating}
|
||||
|
||||
if not await config.update_content(key, winner_stats, winner.id):
|
||||
winner_stats['member_id'] = winner.id
|
||||
await config.add_content(key, winner_stats)
|
||||
if not await config.update_content(key, loser_stats, loser.id):
|
||||
loser_stats['member_id'] = loser.id
|
||||
await config.add_content(key, loser_stats)
|
|
@ -1,3 +0,0 @@
|
|||
from .downloader import Downloader
|
||||
from .playlist import Playlist
|
||||
from .exceptions import *
|
|
@ -1,85 +0,0 @@
|
|||
import os
|
||||
import asyncio
|
||||
import functools
|
||||
import youtube_dl
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
ytdl_format_options = {
|
||||
'format': 'bestaudio/best',
|
||||
'extractaudio': True,
|
||||
'audioformat': 'mp3',
|
||||
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
|
||||
'restrictfilenames': True,
|
||||
'noplaylist': True,
|
||||
'nocheckcertificate': True,
|
||||
'ignoreerrors': False,
|
||||
'logtostderr': False,
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'default_search': 'auto',
|
||||
'source_address': '0.0.0.0'
|
||||
}
|
||||
|
||||
# Fuck your useless bugreports message that gets two link embeds and confuses users
|
||||
youtube_dl.utils.bug_reports_message = lambda: ''
|
||||
|
||||
'''
|
||||
Alright, here's the problem. To catch youtube-dl errors for their useful information, I have to
|
||||
catch the exceptions with `ignoreerrors` off. To not break when ytdl hits a dumb video
|
||||
(rental videos, etc), I have to have `ignoreerrors` on. I can change these whenever, but with async
|
||||
that's bad. So I need multiple ytdl objects.
|
||||
|
||||
'''
|
||||
|
||||
class Downloader:
|
||||
def __init__(self, download_folder=None):
|
||||
self.thread_pool = ThreadPoolExecutor(max_workers=2)
|
||||
self.unsafe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
||||
self.safe_ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
|
||||
self.safe_ytdl.params['ignoreerrors'] = True
|
||||
self.download_folder = download_folder
|
||||
|
||||
if download_folder:
|
||||
otmpl = self.unsafe_ytdl.params['outtmpl']
|
||||
self.unsafe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
|
||||
# print("setting template to " + os.path.join(download_folder, otmpl))
|
||||
|
||||
otmpl = self.safe_ytdl.params['outtmpl']
|
||||
self.safe_ytdl.params['outtmpl'] = os.path.join(download_folder, otmpl)
|
||||
|
||||
|
||||
@property
|
||||
def ytdl(self):
|
||||
return self.safe_ytdl
|
||||
|
||||
async def extract_info(self, loop, *args, on_error=None, retry_on_error=False, **kwargs):
|
||||
"""
|
||||
Runs ytdl.extract_info within the threadpool. Returns a future that will fire when it's done.
|
||||
If `on_error` is passed and an exception is raised, the exception will be caught and passed to
|
||||
on_error as an argument.
|
||||
"""
|
||||
if callable(on_error):
|
||||
try:
|
||||
return await loop.run_in_executor(self.thread_pool, functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# (youtube_dl.utils.ExtractorError, youtube_dl.utils.DownloadError)
|
||||
# I hope I don't have to deal with ContentTooShortError's
|
||||
if asyncio.iscoroutinefunction(on_error):
|
||||
asyncio.ensure_future(on_error(e), loop=loop)
|
||||
|
||||
elif asyncio.iscoroutine(on_error):
|
||||
asyncio.ensure_future(on_error, loop=loop)
|
||||
|
||||
else:
|
||||
loop.call_soon_threadsafe(on_error, e)
|
||||
|
||||
if retry_on_error:
|
||||
return await self.safe_extract_info(loop, *args, **kwargs)
|
||||
else:
|
||||
return await loop.run_in_executor(self.thread_pool, functools.partial(self.unsafe_ytdl.extract_info, *args, **kwargs))
|
||||
|
||||
async def safe_extract_info(self, loop, *args, **kwargs):
|
||||
return await loop.run_in_executor(self.thread_pool, functools.partial(self.safe_ytdl.extract_info, *args, **kwargs))
|
|
@ -1,286 +0,0 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
import discord
|
||||
|
||||
from hashlib import md5
|
||||
from .exceptions import ExtractionError
|
||||
|
||||
async def get_header(session, url, headerfield=None, *, timeout=5):
|
||||
with aiohttp.Timeout(timeout):
|
||||
async with session.head(url) as response:
|
||||
if headerfield:
|
||||
return response.headers.get(headerfield)
|
||||
else:
|
||||
return response.headers
|
||||
|
||||
def md5sum(filename, limit=0):
|
||||
fhash = md5()
|
||||
with open(filename, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
fhash.update(chunk)
|
||||
return fhash.hexdigest()[-limit:]
|
||||
|
||||
class BasePlaylistEntry:
|
||||
def __init__(self):
|
||||
self.filename = None
|
||||
self._is_downloading = False
|
||||
self._waiting_futures = []
|
||||
|
||||
@property
|
||||
def is_downloaded(self):
|
||||
if self._is_downloading:
|
||||
return False
|
||||
|
||||
return bool(self.filename)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, playlist, jsonstring):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
async def _download(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ready_future(self):
|
||||
"""
|
||||
Returns a future that will fire when the song is ready to be played. The future will either fire with the result (being the entry) or an exception
|
||||
as to why the song download failed.
|
||||
"""
|
||||
future = asyncio.Future()
|
||||
if self.is_downloaded:
|
||||
# In the event that we're downloaded, we're already ready for playback.
|
||||
future.set_result(self)
|
||||
|
||||
else:
|
||||
# If we request a ready future, let's ensure that it'll actually resolve at one point.
|
||||
asyncio.ensure_future(self._download())
|
||||
self._waiting_futures.append(future)
|
||||
|
||||
return future
|
||||
|
||||
def _for_each_future(self, cb):
|
||||
"""
|
||||
Calls `cb` for each future that is not cancelled. Absorbs and logs any errors that may have occurred.
|
||||
"""
|
||||
futures = self._waiting_futures
|
||||
self._waiting_futures = []
|
||||
|
||||
for future in futures:
|
||||
if future.cancelled():
|
||||
continue
|
||||
|
||||
try:
|
||||
cb(future)
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
|
||||
class URLPlaylistEntry(BasePlaylistEntry):
|
||||
def __init__(self, playlist, url, title, requester, duration=0, expected_filename=None, **meta):
|
||||
super().__init__()
|
||||
|
||||
self.playlist = playlist
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.duration = duration
|
||||
self.expected_filename = expected_filename
|
||||
self.meta = meta
|
||||
self.requester = requester
|
||||
self.download_folder = self.playlist.downloader.download_folder
|
||||
|
||||
def __str__(self):
|
||||
fmt = '*{0}* requested by **{1.display_name}**'
|
||||
if self.duration:
|
||||
fmt += ' [length: {0[0]}m {0[1]}s]'.format(divmod(round(self.duration, 0), 60))
|
||||
return fmt.format(self.title, self.requester)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
if self.duration:
|
||||
return self.duration
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
if self.start_time:
|
||||
return round(time.time() - self.start_time)
|
||||
|
||||
@property
|
||||
def remaining(self):
|
||||
length = self.length
|
||||
progress = self.progress
|
||||
if length and progress:
|
||||
return length - progress
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, playlist, jsonstring):
|
||||
data = json.loads(jsonstring)
|
||||
print(data)
|
||||
# TODO: version check
|
||||
url = data['url']
|
||||
title = data['title']
|
||||
duration = data['duration']
|
||||
downloaded = data['downloaded']
|
||||
filename = data['filename'] if downloaded else None
|
||||
meta = {}
|
||||
|
||||
# TODO: Better [name] fallbacks
|
||||
if 'channel' in data['meta']:
|
||||
ch = playlist.bot.get_channel(data['meta']['channel']['id'])
|
||||
meta['channel'] = ch or data['meta']['channel']['name']
|
||||
|
||||
if 'author' in data['meta']:
|
||||
meta['author'] = meta['channel'].server.get_member(data['meta']['author']['id'])
|
||||
|
||||
return cls(playlist, url, title, duration, filename, **meta)
|
||||
|
||||
def to_json(self):
|
||||
data = {
|
||||
'version': 1,
|
||||
'type': self.__class__.__name__,
|
||||
'url': self.url,
|
||||
'title': self.title,
|
||||
'duration': self.duration,
|
||||
'downloaded': self.is_downloaded,
|
||||
'filename': self.filename,
|
||||
'meta': {
|
||||
i: {
|
||||
'type': self.meta[i].__class__.__name__,
|
||||
'id': self.meta[i].id,
|
||||
'name': self.meta[i].name
|
||||
} for i in self.meta
|
||||
}
|
||||
# Actually I think I can just getattr instead, getattr(discord, type)
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
async def _download(self):
|
||||
if self._is_downloading:
|
||||
return
|
||||
|
||||
self._is_downloading = True
|
||||
try:
|
||||
# Ensure the folder that we're going to move into exists.
|
||||
if not os.path.exists(self.download_folder):
|
||||
os.makedirs(self.download_folder)
|
||||
|
||||
# self.expected_filename: audio_cache\youtube-9R8aSKwTEMg-NOMA_-_Brain_Power.m4a
|
||||
extractor = os.path.basename(self.expected_filename).split('-')[0]
|
||||
|
||||
# the generic extractor requires special handling
|
||||
if extractor == 'generic':
|
||||
# print("Handling generic")
|
||||
flistdir = [f.rsplit('-', 1)[0] for f in os.listdir(self.download_folder)]
|
||||
expected_fname_noex, fname_ex = os.path.basename(self.expected_filename).rsplit('.', 1)
|
||||
|
||||
if expected_fname_noex in flistdir:
|
||||
try:
|
||||
rsize = int(await get_header(self.playlist.bot.aiosession, self.url, 'CONTENT-LENGTH'))
|
||||
except:
|
||||
rsize = 0
|
||||
|
||||
lfile = os.path.join(
|
||||
self.download_folder,
|
||||
os.listdir(self.download_folder)[flistdir.index(expected_fname_noex)]
|
||||
)
|
||||
|
||||
# print("Resolved %s to %s" % (self.expected_filename, lfile))
|
||||
lsize = os.path.getsize(lfile)
|
||||
# print("Remote size: %s Local size: %s" % (rsize, lsize))
|
||||
|
||||
if lsize != rsize:
|
||||
await self._really_download(hash=True)
|
||||
else:
|
||||
# print("[Download] Cached:", self.url)
|
||||
self.filename = lfile
|
||||
|
||||
else:
|
||||
# print("File not found in cache (%s)" % expected_fname_noex)
|
||||
await self._really_download(hash=True)
|
||||
|
||||
else:
|
||||
ldir = os.listdir(self.download_folder)
|
||||
flistdir = [f.rsplit('.', 1)[0] for f in ldir]
|
||||
expected_fname_base = os.path.basename(self.expected_filename)
|
||||
expected_fname_noex = expected_fname_base.rsplit('.', 1)[0]
|
||||
|
||||
# idk wtf this is but its probably legacy code
|
||||
# or i have youtube to blame for changing shit again
|
||||
|
||||
if expected_fname_base in ldir:
|
||||
self.filename = os.path.join(self.download_folder, expected_fname_base)
|
||||
print("[Download] Cached:", self.url)
|
||||
|
||||
elif expected_fname_noex in flistdir:
|
||||
print("[Download] Cached (different extension):", self.url)
|
||||
self.filename = os.path.join(self.download_folder, ldir[flistdir.index(expected_fname_noex)])
|
||||
print("Expected %s, got %s" % (
|
||||
self.expected_filename.rsplit('.', 1)[-1],
|
||||
self.filename.rsplit('.', 1)[-1]
|
||||
))
|
||||
|
||||
else:
|
||||
await self._really_download()
|
||||
|
||||
# Trigger ready callbacks.
|
||||
self._for_each_future(lambda future: future.set_result(self))
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self._for_each_future(lambda future: future.set_exception(e))
|
||||
|
||||
finally:
|
||||
self._is_downloading = False
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
async def _really_download(self, *, hash=False):
|
||||
print("[Download] Started:", self.url)
|
||||
|
||||
try:
|
||||
result = await self.playlist.downloader.extract_info(self.playlist.loop, self.url, download=True)
|
||||
except Exception as e:
|
||||
raise ExtractionError(e)
|
||||
|
||||
print("[Download] Complete:", self.url)
|
||||
|
||||
if result is None:
|
||||
raise ExtractionError("ytdl broke and hell if I know why")
|
||||
# What the fuck do I do now?
|
||||
|
||||
self.filename = unhashed_fname = self.playlist.downloader.ytdl.prepare_filename(result)
|
||||
|
||||
if hash:
|
||||
# insert the 8 last characters of the file hash to the file name to ensure uniqueness
|
||||
self.filename = md5sum(unhashed_fname, 8).join('-.').join(unhashed_fname.rsplit('.', 1))
|
||||
|
||||
if os.path.isfile(self.filename):
|
||||
# Oh bother it was actually there.
|
||||
os.unlink(unhashed_fname)
|
||||
else:
|
||||
# Move the temporary file to it's final location.
|
||||
os.rename(unhashed_fname, self.filename)
|
||||
def to_embed(self):
|
||||
"""Returns an embed that can be used to display information about this particular song"""
|
||||
# Create the embed object we'll use
|
||||
embed = discord.Embed()
|
||||
# Fill in the simple things
|
||||
embed.add_field(name='Title', value=self.title, inline=False)
|
||||
embed.add_field(name='Requester', value=self.requester.display_name, inline=False)
|
||||
# Get the current length of the song and display this
|
||||
length = divmod(round(self.length, 0), 60)
|
||||
fmt = "{0[0]}m {0[1]}s".format(length)
|
||||
embed.add_field(name='Duration', value=fmt,inline=False)
|
||||
# And return the embed we created
|
||||
return embed
|
|
@ -1,38 +0,0 @@
|
|||
import asyncio
|
||||
import traceback
|
||||
import collections
|
||||
|
||||
|
||||
class EventEmitter:
|
||||
def __init__(self):
|
||||
self._events = collections.defaultdict(list)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
def emit(self, event, *args, **kwargs):
|
||||
if event not in self._events:
|
||||
return
|
||||
|
||||
for cb in self._events[event]:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(cb):
|
||||
asyncio.ensure_future(cb(*args, **kwargs), loop=self.loop)
|
||||
else:
|
||||
cb(*args, **kwargs)
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def on(self, event, cb):
|
||||
self._events[event].append(cb)
|
||||
return self
|
||||
|
||||
def off(self, event, cb):
|
||||
self._events[event].remove(cb)
|
||||
|
||||
if not self._events[event]:
|
||||
del self._events[event]
|
||||
|
||||
return self
|
||||
|
||||
# TODO: add .once
|
|
@ -1,88 +0,0 @@
|
|||
import shutil
|
||||
import textwrap
|
||||
|
||||
# Base class for exceptions
|
||||
class MusicbotException(Exception):
|
||||
def __init__(self, message, *, expire_in=0):
|
||||
self._message = message
|
||||
self.expire_in = expire_in
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return self._message
|
||||
|
||||
@property
|
||||
def message_no_format(self):
|
||||
return self._message
|
||||
|
||||
# Something went wrong during the processing of a command
|
||||
class CommandError(MusicbotException):
|
||||
pass
|
||||
|
||||
# Something went wrong during the processing of a song/ytdl stuff
|
||||
class ExtractionError(MusicbotException):
|
||||
pass
|
||||
|
||||
# The no processing entry type failed and an entry was a playlist/vice versa
|
||||
class WrongEntryTypeError(ExtractionError):
|
||||
def __init__(self, message, is_playlist, use_url):
|
||||
super().__init__(message)
|
||||
self.is_playlist = is_playlist
|
||||
self.use_url = use_url
|
||||
|
||||
# The user doesn't have permission to use a command
|
||||
class PermissionsError(CommandError):
|
||||
@property
|
||||
def message(self):
|
||||
return "You don't have permission to use that command.\nReason: " + self._message
|
||||
|
||||
# Error with pretty formatting for hand-holding users through various errors
|
||||
class HelpfulError(MusicbotException):
|
||||
def __init__(self, issue, solution, *, preface="An error has occured:\n", expire_in=0):
|
||||
self.issue = issue
|
||||
self.solution = solution
|
||||
self.preface = preface
|
||||
self.expire_in = expire_in
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return ("\n{}\n{}\n{}\n").format(
|
||||
self.preface,
|
||||
self._pretty_wrap(self.issue, " Problem: "),
|
||||
self._pretty_wrap(self.solution, " Solution: "))
|
||||
|
||||
@property
|
||||
def message_no_format(self):
|
||||
return "\n{}\n{}\n{}\n".format(
|
||||
self.preface,
|
||||
self._pretty_wrap(self.issue, " Problem: ", width=None),
|
||||
self._pretty_wrap(self.solution, " Solution: ", width=None))
|
||||
|
||||
@staticmethod
|
||||
def _pretty_wrap(text, pretext, *, width=-1):
|
||||
if width is None:
|
||||
return pretext + text
|
||||
elif width == -1:
|
||||
width = shutil.get_terminal_size().columns
|
||||
|
||||
l1, *lx = textwrap.wrap(text, width=width - 1 - len(pretext))
|
||||
|
||||
lx = [((' ' * len(pretext)) + l).rstrip().ljust(width) for l in lx]
|
||||
l1 = (pretext + l1).ljust(width)
|
||||
|
||||
return ''.join([l1, *lx])
|
||||
|
||||
class HelpfulWarning(HelpfulError):
|
||||
pass
|
||||
|
||||
# Base class for control signals
|
||||
class Signal(Exception):
|
||||
pass
|
||||
|
||||
# signal to restart the bot
|
||||
class RestartSignal(Signal):
|
||||
pass
|
||||
|
||||
# signal to end the bot "gracefully"
|
||||
class TerminateSignal(Signal):
|
||||
pass
|
|
@ -1,280 +0,0 @@
|
|||
import datetime
|
||||
import traceback
|
||||
from collections import deque
|
||||
from itertools import islice
|
||||
from random import shuffle
|
||||
|
||||
from .entry import URLPlaylistEntry
|
||||
from .exceptions import ExtractionError, WrongEntryTypeError
|
||||
from .event_emitter import EventEmitter
|
||||
|
||||
|
||||
class Playlist(EventEmitter):
|
||||
"""
|
||||
A playlist is manages the list of songs that will be played.
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.bot = bot
|
||||
self.loop = bot.loop
|
||||
self.downloader = bot.downloader
|
||||
self.entries = deque()
|
||||
self.max_songs = 10
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.entries)
|
||||
|
||||
def shuffle(self):
|
||||
shuffle(self.entries)
|
||||
|
||||
def clear(self):
|
||||
self.entries.clear()
|
||||
|
||||
@property
|
||||
def full(self):
|
||||
return self.count >= self.max_songs
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
if self.entries:
|
||||
return len(self.entries)
|
||||
else:
|
||||
return 0
|
||||
|
||||
async def add_entry(self, song_url, requester, **meta):
|
||||
"""
|
||||
Validates and adds a song_url to be played. This does not start the download of the song.
|
||||
|
||||
Returns the entry & the position it is in the queue.
|
||||
|
||||
:param song_url: The song url to add to the playlist.
|
||||
:param meta: Any additional metadata to add to the playlist entry.
|
||||
"""
|
||||
|
||||
try:
|
||||
info = await self.downloader.extract_info(self.loop, song_url, download=False)
|
||||
except Exception as e:
|
||||
raise ExtractionError('Could not extract information from {}\n\n{}'.format(song_url, e))
|
||||
|
||||
if not info:
|
||||
raise ExtractionError('Could not extract information from %s' % song_url)
|
||||
|
||||
# TODO: Sort out what happens next when this happens
|
||||
if info.get('_type', None) == 'playlist':
|
||||
raise WrongEntryTypeError("This is a playlist.", True, info.get('webpage_url', None) or info.get('url', None))
|
||||
|
||||
if info['extractor'] in ['generic', 'Dropbox']:
|
||||
try:
|
||||
# unfortunately this is literally broken
|
||||
# https://github.com/KeepSafe/aiohttp/issues/758
|
||||
# https://github.com/KeepSafe/aiohttp/issues/852
|
||||
content_type = await get_header(self.bot.aiosession, info['url'], 'CONTENT-TYPE')
|
||||
print("Got content type", content_type)
|
||||
|
||||
except Exception as e:
|
||||
print("[Warning] Failed to get content type for url %s (%s)" % (song_url, e))
|
||||
content_type = None
|
||||
|
||||
if content_type:
|
||||
if content_type.startswith(('application/', 'image/')):
|
||||
if '/ogg' not in content_type: # How does a server say `application/ogg` what the actual fuck
|
||||
raise ExtractionError("Invalid content type \"%s\" for url %s" % (content_type, song_url))
|
||||
|
||||
elif not content_type.startswith(('audio/', 'video/')):
|
||||
print("[Warning] Questionable content type \"%s\" for url %s" % (content_type, song_url))
|
||||
|
||||
entry = URLPlaylistEntry(
|
||||
self,
|
||||
song_url,
|
||||
info.get('title', 'Untitled'),
|
||||
requester,
|
||||
info.get('duration', 0) or 0,
|
||||
self.downloader.ytdl.prepare_filename(info),
|
||||
**meta
|
||||
)
|
||||
self._add_entry(entry)
|
||||
return entry, len(self.entries)
|
||||
|
||||
async def import_from(self, playlist_url, **meta):
|
||||
"""
|
||||
Imports the songs from `playlist_url` and queues them to be played.
|
||||
|
||||
Returns a list of `entries` that have been enqueued.
|
||||
|
||||
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
|
||||
:param meta: Any additional metadata to add to the playlist entry
|
||||
"""
|
||||
position = len(self.entries) + 1
|
||||
entry_list = []
|
||||
|
||||
try:
|
||||
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False)
|
||||
except Exception as e:
|
||||
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
|
||||
|
||||
if not info:
|
||||
raise ExtractionError('Could not extract information from %s' % playlist_url)
|
||||
|
||||
# Once again, the generic extractor fucks things up.
|
||||
if info.get('extractor', None) == 'generic':
|
||||
url_field = 'url'
|
||||
else:
|
||||
url_field = 'webpage_url'
|
||||
|
||||
baditems = 0
|
||||
for items in info['entries']:
|
||||
if items:
|
||||
try:
|
||||
entry = URLPlaylistEntry(
|
||||
self,
|
||||
items[url_field],
|
||||
items.get('title', 'Untitled'),
|
||||
items.get('duration', 0) or 0,
|
||||
self.downloader.ytdl.prepare_filename(items),
|
||||
**meta
|
||||
)
|
||||
|
||||
self._add_entry(entry)
|
||||
entry_list.append(entry)
|
||||
except:
|
||||
baditems += 1
|
||||
# Once I know more about what's happening here I can add a proper message
|
||||
traceback.print_exc()
|
||||
print(items)
|
||||
print("Could not add item")
|
||||
else:
|
||||
baditems += 1
|
||||
|
||||
if baditems:
|
||||
print("Skipped %s bad entries" % baditems)
|
||||
|
||||
return entry_list, position
|
||||
|
||||
async def async_process_youtube_playlist(self, playlist_url, **meta):
|
||||
"""
|
||||
Processes youtube playlists links from `playlist_url` in a questionable, async fashion.
|
||||
|
||||
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
|
||||
:param meta: Any additional metadata to add to the playlist entry
|
||||
"""
|
||||
|
||||
try:
|
||||
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
|
||||
except Exception as e:
|
||||
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
|
||||
|
||||
if not info:
|
||||
raise ExtractionError('Could not extract information from %s' % playlist_url)
|
||||
|
||||
gooditems = []
|
||||
baditems = 0
|
||||
for entry_data in info['entries']:
|
||||
if entry_data:
|
||||
baseurl = info['webpage_url'].split('playlist?list=')[0]
|
||||
song_url = baseurl + 'watch?v=%s' % entry_data['id']
|
||||
|
||||
try:
|
||||
entry, elen = await self.add_entry(song_url, **meta)
|
||||
gooditems.append(entry)
|
||||
except ExtractionError:
|
||||
baditems += 1
|
||||
except Exception as e:
|
||||
baditems += 1
|
||||
print("There was an error adding the song {}: {}: {}\n".format(
|
||||
entry_data['id'], e.__class__.__name__, e))
|
||||
else:
|
||||
baditems += 1
|
||||
|
||||
if baditems:
|
||||
print("Skipped %s bad entries" % baditems)
|
||||
|
||||
return gooditems
|
||||
|
||||
async def async_process_sc_bc_playlist(self, playlist_url, **meta):
|
||||
"""
|
||||
Processes soundcloud set and bancdamp album links from `playlist_url` in a questionable, async fashion.
|
||||
|
||||
:param playlist_url: The playlist url to be cut into individual urls and added to the playlist
|
||||
:param meta: Any additional metadata to add to the playlist entry
|
||||
"""
|
||||
|
||||
try:
|
||||
info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
|
||||
except Exception as e:
|
||||
raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))
|
||||
|
||||
if not info:
|
||||
raise ExtractionError('Could not extract information from %s' % playlist_url)
|
||||
|
||||
gooditems = []
|
||||
baditems = 0
|
||||
for entry_data in info['entries']:
|
||||
if entry_data:
|
||||
song_url = entry_data['url']
|
||||
|
||||
try:
|
||||
entry, elen = await self.add_entry(song_url, **meta)
|
||||
gooditems.append(entry)
|
||||
except ExtractionError:
|
||||
baditems += 1
|
||||
except Exception as e:
|
||||
baditems += 1
|
||||
print("There was an error adding the song {}: {}: {}\n".format(
|
||||
entry_data['id'], e.__class__.__name__, e))
|
||||
else:
|
||||
baditems += 1
|
||||
|
||||
if baditems:
|
||||
print("Skipped %s bad entries" % baditems)
|
||||
|
||||
return gooditems
|
||||
|
||||
def _add_entry(self, entry):
|
||||
self.entries.append(entry)
|
||||
self.emit('entry-added', playlist=self, entry=entry)
|
||||
|
||||
if self.peek() is entry:
|
||||
entry.get_ready_future()
|
||||
|
||||
async def get_next_entry(self, predownload_next=True):
|
||||
"""
|
||||
A coroutine which will return the next song or None if no songs left to play.
|
||||
|
||||
Additionally, if predownload_next is set to True, it will attempt to download the next
|
||||
song to be played - so that it's ready by the time we get to it.
|
||||
"""
|
||||
if not self.entries:
|
||||
return None
|
||||
|
||||
entry = self.entries.popleft()
|
||||
|
||||
if predownload_next:
|
||||
next_entry = self.peek()
|
||||
if next_entry:
|
||||
next_entry.get_ready_future()
|
||||
|
||||
return await entry.get_ready_future()
|
||||
|
||||
def peek(self):
|
||||
"""
|
||||
Returns the next entry that should be scheduled to be played.
|
||||
"""
|
||||
if self.entries:
|
||||
return self.entries[0]
|
||||
|
||||
async def estimate_time_until(self, position, player):
|
||||
"""
|
||||
(very) Roughly estimates the time till the queue will 'position'
|
||||
"""
|
||||
estimated_time = sum([e.duration for e in islice(self.entries, position - 1)])
|
||||
|
||||
# When the player plays a song, it eats the first playlist item, so we just have to add the time back
|
||||
if not player.is_stopped and player.current_entry:
|
||||
estimated_time += player.current_entry.duration - player.progress
|
||||
|
||||
return datetime.timedelta(seconds=estimated_time)
|
||||
|
||||
def count_for_user(self, user):
|
||||
return sum(1 for e in self.entries if e.meta.get('author', None) == user)
|
||||
|
|
@ -1,24 +1,17 @@
|
|||
bot_token: 'token'
|
||||
owner_id: ['12345']
|
||||
description: 'Bot Description Here'
|
||||
command_prefix: '!'
|
||||
default_status: '!help for a list of commands'
|
||||
discord_bots_key: 'key'
|
||||
carbon_key: 'key'
|
||||
twitch_key: 'key'
|
||||
description: Description
|
||||
command_prefix: ['?', '!']
|
||||
default_status: 'Status'
|
||||
dev_server: 'https://discord.gg/f6uzJEj'
|
||||
user_agent: 'Bonfire/4.0.3 (https://github.com/Phxntxm/Bonfire)'
|
||||
|
||||
youtube_key: 'key'
|
||||
osu_key: 'key'
|
||||
dev_server: 'https://discord.gg/123456'
|
||||
patreon_key: 'key'
|
||||
patreon_id: 'id'
|
||||
patreon_link: 'link'
|
||||
spotify_id: 'id'
|
||||
spotify_secret: 'secret'
|
||||
|
||||
user_agent: 'User-Agent/1.0.0 (Comment like link to site)'
|
||||
extensions: [cogs.cog1, cogs.cog2]
|
||||
|
||||
shard_count: 1
|
||||
shard_id: 0
|
||||
|
||||
db_host: 'localhost'
|
||||
db_name: 'Bot_Name'
|
||||
db_cert: 'cert.pem'
|
||||
db_port: 28015
|
||||
db_user: 'admin'
|
||||
db_pass: 'password'
|
||||
db_name: 'bonfire'
|
||||
db_user: 'bonfire'
|
|
@ -343,21 +343,21 @@ Moderator Utilities
|
|||
Sets custom permissions for a provided command. Format must be 'perms add <command> <permission>'
|
||||
If you want to open the command to everyone, provide 'none' as the permission
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
- Aliases `setup, create`
|
||||
|
||||
.. data:: perms remove
|
||||
|
||||
Removes the custom permissions setup on a command
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
- Aliases `delete`
|
||||
|
||||
.. data:: prefix
|
||||
|
||||
Used to setup a custom prefix for this server
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
|
||||
.. data:: purge
|
||||
|
||||
|
@ -384,14 +384,14 @@ Moderator Utilities
|
|||
|
||||
Adds the specified rule to the list of server's rules.
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
- Aliases `rules create, rule create, rule add`
|
||||
|
||||
.. data:: rules remove
|
||||
|
||||
Deletes a specified rule from the server; the rule deleted needs to be specified by the number.
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
- Aliases `rules delete, rule delete, rules remove`
|
||||
|
||||
Stats
|
||||
|
@ -468,7 +468,7 @@ Blackjack
|
|||
That is why this is restricted to someone who can manage the server, as it should only be used in case
|
||||
people have gone afk and the game is still running, which can get annoying.
|
||||
|
||||
- Default permissions required: manage_server
|
||||
- Default permissions required: manage_guild
|
||||
|
||||
DeviantArt
|
||||
----------
|
||||
|
|
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 229 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 169 KiB |
Before Width: | Height: | Size: 127 KiB |
BIN
images/snek0.jpg
Before Width: | Height: | Size: 189 KiB |
BIN
images/snek1.jpg
Before Width: | Height: | Size: 459 KiB |
Before Width: | Height: | Size: 393 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 399 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 50 KiB |
BIN
images/snek2.jpg
Before Width: | Height: | Size: 233 KiB |
BIN
images/snek3.jpg
Before Width: | Height: | Size: 15 KiB |
BIN
images/snek4.jpg
Before Width: | Height: | Size: 73 KiB |
BIN
images/snek5.jpg
Before Width: | Height: | Size: 373 KiB |
BIN
images/snek6.jpg
Before Width: | Height: | Size: 227 KiB |
BIN
images/snek7.jpg
Before Width: | Height: | Size: 296 KiB |
BIN
images/snek8.jpg
Before Width: | Height: | Size: 144 KiB |
BIN
images/snek9.jpg
Before Width: | Height: | Size: 91 KiB |
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
pyyaml
|
||||
psutil
|
||||
pendulum
|
||||
beautifulsoup4
|
||||
osuapi
|
||||
asyncpg
|
||||
discord.py
|
6
utils/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from .cards import Deck, Face, Suit
|
||||
from .checks import can_run
|
||||
from .config import *
|
||||
from .utilities import *
|
||||
from .paginator import Pages, CannotPaginate, HelpPaginator
|
||||
from .database import DB, Cache
|
188
utils/cards.py
Normal file
|
@ -0,0 +1,188 @@
|
|||
import random
|
||||
|
||||
from enum import IntEnum, auto
|
||||
|
||||
|
||||
class Suit(IntEnum):
|
||||
clubs = auto()
|
||||
hearts = auto()
|
||||
diamonds = auto()
|
||||
spades = auto()
|
||||
|
||||
|
||||
class Face(IntEnum):
|
||||
ace = auto()
|
||||
two = auto()
|
||||
three = auto()
|
||||
four = auto()
|
||||
five = auto()
|
||||
six = auto()
|
||||
seven = auto()
|
||||
eight = auto()
|
||||
nine = auto()
|
||||
ten = auto()
|
||||
jack = auto()
|
||||
queen = auto()
|
||||
king = auto()
|
||||
|
||||
|
||||
class Deck:
|
||||
def __init__(self, prefill=True, ace_high=True, spades_high=False):
|
||||
self.deck = []
|
||||
if prefill:
|
||||
self.refresh()
|
||||
|
||||
self.ace_high = ace_high
|
||||
self.spades_high = spades_high
|
||||
|
||||
def __iter__(self):
|
||||
for card in self.deck:
|
||||
yield card
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.deck[key]
|
||||
|
||||
def refresh(self):
|
||||
"""A method that 'restarts' the deck, filling it back with 52 cards"""
|
||||
self.deck = []
|
||||
|
||||
for _suit in Suit:
|
||||
for _face in Face:
|
||||
self.insert(Card(_suit, _face, self))
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
"""A property to provide how many cards are currently in the deck"""
|
||||
return len(self.deck)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
"""A property to determine whether or not the deck has cards in it"""
|
||||
return len(self.deck) == 0
|
||||
|
||||
def draw(self, count=1):
|
||||
"""Generator to draw from the deck"""
|
||||
try:
|
||||
for i in range(count):
|
||||
yield self.deck.pop()
|
||||
except IndexError:
|
||||
yield None
|
||||
|
||||
def insert(self, cards):
|
||||
"""Adds the provided cards to the end of the deck"""
|
||||
try:
|
||||
self.deck.extend(cards)
|
||||
for card in cards:
|
||||
card._deck = self
|
||||
except TypeError:
|
||||
self.deck.append(cards)
|
||||
cards._deck = self
|
||||
|
||||
def index(self, card):
|
||||
"""Returns the index of the card provided (-1 if card is not in the deck)"""
|
||||
return self.deck.index(card)
|
||||
|
||||
def pluck(self, index=None, card=None):
|
||||
"""Pulls the provided card from the deck"""
|
||||
if index:
|
||||
return self.deck.pop(index)
|
||||
elif card:
|
||||
return self.deck.pop(self.index(card))
|
||||
|
||||
def shuffle(self):
|
||||
"""Shuffles the deck in place"""
|
||||
random.SystemRandom().shuffle(self.deck)
|
||||
|
||||
def get_card(self, suit, face):
|
||||
"""Returns the provided card in the deck"""
|
||||
for card in self.deck:
|
||||
if card.suit == suit and card.value == face:
|
||||
return card
|
||||
|
||||
|
||||
class Card:
|
||||
"""The class that holds all the details for a card in the deck"""
|
||||
|
||||
def __init__(self, suit, value, deck):
|
||||
self._suit = suit
|
||||
self._value = value
|
||||
self._deck = deck
|
||||
|
||||
@property
|
||||
def suit(self):
|
||||
"""The suit (club, diamond, heart, spade)"""
|
||||
return self._suit
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""The face value (2-10, J, Q, K, A)"""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def face_short(self):
|
||||
"""The first 'letter' of the face, (2-10 will be the numbers)"""
|
||||
if self.value == Face.ace or \
|
||||
self.value == Face.jack or \
|
||||
self.value == Face.queen or \
|
||||
self.value == Face.king:
|
||||
return self.value.name[0]
|
||||
else:
|
||||
return self.value.value
|
||||
|
||||
def __hash__(self):
|
||||
return self.value.value + len(Face) * (self.suit.value - 1)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Card):
|
||||
return False
|
||||
return self.suit == other.suit and self.value == other.value
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Card):
|
||||
return True
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return "%s of %s" % (self.value.name, self.suit.name)
|
||||
|
||||
def __lt__(self, other):
|
||||
if self._deck.spades_high:
|
||||
if other.suit == Suit.spades and self.suit != Suit.spades:
|
||||
return True
|
||||
if self.suit == Suit.spades and other.suit != Suit.spades:
|
||||
return False
|
||||
|
||||
self_value = self.value.value
|
||||
other_value = other.value.value
|
||||
|
||||
if self._deck.ace_high:
|
||||
if self.value == Face.ace:
|
||||
self_value = 14
|
||||
if other.value == Face.ace:
|
||||
other_value = 14
|
||||
|
||||
return self_value < other_value
|
||||
|
||||
def __gt__(self, other):
|
||||
if self._deck.spades_high:
|
||||
if other.suit == Suit.spades and self.suit != Suit.spades:
|
||||
return False
|
||||
if self.suit == Suit.spades and other.suit != Suit.spades:
|
||||
return True
|
||||
|
||||
self_value = self.value.value
|
||||
other_value = other.value.value
|
||||
|
||||
if self._deck.ace_high:
|
||||
if self.value == Face.ace:
|
||||
self_value = 14
|
||||
if other.value == Face.ace:
|
||||
other_value = 14
|
||||
|
||||
return self_value > other_value
|
||||
|
||||
def __le__(self, other):
|
||||
return self.__eq__(other) or self.__lt__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.__eq__(other) or self.__gt__(other)
|
138
utils/checks.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
import asyncio
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
from . import utilities
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
def should_ignore(ctx):
|
||||
if ctx.message.guild is None:
|
||||
return False
|
||||
ignored = ctx.bot.cache.ignored[ctx.guild.id]
|
||||
if not ignored:
|
||||
return False
|
||||
return ctx.message.author.id in ignored['members'] or ctx.message.channel.id in ignored['channels']
|
||||
|
||||
|
||||
async def check_not_restricted(ctx):
|
||||
# Return true if this is a private channel, we'll handle that in the registering of the command
|
||||
if type(ctx.message.channel) is discord.DMChannel:
|
||||
return True
|
||||
|
||||
# First get all the restrictions
|
||||
restrictions = ctx.bot.cache.restrictions[ctx.guild.id]
|
||||
# Now lets check the "from" restrictions
|
||||
for from_restriction in restrictions.get('from', []):
|
||||
# Get the source and destination
|
||||
# Source should ALWAYS be a command in this case
|
||||
source = from_restriction.get('source')
|
||||
destination = from_restriction.get('destination')
|
||||
# Special check for what the "disable" command produces
|
||||
if destination == "everyone" and ctx.command.qualified_name == source:
|
||||
return False
|
||||
# Convert destination to the object we want
|
||||
destination = await utilities.convert(ctx, destination)
|
||||
# If we couldn't find the destination, just continue with other restrictions
|
||||
# Also if this restriction we're checking isn't for this command
|
||||
if destination is None or source != ctx.command.qualified_name:
|
||||
continue
|
||||
|
||||
# This means that the type of restriction we have is `command from channel`
|
||||
# Which means we do not want commands to be ran in this channel
|
||||
if destination == ctx.message.channel:
|
||||
return False
|
||||
# This type is `command from Role` meaning anyone with this role can't run this command
|
||||
elif destination in ctx.message.author.roles:
|
||||
return False
|
||||
# This is `command from Member` meaning this user specifically cannot run this command
|
||||
elif destination == ctx.message.author:
|
||||
return False
|
||||
|
||||
# If we are here, then there are no blacklists stopping this from running
|
||||
|
||||
# Now for the to restrictions this is a little different, we need to make a whitelist and
|
||||
# see if our current channel is in this whitelist, as well as any whitelisted roles are in the author's roles
|
||||
# Only if there is no whitelist, do we want to blanket return True
|
||||
to_restrictions = restrictions.get('to', [])
|
||||
if len(to_restrictions) == 0:
|
||||
return True
|
||||
|
||||
# Otherwise there is a whitelist, and we need to start it
|
||||
whitelisted_channels = []
|
||||
whitelisted_roles = []
|
||||
|
||||
for to_restriction in to_restrictions:
|
||||
# Get the source and destination
|
||||
# Source should ALWAYS be a command in this case
|
||||
source = to_restriction.get('source')
|
||||
destination = to_restriction.get('destination')
|
||||
# Convert destination to the object we want
|
||||
destination = await utilities.convert(ctx, destination)
|
||||
# If we couldn't find the destination, just continue with other restrictions
|
||||
# Also if this restriction we're checking isn't for this command
|
||||
if destination is None or source != ctx.command.qualified_name:
|
||||
continue
|
||||
|
||||
# Append to our two whitelists depending on what type this is
|
||||
if isinstance(destination, discord.TextChannel):
|
||||
whitelisted_channels.append(destination)
|
||||
elif isinstance(destination, discord.Role):
|
||||
whitelisted_roles.append(destination)
|
||||
|
||||
if whitelisted_channels:
|
||||
if ctx.channel not in whitelisted_channels:
|
||||
return False
|
||||
if whitelisted_roles:
|
||||
if not any(x in ctx.message.author.roles for x in whitelisted_roles):
|
||||
return False
|
||||
|
||||
# If we have passed all of these, then we are allowed to run this command
|
||||
# This looks like a whole lot, but all of these lists will be very tiny in almost all cases
|
||||
# And only delving deep into the specific lists that may be large, will we finally see "large" lists
|
||||
# Which means this still will not be slow in other cases
|
||||
return True
|
||||
|
||||
|
||||
def has_perms(ctx, **perms):
|
||||
# Return true if this is a private channel, we'll handle that in the registering of the command
|
||||
if type(ctx.message.channel) is discord.DMChannel:
|
||||
return True
|
||||
|
||||
# Get the member permissions so that we can compare
|
||||
guild_perms = ctx.message.author.guild_permissions
|
||||
channel_perms = ctx.message.author.permissions_in(ctx.message.channel)
|
||||
# Currently the library doesn't handle administrator overrides..so lets do this manually
|
||||
if guild_perms.administrator:
|
||||
return True
|
||||
# Next, set the default permissions if one is not used, based on what was passed
|
||||
# This will be overriden later, if we have custom permissions
|
||||
required_perm = discord.Permissions.none()
|
||||
for perm, setting in perms.items():
|
||||
setattr(required_perm, perm, setting)
|
||||
|
||||
required_perm_value = ctx.bot.cache.custom_permissions[ctx.guild.id].get(ctx.command.qualified_name)
|
||||
if required_perm_value:
|
||||
required_perm = discord.Permissions(required_perm_value)
|
||||
|
||||
# Now just check if the person running the command has these permissions
|
||||
return guild_perms >= required_perm or channel_perms >= required_perm
|
||||
|
||||
|
||||
def can_run(**kwargs):
|
||||
async def predicate(ctx):
|
||||
# Next check if it requires any certain permissions
|
||||
if kwargs and not has_perms(ctx, **kwargs):
|
||||
return False
|
||||
# Next...check custom restrictions
|
||||
if not await check_not_restricted(ctx):
|
||||
return False
|
||||
# Then if the user/channel should be ignored
|
||||
if should_ignore(ctx):
|
||||
return False
|
||||
# Otherwise....we're good
|
||||
return True
|
||||
|
||||
predicate.perms = kwargs
|
||||
return commands.check(predicate)
|
106
utils/config.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import yaml
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
global_config = {}
|
||||
|
||||
# Ensure that the required config.yml file actually exists
|
||||
try:
|
||||
with open("config.yml", "r") as f:
|
||||
global_config = yaml.safe_load(f)
|
||||
global_config = {k: v for k, v in global_config.items() if v}
|
||||
except FileNotFoundError:
|
||||
print("You have no config file setup! Please use config.yml.sample to setup a valid config file")
|
||||
quit()
|
||||
|
||||
try:
|
||||
bot_token = global_config["bot_token"]
|
||||
except KeyError:
|
||||
print("You have no bot_token saved, this is a requirement for running a bot.")
|
||||
print("Please use config.yml.sample to setup a valid config file")
|
||||
quit()
|
||||
|
||||
# Default bot's description
|
||||
bot_description = global_config.get("description")
|
||||
# Bot's default prefix for commands
|
||||
default_prefix = global_config.get("command_prefix", "!")
|
||||
# The key for bot sites (discord bots and discordbots are TWO DIFFERENT THINGS)
|
||||
discord_bots_key = global_config.get('discord_bots_key', "")
|
||||
discordbots_key = global_config.get('discordbots_key', "")
|
||||
carbon_key = global_config.get('carbon_key', "")
|
||||
# The client ID for twitch requests
|
||||
twitch_key = global_config.get('twitch_key', "")
|
||||
# The key for youtube API calls
|
||||
youtube_key = global_config.get("youtube_key", "")
|
||||
# The key for Osu API calls
|
||||
osu_key = global_config.get('osu_key', '')
|
||||
# The key for e621 calls
|
||||
e621_key = global_config.get('e621_key', '')
|
||||
# The username the API key is under
|
||||
e621_user = global_config.get('e621_user', '')
|
||||
# The key for League of Legends API calls
|
||||
lol_key = global_config.get('lol_key', '')
|
||||
# The keys needed for deviant art calls
|
||||
# The invite link for the server made for the bot
|
||||
dev_server = global_config.get("dev_server", "")
|
||||
# The User-Agent that we'll use for most requests
|
||||
user_agent = global_config.get('user_agent', None)
|
||||
# The URL to proxy youtube_dl's requests through
|
||||
ytdl_proxy = global_config.get('youtube_dl_proxy', None)
|
||||
# The patreon key, as well as the patreon ID to use
|
||||
patreon_key = global_config.get('patreon_key', None)
|
||||
patreon_id = global_config.get('patreon_id', None)
|
||||
patreon_link = global_config.get('patreon_link', None)
|
||||
# The client ID/secret for spotify
|
||||
spotify_id = global_config.get("spotify_id", None)
|
||||
spotify_secret = global_config.get("spotify_secret", None)
|
||||
# Error channel to send uncaught exceptions to
|
||||
error_channel = global_config.get("error_channel", None)
|
||||
|
||||
# The extensions to load
|
||||
extensions = [
|
||||
'cogs.interaction',
|
||||
'cogs.misc',
|
||||
'cogs.mod',
|
||||
'cogs.admin',
|
||||
'cogs.config',
|
||||
'cogs.images',
|
||||
'cogs.birthday',
|
||||
'cogs.owner',
|
||||
'cogs.stats',
|
||||
'cogs.picarto',
|
||||
'cogs.links',
|
||||
'cogs.roles',
|
||||
'cogs.tictactoe',
|
||||
'cogs.hangman',
|
||||
'cogs.events',
|
||||
'cogs.raffle',
|
||||
'cogs.blackjack',
|
||||
'cogs.osu',
|
||||
'cogs.tags',
|
||||
'cogs.roulette',
|
||||
'cogs.spotify',
|
||||
'cogs.polls'
|
||||
]
|
||||
|
||||
|
||||
# The default status the bot will use
|
||||
default_status = global_config.get("default_status", None)
|
||||
# The database hostname
|
||||
db_host = global_config.get('db_host', None)
|
||||
# The database name
|
||||
db_name = global_config.get('db_name', 'bonfire')
|
||||
# The database port
|
||||
db_port = global_config.get('db_port', None)
|
||||
# The user and password assigned
|
||||
db_user = global_config.get('db_user', None)
|
||||
db_pass = global_config.get('db_pass', None)
|
||||
# We've set all the options we need to be able to connect
|
||||
# so create a dictionary that we can use to unload to connect
|
||||
db_opts = {'host': db_host, 'database': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
|
||||
|
||||
|
||||
def command_prefix(bot, message):
|
||||
if not message.guild:
|
||||
return default_prefix
|
||||
return bot.cache.prefixes.get(message.guild.id) or default_prefix
|
141
utils/database.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
import asyncio
|
||||
import asyncpg
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
class Cache:
|
||||
"""A class to hold the entires that are called on every message/command"""
|
||||
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.prefixes = {}
|
||||
self.ignored = defaultdict(dict)
|
||||
self.custom_permissions = defaultdict(dict)
|
||||
self.restrictions = defaultdict(dict)
|
||||
|
||||
async def setup(self):
|
||||
# Make sure db is setup first
|
||||
await self.db.setup()
|
||||
|
||||
await self.load_prefixes()
|
||||
await self.load_custom_permissions()
|
||||
await self.load_restrictions()
|
||||
await self.load_ignored()
|
||||
|
||||
async def load_ignored(self):
|
||||
query = """
|
||||
SELECT
|
||||
id, ignored_channels, ignored_members
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
array_length(ignored_channels, 1) > 0 OR
|
||||
array_length(ignored_members, 1) > 0
|
||||
"""
|
||||
rows = await self.db.fetch(query)
|
||||
for row in rows:
|
||||
self.ignored[row['id']]['members'] = row['ignored_members']
|
||||
self.ignored[row['id']]['channels'] = row['ignored_channels']
|
||||
|
||||
async def load_prefixes(self):
|
||||
query = """
|
||||
SELECT
|
||||
id, prefix
|
||||
FROM
|
||||
guilds
|
||||
WHERE
|
||||
prefix IS NOT NULL
|
||||
"""
|
||||
rows = await self.db.fetch(query)
|
||||
for row in rows:
|
||||
self.prefixes[row['id']] = row['prefix']
|
||||
|
||||
def update_prefix(self, guild, prefix):
|
||||
self.prefixes[guild.id] = prefix
|
||||
|
||||
async def load_custom_permissions(self):
|
||||
query = """
|
||||
SELECT
|
||||
guild, command, permission
|
||||
FROM
|
||||
custom_permissions
|
||||
WHERE
|
||||
permission IS NOT NULL
|
||||
"""
|
||||
rows = await self.db.fetch(query)
|
||||
for row in rows:
|
||||
self.custom_permissions[row['guild']][row['command']] = row['permission']
|
||||
|
||||
def update_custom_permission(self, guild, command, permission):
|
||||
self.custom_permissions[guild.id][command.qualified_name] = permission
|
||||
|
||||
async def load_restrictions(self):
|
||||
query = """
|
||||
SELECT
|
||||
guild, source, from_to, destination
|
||||
FROM
|
||||
restrictions
|
||||
"""
|
||||
rows = await self.db.fetch(query)
|
||||
for row in rows:
|
||||
opt = {"source": row['source'], "destination": row['destination']}
|
||||
from_restrictions = self.restrictions[row['guild']].get(row['from_to'], [])
|
||||
from_restrictions.append(opt)
|
||||
self.restrictions[row['guild']][row['from_to']] = from_restrictions
|
||||
|
||||
def add_restriction(self, guild, from_to, restriction):
|
||||
restrictions = self.restrictions[guild.id].get(from_to, [])
|
||||
restrictions.append(restriction)
|
||||
self.restrictions[guild.id][from_to] = restrictions
|
||||
|
||||
def remove_restriction(self, guild, from_to, restriction):
|
||||
restrictions = self.restrictions[guild.id].get(from_to, [])
|
||||
if restriction in restrictions:
|
||||
restrictions.remove(restriction)
|
||||
self.restrictions[guild.id][from_to] = restrictions
|
||||
|
||||
|
||||
class DB:
|
||||
def __init__(self):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.opts = config.db_opts
|
||||
self.cache = {}
|
||||
self._pool = None
|
||||
|
||||
async def connect(self):
|
||||
self._pool = await asyncpg.create_pool(**self.opts)
|
||||
|
||||
async def setup(self):
|
||||
await self.connect()
|
||||
|
||||
async def _query(self, call, query, *args, **kwargs):
|
||||
"""this will acquire a connection and make the call, then return the result"""
|
||||
async with self._pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
return await getattr(connection, call)(query, *args, **kwargs)
|
||||
|
||||
async def execute(self, *args, **kwargs):
|
||||
return await self._query("execute", *args, **kwargs)
|
||||
|
||||
async def fetch(self, *args, **kwargs):
|
||||
return await self._query("fetch", *args, **kwargs)
|
||||
|
||||
async def fetchrow(self, *args, **kwargs):
|
||||
return await self._query("fetchrow", *args, **kwargs)
|
||||
|
||||
async def fetchval(self, *args, **kwargs):
|
||||
return await self._query("fetchval", *args, **kwargs)
|
||||
|
||||
async def upsert(self, table, data):
|
||||
keys = values = ""
|
||||
for num, k in enumerate(data.keys()):
|
||||
if num > 0:
|
||||
keys += ", "
|
||||
values += ", "
|
||||
keys += k
|
||||
values += f"${num}"
|
||||
query = f"INSERT INTO {table} ({keys}) VALUES ({values}) ON CONFLICT DO UPDATE"
|
||||
return await self.execute(query, *data.values())
|
|
@ -1,20 +1,28 @@
|
|||
import aiohttp
|
||||
import datetime
|
||||
import os
|
||||
from shutil import copyfile
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
from . import utilities
|
||||
|
||||
base_path = "images/banner/base"
|
||||
tmp_path = "images/banner/tmp"
|
||||
whitneyMedium = "/usr/share/fonts/whitney-medium.ttf"
|
||||
whitneyBold = "/usr/share/fonts/whitney-bold.ttf"
|
||||
whitneyMedium = "fonts/whitney-medium.ttf"
|
||||
whitneyBold = "fonts/whitney-bold.ttf"
|
||||
header_height = 125
|
||||
canvas_height = 145
|
||||
banner_background = "{}/bannerTop2.png".format(base_path)
|
||||
banner_bot = "{}/bannerBot.png".format(base_path)
|
||||
|
||||
|
||||
def convert_to_file(img):
|
||||
"""Converts a Pillow image to a file-like object"""
|
||||
new_file = BytesIO()
|
||||
# Save to this file as jpeg
|
||||
img.save(new_file, format='PNG')
|
||||
# In order to use the file, we need to seek back to the 0th position
|
||||
new_file.seek(0)
|
||||
return new_file
|
||||
|
||||
|
||||
async def create_banner(member, image_title, data):
|
||||
"""Creates a banner based on the options passed
|
||||
Paramaters:
|
||||
|
@ -23,23 +31,10 @@ async def create_banner(member, image_title, data):
|
|||
data -> A dictionary that will be displayed, in the format 'Key: Value' like normal dictionaries"""
|
||||
# First ensure the paths we need are created
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
os.makedirs(tmp_path, exist_ok=True)
|
||||
offset = 125
|
||||
|
||||
# Open up the avatar, save it as a temporary file
|
||||
avatar_url = member.avatar_url
|
||||
avatar_path = "{}/avatar_{}_{}.jpg".format(tmp_path, member.id, int(datetime.datetime.utcnow().timestamp()))
|
||||
# Ensure the user has an avatar
|
||||
if avatar_url != "":
|
||||
with aiohttp.ClientSession() as s:
|
||||
async with s.get(avatar_url) as r:
|
||||
val = await r.read()
|
||||
with open(avatar_path, "wb") as f:
|
||||
f.write(val)
|
||||
# Otherwise use the default avatar
|
||||
else:
|
||||
avatar_src_path = "{}/default_avatar.png".format(base_path)
|
||||
copyfile(avatar_src_path, avatar_path)
|
||||
# Download the avatar
|
||||
avatar = await utilities.download_image(str(member.avatar_url))
|
||||
|
||||
# Parse the data we need to create our image
|
||||
username = (member.display_name[:23] + '...') if len(member.display_name) > 23 else member.display_name
|
||||
|
@ -47,12 +42,11 @@ async def create_banner(member, image_title, data):
|
|||
result_keys = [k for k, v in data]
|
||||
result_values = [v for k, v in data]
|
||||
lines_of_text = len(result_keys)
|
||||
output_file = "{}/banner_{}_{}.jpg".format(tmp_path, member.id, int(datetime.datetime.utcnow().timestamp()))
|
||||
base_height = canvas_height + (lines_of_text * 20)
|
||||
|
||||
# This is the background to the avatar
|
||||
mask = Image.open('{}/mask.png'.format(base_path)).convert('L')
|
||||
user_avatar = Image.open(avatar_path)
|
||||
user_avatar = Image.open(avatar)
|
||||
output = ImageOps.fit(user_avatar, mask.size, centering=(0.5, 0.5))
|
||||
output.putalpha(mask)
|
||||
|
||||
|
@ -98,14 +92,13 @@ async def create_banner(member, image_title, data):
|
|||
stat_offset = draw.textsize(text, font=font, spacing=0)
|
||||
|
||||
font = ImageFont.truetype(whitneyMedium, 96)
|
||||
draw.text((360, -4), text, (255, 255, 255), font=font, align="center")
|
||||
# draw.text((360, -4), text, (255, 255, 255), font=font, align="center")
|
||||
draw.text((360, -4), text, (255, 255, 255), font=font)
|
||||
draw.text((360 + stat_offset[0], -4), stat_text, (0, 402, 504), font=font)
|
||||
save_me = text_bar.resize((350, 20), Image.ANTIALIAS)
|
||||
offset += 20
|
||||
base_image.paste(save_me, (0, offset), save_me)
|
||||
base_image.paste(header, (0, 0), header)
|
||||
base_image.save(output_file)
|
||||
output = convert_to_file(base_image)
|
||||
|
||||
os.remove(avatar_path)
|
||||
|
||||
return output_file
|
||||
return output
|
509
utils/paginator.py
Normal file
|
@ -0,0 +1,509 @@
|
|||
import asyncio
|
||||
import discord
|
||||
import itertools
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
||||
class CannotPaginate(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Pages:
|
||||
"""Implements a paginator that queries the user for the
|
||||
pagination interface.
|
||||
|
||||
Pages are 1-index based, not 0-index based.
|
||||
|
||||
If the user does not reply within 2 minutes then the pagination
|
||||
interface exits automatically.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
ctx: Context
|
||||
The context of the command.
|
||||
entries: List[str]
|
||||
A list of entries to paginate.
|
||||
per_page: int
|
||||
How many entries show up per page.
|
||||
show_entry_count: bool
|
||||
Whether to show an entry count in the footer.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
embed: discord.Embed
|
||||
The embed object that is being used to send pagination info.
|
||||
Feel free to modify this externally. Only the description,
|
||||
footer fields, and colour are internally modified.
|
||||
permissions: discord.Permissions
|
||||
Our permissions for the channel.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
|
||||
self.bot = ctx.bot
|
||||
self.entries = entries
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
self.per_page = per_page
|
||||
pages, left_over = divmod(len(self.entries), self.per_page)
|
||||
if left_over:
|
||||
pages += 1
|
||||
self.maximum_pages = pages
|
||||
self.embed = discord.Embed(colour=discord.Colour.blurple())
|
||||
self.paginating = len(entries) > per_page
|
||||
self.show_entry_count = show_entry_count
|
||||
self.reaction_emojis = [
|
||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
|
||||
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
|
||||
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
|
||||
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
|
||||
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page),
|
||||
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
|
||||
('\N{INFORMATION SOURCE}', self.show_help),
|
||||
]
|
||||
|
||||
if ctx.guild is not None:
|
||||
self.permissions = self.channel.permissions_for(ctx.guild.me)
|
||||
else:
|
||||
self.permissions = self.channel.permissions_for(ctx.bot.user)
|
||||
|
||||
if not self.permissions.embed_links:
|
||||
raise CannotPaginate('Bot does not have embed links permission.')
|
||||
|
||||
if not self.permissions.send_messages:
|
||||
raise CannotPaginate('Bot cannot send messages.')
|
||||
|
||||
if self.paginating:
|
||||
# verify we can actually use the pagination session
|
||||
if not self.permissions.add_reactions:
|
||||
raise CannotPaginate('Bot does not have add reactions permission.')
|
||||
|
||||
if not self.permissions.read_message_history:
|
||||
raise CannotPaginate('Bot does not have Read Message History permission.')
|
||||
|
||||
def get_page(self, page):
|
||||
base = (page - 1) * self.per_page
|
||||
return self.entries[base:base + self.per_page]
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
p = []
|
||||
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
|
||||
p.append(f'{index}. {entry}')
|
||||
|
||||
if self.maximum_pages > 1:
|
||||
if self.show_entry_count:
|
||||
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
|
||||
else:
|
||||
text = f'Page {page}/{self.maximum_pages}'
|
||||
|
||||
self.embed.set_footer(text=text)
|
||||
|
||||
if not self.paginating:
|
||||
self.embed.description = '\n'.join(p)
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
self.embed.description = '\n'.join(p)
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
p.append('')
|
||||
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
|
||||
self.embed.description = '\n'.join(p)
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
async def checked_show_page(self, page):
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
|
||||
async def first_page(self):
|
||||
"""goes to the first page"""
|
||||
await self.show_page(1)
|
||||
|
||||
async def last_page(self):
|
||||
"""goes to the last page"""
|
||||
await self.show_page(self.maximum_pages)
|
||||
|
||||
async def next_page(self):
|
||||
"""goes to the next page"""
|
||||
await self.checked_show_page(self.current_page + 1)
|
||||
|
||||
async def previous_page(self):
|
||||
"""goes to the previous page"""
|
||||
await self.checked_show_page(self.current_page - 1)
|
||||
|
||||
async def show_current_page(self):
|
||||
if self.paginating:
|
||||
await self.show_page(self.current_page)
|
||||
|
||||
async def numbered_page(self):
|
||||
"""lets you type a page number to go to"""
|
||||
to_delete = []
|
||||
to_delete.append(await self.channel.send('What page do you want to go to?'))
|
||||
|
||||
def message_check(m):
|
||||
return m.author == self.author and self.channel == m.channel and m.content.isdigit()
|
||||
|
||||
try:
|
||||
msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
to_delete.append(await self.channel.send('Took too long.'))
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
page = int(msg.content)
|
||||
to_delete.append(msg)
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
else:
|
||||
to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
try:
|
||||
await self.channel.delete_messages(to_delete)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
messages = ['Welcome to the interactive paginator!\n']
|
||||
messages.append('This interactively allows you to see pages of text by navigating with '
|
||||
'reactions. They are as follows:\n')
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
messages.append(f'{emoji} {func.__doc__}')
|
||||
|
||||
self.embed.description = '\n'.join(messages)
|
||||
self.embed.clear_fields()
|
||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(60.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def stop_pages(self):
|
||||
"""stops the interactive pagination session"""
|
||||
await self.message.delete()
|
||||
self.paginating = False
|
||||
|
||||
def react_check(self, reaction, user):
|
||||
if user is None or user.id != self.author.id:
|
||||
return False
|
||||
|
||||
if reaction.message.id != self.message.id:
|
||||
return False
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
if reaction.emoji == emoji:
|
||||
self.match = func
|
||||
return True
|
||||
return False
|
||||
|
||||
async def paginate(self):
|
||||
"""Actually paginate the entries and run the interactive loop if necessary."""
|
||||
first_page = self.show_page(1, first=True)
|
||||
if not self.paginating:
|
||||
await first_page
|
||||
else:
|
||||
# allow us to react to reactions right away if we're paginating
|
||||
self.bot.loop.create_task(first_page)
|
||||
|
||||
while self.paginating:
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
|
||||
except asyncio.TimeoutError:
|
||||
self.paginating = False
|
||||
try:
|
||||
await self.message.clear_reactions()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
try:
|
||||
await self.message.remove_reaction(reaction, user)
|
||||
except:
|
||||
pass # can't remove it so don't bother doing so
|
||||
|
||||
await self.match()
|
||||
|
||||
|
||||
class FieldPages(Pages):
|
||||
"""Similar to Pages except entries should be a list of
|
||||
tuples having (key, value) to show as embed fields instead.
|
||||
"""
|
||||
async def show_page(self, page, *, first=False):
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
|
||||
self.embed.clear_fields()
|
||||
self.embed.description = discord.Embed.Empty
|
||||
|
||||
for key, value in entries:
|
||||
self.embed.add_field(name=key, value=value, inline=False)
|
||||
|
||||
if self.maximum_pages > 1:
|
||||
if self.show_entry_count:
|
||||
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
|
||||
else:
|
||||
text = f'Page {page}/{self.maximum_pages}'
|
||||
|
||||
self.embed.set_footer(text=text)
|
||||
|
||||
if not self.paginating:
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
|
||||
# ?help
|
||||
# ?help Cog
|
||||
# ?help command
|
||||
# -> could be a subcommand
|
||||
|
||||
_mention = re.compile(r'<@\!?([0-9]{1,19})>')
|
||||
|
||||
|
||||
def cleanup_prefix(bot, prefix):
|
||||
m = _mention.match(prefix)
|
||||
if m:
|
||||
user = bot.get_user(int(m.group(1)))
|
||||
if user:
|
||||
return f'@{user.name} '
|
||||
return prefix
|
||||
|
||||
|
||||
async def _can_run(cmd, ctx):
|
||||
try:
|
||||
return await cmd.can_run(ctx)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def _command_signature(cmd):
|
||||
# this is modified from discord.py source
|
||||
|
||||
result = [cmd.qualified_name]
|
||||
if cmd.usage:
|
||||
result.append(cmd.usage)
|
||||
return ' '.join(result)
|
||||
|
||||
params = cmd.clean_params
|
||||
if not params:
|
||||
return ' '.join(result)
|
||||
|
||||
for name, param in params.items():
|
||||
if param.default is not param.empty:
|
||||
# We don't want None or '' to trigger the [name=value] case and instead it should
|
||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
||||
should_print = param.default if isinstance(param.default, str) else param.default is not None
|
||||
if should_print:
|
||||
result.append(f'[{name}={param.default!r}]')
|
||||
else:
|
||||
result.append(f'[{name}]')
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
result.append(f'[{name}...]')
|
||||
else:
|
||||
result.append(f'<{name}>')
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
class HelpPaginator(Pages):
|
||||
def __init__(self, ctx, entries, *, per_page=4):
|
||||
super().__init__(ctx, entries=entries, per_page=per_page)
|
||||
self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
|
||||
self.total = len(entries)
|
||||
|
||||
@classmethod
|
||||
async def from_cog(cls, ctx, cog):
|
||||
cog_name = cog.__class__.__name__
|
||||
|
||||
# get the commands
|
||||
entries = sorted(ctx.bot.get_cog_commands(cog_name), key=lambda c: c.name)
|
||||
|
||||
# remove the ones we can't run
|
||||
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
||||
|
||||
self = cls(ctx, entries)
|
||||
self.title = f'{cog_name} Commands'
|
||||
self.description = inspect.getdoc(cog)
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
async def from_command(cls, ctx, command):
|
||||
try:
|
||||
entries = sorted(command.commands, key=lambda c: c.name)
|
||||
except AttributeError:
|
||||
entries = []
|
||||
else:
|
||||
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
||||
|
||||
self = cls(ctx, entries)
|
||||
self.title = command.signature
|
||||
|
||||
if command.description:
|
||||
self.description = f'{command.description}\n\n{command.help}'
|
||||
else:
|
||||
self.description = command.help or 'No help given.'
|
||||
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
async def from_bot(cls, ctx):
|
||||
def key(c):
|
||||
return c.cog_name or '\u200bMisc'
|
||||
|
||||
entries = sorted(ctx.bot.commands, key=key)
|
||||
nested_pages = []
|
||||
per_page = 9
|
||||
|
||||
# 0: (cog, desc, commands) (max len == 9)
|
||||
# 1: (cog, desc, commands) (max len == 9)
|
||||
# ...
|
||||
|
||||
for cog, commands in itertools.groupby(entries, key=key):
|
||||
plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
||||
if len(plausible) == 0:
|
||||
continue
|
||||
|
||||
description = ctx.bot.get_cog(cog)
|
||||
if description is None:
|
||||
description = discord.Embed.Empty
|
||||
else:
|
||||
description = inspect.getdoc(description) or discord.Embed.Empty
|
||||
|
||||
nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
|
||||
|
||||
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
|
||||
# swap the get_page implementation with one that supports our style of pagination
|
||||
self.get_page = self.get_bot_page
|
||||
self._is_bot = True
|
||||
|
||||
# replace the actual total
|
||||
self.total = sum(len(o) for _, _, o in nested_pages)
|
||||
return self
|
||||
|
||||
def get_bot_page(self, page):
|
||||
cog, description, commands = self.entries[page - 1]
|
||||
self.title = f'{cog} Commands'
|
||||
self.description = description
|
||||
return commands
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
|
||||
self.embed.clear_fields()
|
||||
self.embed.description = self.description
|
||||
self.embed.title = self.title
|
||||
|
||||
if hasattr(self, '_is_bot'):
|
||||
value = 'For more help, join the official bot support server: https://discord.gg/f6uzJEj'
|
||||
self.embed.add_field(name='Support', value=value, inline=False)
|
||||
|
||||
self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
|
||||
|
||||
signature = _command_signature
|
||||
|
||||
for entry in entries:
|
||||
self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
|
||||
|
||||
if self.maximum_pages:
|
||||
self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
|
||||
|
||||
if not self.paginating:
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
|
||||
self.embed.title = 'Paginator help'
|
||||
self.embed.description = 'Hello! Welcome to the help page.'
|
||||
|
||||
messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
|
||||
self.embed.clear_fields()
|
||||
self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
|
||||
|
||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(30.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def show_bot_help(self):
|
||||
"""shows how to use the bot"""
|
||||
|
||||
self.embed.title = 'Using the bot'
|
||||
self.embed.description = 'Hello! Welcome to the help page.'
|
||||
self.embed.clear_fields()
|
||||
|
||||
entries = (
|
||||
('<argument>', 'This means the argument is __**required**__.'),
|
||||
('[argument]', 'This means the argument is __**optional**__.'),
|
||||
('[A|B]', 'This means the it can be __**either A or B**__.'),
|
||||
('[argument...]', 'This means you can have multiple arguments.\n'
|
||||
'Now that you know the basics, it should be noted that...\n'
|
||||
'__**You do not type in the brackets!**__')
|
||||
)
|
||||
|
||||
self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
|
||||
|
||||
for name, value in entries:
|
||||
self.embed.add_field(name=name, value=value, inline=False)
|
||||
|
||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(30.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|