mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
Compare commits
72 Commits
3.0.0rc1
...
3.0.0rc3.p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32bd47e105 | ||
|
|
1bb5d698cc | ||
|
|
9752a9c719 | ||
|
|
7973babe4b | ||
|
|
78e4b578e2 | ||
|
|
8eb8848898 | ||
|
|
aac1460240 | ||
|
|
dde5582669 | ||
|
|
aa854cf1f9 | ||
|
|
2bd05a5a04 | ||
|
|
3a8da1f82b | ||
|
|
811634a2b0 | ||
|
|
2512320b30 | ||
|
|
db03faf042 | ||
|
|
701259158f | ||
|
|
a5efdc6492 | ||
|
|
38b15ded87 | ||
|
|
351749dff6 | ||
|
|
2d9912cea7 | ||
|
|
c4ab34a049 | ||
|
|
985e7b3c6d | ||
|
|
7546c50226 | ||
|
|
6435f6b882 | ||
|
|
bbccb671b8 | ||
|
|
8abb24bc01 | ||
|
|
419008f644 | ||
|
|
d17c2430d7 | ||
|
|
ca533f8937 | ||
|
|
9d22d5b7b5 | ||
|
|
2846dce6ea | ||
|
|
9973b2e3b8 | ||
|
|
d008a2559a | ||
|
|
7f3a0b8a88 | ||
|
|
d0fca373ba | ||
|
|
451c4c9d54 | ||
|
|
a59002275d | ||
|
|
99bbde7be9 | ||
|
|
92dbd14006 | ||
|
|
6e9243f6e9 | ||
|
|
8bba860f85 | ||
|
|
d2d26835c3 | ||
|
|
aff62a8006 | ||
|
|
b5fd28ef7c | ||
|
|
c510ebe5e5 | ||
|
|
5ba95090d9 | ||
|
|
ad51fa830b | ||
|
|
1ba922eba2 | ||
|
|
9588a5740c | ||
|
|
7cd765d548 | ||
|
|
6022c0f7d7 | ||
|
|
0548744e94 | ||
|
|
8b2d115335 | ||
|
|
094735566d | ||
|
|
f7b1f9f0dc | ||
|
|
ce25011f0d | ||
|
|
f85034eb27 | ||
|
|
849755ecd2 | ||
|
|
9217275908 | ||
|
|
9e13ca45e6 | ||
|
|
46c38a28eb | ||
|
|
76bbcf2f8c | ||
|
|
ee7e8aa782 | ||
|
|
fd0abc250d | ||
|
|
847f9fc8d1 | ||
|
|
046e98565e | ||
|
|
71eddc89ea | ||
|
|
9730a424ec | ||
|
|
7b260cdafc | ||
|
|
4369095a51 | ||
|
|
1c706e8c45 | ||
|
|
91029b73e5 | ||
|
|
de4b42a11e |
@@ -34,6 +34,7 @@ jobs:
|
||||
python: 3.6.6
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
- TOXENV=py36
|
||||
deploy:
|
||||
- provider: pypi
|
||||
user: Red-DiscordBot
|
||||
@@ -49,12 +50,13 @@ jobs:
|
||||
python: 3.6.6
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
- TOXENV=py36
|
||||
before_deploy:
|
||||
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y crowdin
|
||||
- pip install redgettext==2.1
|
||||
- pip install redgettext==2.2
|
||||
deploy:
|
||||
- provider: script
|
||||
script: make gettext
|
||||
|
||||
453
Pipfile.lock
generated
453
Pipfile.lock
generated
@@ -57,11 +57,10 @@
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_version >= '3.5.3'",
|
||||
"version": "==3.0.0"
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@@ -79,15 +78,20 @@
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==0.3.9"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"discord-py": {
|
||||
"editable": true,
|
||||
"git": "git://github.com/Rapptz/discord.py",
|
||||
"ref": "7f4c57dd5ad20b7fa10aea485f674a4bc24b9547"
|
||||
},
|
||||
"discord.py": {
|
||||
"editable": true,
|
||||
"git": "git://github.com/Rapptz/discord.py",
|
||||
"ref": "836ae730401ea370aa10127bb9c86854c8b516ac"
|
||||
"ref": "rewrite"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
@@ -98,10 +102,10 @@
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c",
|
||||
"sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31"
|
||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
||||
],
|
||||
"version": "==1.15.0"
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
@@ -120,10 +124,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.7"
|
||||
"version": "==2.8"
|
||||
},
|
||||
"idna-ssl": {
|
||||
"hashes": [
|
||||
@@ -140,72 +144,76 @@
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"pymongo": {
|
||||
"hashes": [
|
||||
"sha256:08dea6dbff33363419af7af3bf2e9a373ff71eb22833dd7063f9b953f09a0bdf",
|
||||
"sha256:0949110db76eb1b54cecfc0c0f8468a8b9a7fd42ba23fd0d4a37d97e0b4ca203",
|
||||
"sha256:0c31a39f440801cc8603547ccaacf4cb1f02b81af6ba656621c13677b27f4426",
|
||||
"sha256:1e10b3fda5677d360440ebd12a1185944dc81d9ea9acf0c6b0681013b3fb9bc2",
|
||||
"sha256:1f59440b993666a417ba1954cfb1b7fb11cb4dea1a1d2777897009f688d000ee",
|
||||
"sha256:2b5a3806d9f656c14e9d9b693a344fc5684fdd045155594be0c505c6e9410a94",
|
||||
"sha256:4a14e2d7c2c0e07b5affcfbfc5c395d767f94bb1a822934a41a3b5371cde1458",
|
||||
"sha256:4cb50541225208b37786fdb0de632e475c4f00ec4792579df551ef48d6999d69",
|
||||
"sha256:52999666ad01de885653e1f74a86c2a6520d1004afec475180bebf3d7393a8fc",
|
||||
"sha256:562c353079e8ce7e2ad611fd7436a72f5df97be72bca59ae9ebf789a724afd5c",
|
||||
"sha256:5ce2a71f473f4703daa8d6c61a00b35ce625a7f5015b4371e3af728dafca296a",
|
||||
"sha256:6613e633676168a4500e5e6bb6e3e64d3fdb96d2dc472eb4b99235fb4141adb1",
|
||||
"sha256:8330406f294df118399c721f80979f2516447bcc73e4262826687872c864751e",
|
||||
"sha256:8e939dfa7d16609b99eb4d1fd2fc74f7a90f4fd0aaf31d611822daaff456236f",
|
||||
"sha256:8fa4303e1f50d9f0c8f2f7833b5a370a94d19d41449def62b34ae072126b4dfd",
|
||||
"sha256:966d987975aa3b4cfcdf1495930ff6ecb152fafe8e544e40633e41b24ca3e1c5",
|
||||
"sha256:aec4ea43a1b8e9782246a259410f66692f2d3aa0f03c54477e506193b0781cb6",
|
||||
"sha256:b73f889f032fbef05863f5056b46468a8262ae83628898e20b10bbbb79a3617e",
|
||||
"sha256:b752088a2f819f163d11dfdbbe627b27eef9d8478c7e57d42c5e7c600fee434e",
|
||||
"sha256:c8669f96277f140797e0ff99f80bd706271674942672a38ed694e2bfa66f3900",
|
||||
"sha256:ccf00549efaf6f8d5b35b654beb9aed2b788a5b33b05606eb818ddaa4e924ea3",
|
||||
"sha256:ce7c91463ad21ac72fc795188292b01c8366cf625e2d1e5ed473ce127b844f60",
|
||||
"sha256:d776d8d47884e6ad39ff8a301f1ae6b7d2186f209218cf024f43334dbba79c64",
|
||||
"sha256:dab0f63841aebb2b421fadb31f3c7eef27898f21274a8e5b45c4f2bccb40f9ed",
|
||||
"sha256:daedcfbf3b24b2b687e35b33252a9315425c2dd06a085a36906d516135bdd60e",
|
||||
"sha256:e7ad1ec621db2c5ad47924f63561f75abfd4fff669c62c8cc99c169c90432f59",
|
||||
"sha256:f14fb6c4058772a0d74d82874d3b89d7264d89b4ed7fa0413ea0ef8112b268b9",
|
||||
"sha256:f16c7b6b98bc400d180f05e65e2236ef4ee9d71f3815280558582670e1e67536",
|
||||
"sha256:f2d9eb92b26600ae6e8092f66da4bcede1b61a647c9080d6b44c148aff3a8ea4",
|
||||
"sha256:ffe94f9d17800610dda5282d7f6facfc216d79a93dd728a03d2f21cff3af7cc6"
|
||||
"sha256:025f94fc1e1364f00e50badc88c47f98af20012f23317234e51a11333ef986e6",
|
||||
"sha256:02aa7fb282606331aefbc0586e2cf540e9dbe5e343493295e7f390936ad2738e",
|
||||
"sha256:057210e831573e932702cf332012ed39da78edf0f02d24a3f0b213264a87a397",
|
||||
"sha256:0d946b79c56187fe139276d4c8ed612a27a616966c8b9779d6b79e2053587c8b",
|
||||
"sha256:104790893b928d310aae8a955e0bdbaa442fb0ac0a33d1bbb0741c791a407778",
|
||||
"sha256:15527ef218d95a8717486106553b0d54ff2641e795b65668754e17ab9ca6e381",
|
||||
"sha256:1826527a0b032f6e20e7ac7f72d7c26dd476a5e5aa82c04aa1c7088a59fded7d",
|
||||
"sha256:22e3aa4ce1c3eebc7f70f9ca7fd4ce1ea33e8bdb7b61996806cd312f08f84a3a",
|
||||
"sha256:244e1101e9a48615b9a16cbd194f73c115fdfefc96894803158608115f703b26",
|
||||
"sha256:24b8c04fdb633a84829d03909752c385faef249c06114cc8d8e1700b95aae5c8",
|
||||
"sha256:2c276696350785d3104412cbe3ac70ab1e3a10c408e7b20599ee41403a3ed630",
|
||||
"sha256:2d8474dc833b1182b651b184ace997a7bd83de0f51244de988d3c30e49f07de3",
|
||||
"sha256:3119b57fe1d964781e91a53e81532c85ed1701baaddec592e22f6b77a9fdf3df",
|
||||
"sha256:3bee8e7e0709b0fcdaa498a3e513bde9ffc7cd09dbceb11e425bd91c89dbd5b6",
|
||||
"sha256:436c071e01a464753d30dbfc8768dd93aecf2a8e378e5314d130b95e77b4d612",
|
||||
"sha256:46635e3f19ad04d5a7d7cf23d232388ddbfccf46d9a3b7436b6abadda4e84813",
|
||||
"sha256:4772e0b679717e7ac4608d996f57b6f380748a919b457cb05bb941467b888b22",
|
||||
"sha256:4e2cd80e16f481a62c3175b607373200e714ed29025f21559ebf7524f295689f",
|
||||
"sha256:52732960efa0e003ca1c092dc0a3c65276e897681287a788a01ca78dda3b41f0",
|
||||
"sha256:55a7de51ec7d1731b2431886d0349146645f2816e5b8eb982d7c49f89472c9f3",
|
||||
"sha256:5f8ed5934197a2d4b2087646e98de3e099a237099dcf498b9e38dd3465f74ef4",
|
||||
"sha256:64b064124fcbc8eb04a155117dc4d9a336e3cda3f069958fbc44fe70c3c3d1e9",
|
||||
"sha256:65958b8e4319f992e85dad59d8081888b97fcdbde5f0d14bc28f2848b92d3ef1",
|
||||
"sha256:7683428862e20c6a790c19e64f8ccf487f613fbc83d47e3d532df9c81668d451",
|
||||
"sha256:78566d5570c75a127c2491e343dc006798a384f06be588fe9b0cbe5595711559",
|
||||
"sha256:7d1cb00c093dbf1d0b16ccf123e79dee3b82608e4a2a88947695f0460eef13ff",
|
||||
"sha256:8c74e2a9b594f7962c62cef7680a4cb92a96b4e6e3c2f970790da67cc0213a7e",
|
||||
"sha256:8e60aa7699170f55f4b0f56ee6f8415229777ac7e4b4b1aa41fc61eec08c1f1d",
|
||||
"sha256:9447b561529576d89d3bf973e5241a88cf76e45bd101963f5236888713dea774",
|
||||
"sha256:970055bfeb0be373f2f5299a3db8432444bad3bc2f198753ee6c2a3a781e0959",
|
||||
"sha256:a6344b8542e584e140dc3c651d68bde51270e79490aa9320f9e708f9b2c39bd5",
|
||||
"sha256:ce309ca470d747b02ba6069d286a17b7df8e9c94d10d727d9cf3a64e51d85184",
|
||||
"sha256:cfbd86ed4c2b2ac71bbdbcea6669bf295def7152e3722ddd9dda94ac7981f33d",
|
||||
"sha256:d7929c513732dff093481f4a0954ed5ff16816365842136b17caa0b4992e49d3"
|
||||
],
|
||||
"version": "==3.7.1"
|
||||
"version": "==3.7.2"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
@@ -231,10 +239,10 @@
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
|
||||
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
|
||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
||||
],
|
||||
"version": "==6.9.0"
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"raven-aiohttp": {
|
||||
"hashes": [
|
||||
@@ -280,23 +288,23 @@
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
|
||||
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
|
||||
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
|
||||
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
|
||||
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
|
||||
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
|
||||
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
|
||||
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
|
||||
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
|
||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||
],
|
||||
"markers": "python_version >= '3.4.1'",
|
||||
"version": "==1.2.6"
|
||||
"version": "==1.3.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@@ -336,10 +344,10 @@
|
||||
},
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
|
||||
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
|
||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||
],
|
||||
"version": "==0.7.11"
|
||||
"version": "==0.7.12"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
@@ -350,18 +358,16 @@
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
|
||||
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_version >= '3.5.3'",
|
||||
"version": "==3.0.0"
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"attrs": {
|
||||
@@ -387,10 +393,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
||||
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
||||
],
|
||||
"version": "==2018.8.24"
|
||||
"version": "==2018.11.29"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -408,10 +414,10 @@
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==0.3.9"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
@@ -437,6 +443,13 @@
|
||||
],
|
||||
"path": "."
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
|
||||
"sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"
|
||||
],
|
||||
"version": "==3.0.10"
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
||||
@@ -446,10 +459,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.7"
|
||||
"version": "==2.8"
|
||||
},
|
||||
"idna-ssl": {
|
||||
"hashes": [
|
||||
@@ -462,7 +475,6 @@
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -474,51 +486,78 @@
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.0"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
@@ -529,48 +568,45 @@
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||
],
|
||||
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==0.7.1"
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
|
||||
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
|
||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||
],
|
||||
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
|
||||
"version": "==1.6.0"
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
||||
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
|
||||
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
|
||||
],
|
||||
"version": "==2.2.2"
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||
"sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02",
|
||||
"sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"
|
||||
],
|
||||
"version": "==3.8.2"
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
|
||||
"sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
|
||||
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
|
||||
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
|
||||
],
|
||||
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==0.9.0"
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
@@ -580,10 +616,10 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
||||
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
||||
],
|
||||
"version": "==2018.5"
|
||||
"version": "==2018.9"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@@ -603,10 +639,10 @@
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
|
||||
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
|
||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
||||
],
|
||||
"version": "==6.9.0"
|
||||
"version": "==6.10.0"
|
||||
},
|
||||
"raven-aiohttp": {
|
||||
"hashes": [
|
||||
@@ -617,10 +653,10 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||
],
|
||||
"version": "==2.19.1"
|
||||
"version": "==2.21.0"
|
||||
},
|
||||
"schema": {
|
||||
"hashes": [
|
||||
@@ -631,10 +667,10 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
@@ -645,17 +681,17 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
|
||||
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
|
||||
"sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5",
|
||||
"sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"
|
||||
],
|
||||
"version": "==1.7.9"
|
||||
"version": "==1.8.3"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
|
||||
"sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
|
||||
"sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09",
|
||||
"sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"sphinxcontrib-asyncio": {
|
||||
"hashes": [
|
||||
@@ -668,39 +704,36 @@
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42",
|
||||
"sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957"
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.9.6"
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
"sha256:7f802b37fffd3b5ef2aab104943fa5dad24bf9564bb7e732e54b8d0cfec2fca0",
|
||||
"sha256:cc97859bd7f38aa5b3b8ba55ffe7ee9952e7050faad1aedc0829cd3db2fb61d6"
|
||||
"sha256:2a8d8a63660563e41e64e3b5b677e81ce1ffa5e2a93c2c565d3768c287445800",
|
||||
"sha256:edfca7809925f49bdc110d0a2d9966bbf35a0c25637216d9586e7a5c5de17bfb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.4.0"
|
||||
"version": "==3.6.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.1.*'",
|
||||
"version": "==1.23"
|
||||
"version": "==1.24.1"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
|
||||
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
|
||||
"sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c",
|
||||
"sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd"
|
||||
],
|
||||
"markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==16.0.0"
|
||||
"version": "==16.2.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
@@ -726,23 +759,23 @@
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
|
||||
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
|
||||
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
|
||||
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
|
||||
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
|
||||
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
|
||||
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
|
||||
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
|
||||
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
|
||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||
],
|
||||
"markers": "python_version >= '3.4.1'",
|
||||
"version": "==1.2.6"
|
||||
"version": "==1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
https://github.com/Rapptz/discord.py/tarball/836ae730401ea370aa10127bb9c86854c8b516ac#egg=discord.py-1.0.0a0
|
||||
https://github.com/Rapptz/discord.py/tarball/7f4c57dd5ad20b7fa10aea485f674a4bc24b9547#egg=discord.py-1.0.0a0
|
||||
|
||||
@@ -374,6 +374,21 @@ API Reference
|
||||
inside the bot itself! Simply take a peek inside of the :code:`tests/core/test_config.py` file for examples of using
|
||||
Config in all kinds of ways.
|
||||
|
||||
.. important::
|
||||
|
||||
When getting, setting or clearing values in Config, all keys are casted to `str` for you. This
|
||||
includes keys within a `dict` when one is being set, as well as keys in nested dictionaries
|
||||
within that `dict`. For example::
|
||||
|
||||
>>> conf = Config.get_conf(self, identifier=999)
|
||||
>>> conf.register_global(foo={})
|
||||
>>> await conf.foo.set_raw(123, value=True)
|
||||
>>> await conf.foo()
|
||||
{'123': True}
|
||||
>>> await conf.foo.set({123: True, 456: {789: False}}
|
||||
>>> await conf.foo()
|
||||
{'123': True, '456': {'789': False}}
|
||||
|
||||
.. automodule:: redbot.core.config
|
||||
|
||||
Config
|
||||
|
||||
@@ -359,7 +359,7 @@ class Admin(commands.Cog):
|
||||
selfroles = await self._valid_selfroles(ctx.guild)
|
||||
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
|
||||
|
||||
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles)
|
||||
msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles)
|
||||
await ctx.send(box(msg, "diff"))
|
||||
|
||||
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
||||
|
||||
@@ -288,7 +288,10 @@ class Alias(commands.Cog):
|
||||
"""Try to execute help for the base command of the alias."""
|
||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||
if is_alias:
|
||||
base_cmd = alias.command[0]
|
||||
if self.is_command(alias.command):
|
||||
base_cmd = alias.command
|
||||
else:
|
||||
base_cmd = alias.command.rsplit(" ", 1)[0]
|
||||
|
||||
new_msg = copy(ctx.message)
|
||||
new_msg.content = _("{prefix}help {command}").format(
|
||||
|
||||
@@ -34,14 +34,14 @@ async def download_lavalink(session):
|
||||
|
||||
async def maybe_download_lavalink(loop, cog):
|
||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
||||
current_build = redbot.core.VersionInfo(*await cog.config.current_build())
|
||||
current_build = redbot.core.VersionInfo.from_json(await cog.config.current_version())
|
||||
|
||||
if not jar_exists or current_build < redbot.core.version_info:
|
||||
log.info("Downloading Lavalink.jar")
|
||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
async with ClientSession(loop=loop) as session:
|
||||
await download_lavalink(session)
|
||||
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
||||
await cog.config.current_version.set(redbot.core.version_info.to_json())
|
||||
|
||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class Audio(commands.Cog):
|
||||
"ws_port": "2332",
|
||||
"password": "youshallnotpass",
|
||||
"status": False,
|
||||
"current_build": [3, 0, 0, "alpha", 0],
|
||||
"current_version": redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
|
||||
"use_external_lavalink": False,
|
||||
}
|
||||
|
||||
@@ -253,7 +253,9 @@ class Audio(commands.Cog):
|
||||
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
|
||||
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled))
|
||||
await self._embed_msg(
|
||||
ctx, _("DJ role enabled: {true_or_false}.".format(true_or_false=not dj_enabled))
|
||||
)
|
||||
|
||||
@audioset.command()
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@@ -332,7 +334,7 @@ class Audio(commands.Cog):
|
||||
jarbuild = redbot.core.__version__
|
||||
|
||||
vote_percent = data["vote_percent"]
|
||||
msg = "----" + _("Server Settings") + "----"
|
||||
msg = "----" + _("Server Settings") + "----\n"
|
||||
if emptydc_enabled:
|
||||
msg += _("Disconnect timer: [{num_seconds}]\n").format(
|
||||
num_seconds=self._dynamic_time(emptydc_timer)
|
||||
@@ -370,7 +372,9 @@ class Audio(commands.Cog):
|
||||
"""Toggle displaying a thumbnail on audio messages."""
|
||||
thumbnail = await self.config.guild(ctx.guild).thumbnail()
|
||||
await self.config.guild(ctx.guild).thumbnail.set(not thumbnail)
|
||||
await self._embed_msg(ctx, _("Thumbnail display: {}.").format(not thumbnail))
|
||||
await self._embed_msg(
|
||||
ctx, _("Thumbnail display: {true_or_false}.").format(true_or_false=not thumbnail)
|
||||
)
|
||||
|
||||
@audioset.command()
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@@ -565,6 +569,8 @@ class Audio(commands.Cog):
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
|
||||
else:
|
||||
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
|
||||
|
||||
@@ -1097,7 +1103,7 @@ class Audio(commands.Cog):
|
||||
(
|
||||
bold(playlist_name),
|
||||
_("Tracks: {num}").format(num=len(tracks)),
|
||||
_("Author: {name}").format(self.bot.get_user(author)),
|
||||
_("Author: {name}\n").format(name=self.bot.get_user(author)),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1254,6 +1260,9 @@ class Audio(commands.Cog):
|
||||
try:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
for track in playlists[playlist_name]["tracks"]:
|
||||
if track["info"]["uri"].startswith("localtracks/"):
|
||||
if not os.path.isfile(track["info"]["uri"]):
|
||||
continue
|
||||
player.add(author_obj, lavalink.rest_api.Track(data=track))
|
||||
track_count = track_count + 1
|
||||
embed = discord.Embed(
|
||||
@@ -1533,9 +1542,9 @@ class Audio(commands.Cog):
|
||||
)
|
||||
else:
|
||||
queue_list += _("Playing: ")
|
||||
queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
|
||||
queue_list += _("Requested by: **{user}**").format(user=player.current.requester)
|
||||
queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n"
|
||||
queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
|
||||
queue_list += _("Requested by: **{user}**").format(user=player.current.requester)
|
||||
queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n"
|
||||
|
||||
for i, track in enumerate(
|
||||
player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start
|
||||
@@ -1974,6 +1983,7 @@ class Audio(commands.Cog):
|
||||
async def seek(self, ctx, seconds: int = 30):
|
||||
"""Seek ahead or behind on a track by seconds."""
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, _("Nothing playing."))
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
@@ -1986,6 +1996,13 @@ class Audio(commands.Cog):
|
||||
ctx, ctx.author
|
||||
):
|
||||
return await self._embed_msg(ctx, _("You need the DJ role to use seek."))
|
||||
if vote_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
|
||||
ctx, ctx.author
|
||||
):
|
||||
return await self._embed_msg(
|
||||
ctx, _("There are other people listening - vote to skip instead.")
|
||||
)
|
||||
if player.current:
|
||||
if player.current.is_stream:
|
||||
return await self._embed_msg(ctx, _("Can't seek on a stream."))
|
||||
@@ -2360,7 +2377,7 @@ class Audio(commands.Cog):
|
||||
if await self._check_external():
|
||||
embed = discord.Embed(
|
||||
colour=await ctx.embed_colour(),
|
||||
title=_("Websocket port set to {}.").format(ws_port),
|
||||
title=_("Websocket port set to {port}.").format(port=ws_port),
|
||||
)
|
||||
embed.set_footer(text=_("External lavalink server set to True."))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -71,13 +71,19 @@ async def get_java_version(loop) -> _JavaVersion:
|
||||
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
||||
# ...
|
||||
# We only care about the major and minor parts though.
|
||||
version_line_re = re.compile(r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?"')
|
||||
version_line_re = re.compile(
|
||||
r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?(?:-[A-Za-z0-9]+)?"'
|
||||
)
|
||||
short_version_re = re.compile(r'version "(?P<major>\d+)"')
|
||||
|
||||
lines = version_info.splitlines()
|
||||
for line in lines:
|
||||
match = version_line_re.search(line)
|
||||
short_match = short_version_re.search(line)
|
||||
if match:
|
||||
return int(match["major"]), int(match["minor"])
|
||||
elif short_match:
|
||||
return int(short_match["major"]), 0
|
||||
|
||||
raise RuntimeError(
|
||||
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Union, List, Callable
|
||||
from typing import Union, List, Callable, Set
|
||||
|
||||
import discord
|
||||
|
||||
@@ -94,7 +94,7 @@ class Cleanup(commands.Cog):
|
||||
):
|
||||
if message.created_at < two_weeks_ago:
|
||||
break
|
||||
if check(message):
|
||||
if message_filter(message):
|
||||
collected.append(message)
|
||||
if number and number <= len(collected):
|
||||
break
|
||||
@@ -133,8 +133,6 @@ class Cleanup(commands.Cog):
|
||||
def check(m):
|
||||
if text in m.content:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -145,6 +143,7 @@ class Cleanup(commands.Cog):
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
||||
author.name, author.id, len(to_delete), text, channel.id
|
||||
@@ -169,7 +168,7 @@ class Cleanup(commands.Cog):
|
||||
|
||||
member = None
|
||||
try:
|
||||
member = await commands.converter.MemberConverter().convert(ctx, user)
|
||||
member = await commands.MemberConverter().convert(ctx, user)
|
||||
except commands.BadArgument:
|
||||
try:
|
||||
_id = int(user)
|
||||
@@ -188,8 +187,6 @@ class Cleanup(commands.Cog):
|
||||
def check(m):
|
||||
if m.author.id == _id:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -200,6 +197,8 @@ class Cleanup(commands.Cog):
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = (
|
||||
"{}({}) deleted {} messages "
|
||||
" made by {}({}) in channel {}."
|
||||
@@ -263,6 +262,7 @@ class Cleanup(commands.Cog):
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
channel=channel, number=number, before=before, delete_pinned=delete_pinned
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||
author.name, author.id, len(to_delete), channel.name
|
||||
@@ -323,15 +323,35 @@ class Cleanup(commands.Cog):
|
||||
if "" in prefixes:
|
||||
prefixes.remove("")
|
||||
|
||||
cc_cog = self.bot.get_cog("CustomCommands")
|
||||
if cc_cog is not None:
|
||||
command_names: Set[str] = await cc_cog.get_command_names(ctx.guild)
|
||||
is_cc = lambda name: name in command_names
|
||||
else:
|
||||
is_cc = lambda name: False
|
||||
alias_cog = self.bot.get_cog("Alias")
|
||||
if alias_cog is not None:
|
||||
alias_names: Set[str] = (
|
||||
set((a.name for a in await alias_cog.unloaded_global_aliases()))
|
||||
| set(a.name for a in await alias_cog.unloaded_aliases(ctx.guild))
|
||||
)
|
||||
is_alias = lambda name: name in alias_names
|
||||
else:
|
||||
is_alias = lambda name: False
|
||||
|
||||
bot_id = self.bot.user.id
|
||||
|
||||
def check(m):
|
||||
if m.author.id == self.bot.user.id:
|
||||
if m.author.id == bot_id:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
p = discord.utils.find(m.content.startswith, prefixes)
|
||||
if p and len(p) > 0:
|
||||
cmd_name = m.content[len(p) :].split(" ")[0]
|
||||
return bool(self.bot.get_command(cmd_name))
|
||||
return (
|
||||
bool(self.bot.get_command(cmd_name)) or is_alias(cmd_name) or is_cc(cmd_name)
|
||||
)
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
|
||||
@@ -3,13 +3,14 @@ import random
|
||||
from datetime import datetime, timedelta
|
||||
from inspect import Parameter
|
||||
from collections import OrderedDict
|
||||
from typing import Mapping, Tuple, Dict
|
||||
from typing import Mapping, Tuple, Dict, Set
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils import menus
|
||||
from redbot.core.utils.chat_formatting import box, pagify, escape
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
_ = Translator("CustomCommands", __file__)
|
||||
@@ -43,11 +44,8 @@ class CommandObj:
|
||||
|
||||
@staticmethod
|
||||
async def get_commands(config) -> dict:
|
||||
commands = await config.commands()
|
||||
customcommands = {k: v for k, v in commands.items() if commands[k]}
|
||||
if len(customcommands) == 0:
|
||||
return None
|
||||
return customcommands
|
||||
_commands = await config.commands()
|
||||
return {k: v for k, v in _commands.items() if _commands[k]}
|
||||
|
||||
async def get_responses(self, ctx):
|
||||
intro = _(
|
||||
@@ -79,7 +77,8 @@ class CommandObj:
|
||||
responses.append(msg.content)
|
||||
return responses
|
||||
|
||||
def get_now(self) -> str:
|
||||
@staticmethod
|
||||
def get_now() -> str:
|
||||
# Get current time as a string, for 'created_at' and 'edited_at' fields
|
||||
# in the ccinfo dict
|
||||
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||
@@ -116,7 +115,7 @@ class CommandObj:
|
||||
*,
|
||||
response=None,
|
||||
cooldowns: Mapping[str, int] = None,
|
||||
ask_for: bool = True
|
||||
ask_for: bool = True,
|
||||
):
|
||||
"""Edit an already existing custom command"""
|
||||
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
||||
@@ -312,8 +311,6 @@ class CustomCommands(commands.Cog):
|
||||
Example:
|
||||
- `[p]customcom edit yourcommand Text you want`
|
||||
"""
|
||||
command = command.lower()
|
||||
|
||||
try:
|
||||
await self.commandobj.edit(ctx=ctx, command=command, response=text)
|
||||
await ctx.send(_("Custom command successfully edited."))
|
||||
@@ -327,12 +324,16 @@ class CustomCommands(commands.Cog):
|
||||
await ctx.send(e.args[0])
|
||||
|
||||
@customcom.command(name="list")
|
||||
async def cc_list(self, ctx):
|
||||
"""List all available custom commands."""
|
||||
@checks.bot_has_permissions(add_reactions=True)
|
||||
async def cc_list(self, ctx: commands.Context):
|
||||
"""List all available custom commands.
|
||||
|
||||
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||
The list displays a preview of each command's response, with
|
||||
markdown escaped and newlines replaced with spaces.
|
||||
"""
|
||||
cc_dict = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||
|
||||
if not response:
|
||||
if not cc_dict:
|
||||
await ctx.send(
|
||||
_(
|
||||
"There are no custom commands in this server."
|
||||
@@ -342,8 +343,7 @@ class CustomCommands(commands.Cog):
|
||||
return
|
||||
|
||||
results = []
|
||||
|
||||
for command, body in response.items():
|
||||
for command, body in sorted(cc_dict.items(), key=lambda t: t[0]):
|
||||
responses = body["response"]
|
||||
if isinstance(responses, list):
|
||||
result = ", ".join(responses)
|
||||
@@ -351,15 +351,33 @@ class CustomCommands(commands.Cog):
|
||||
result = responses
|
||||
else:
|
||||
continue
|
||||
results.append("{command:<15} : {result}".format(command=command, result=result))
|
||||
# Cut preview to 52 characters max
|
||||
if len(result) > 52:
|
||||
result = result[:49] + "..."
|
||||
# Replace newlines with spaces
|
||||
result = result.replace("\n", " ")
|
||||
# Escape markdown and mass mentions
|
||||
result = escape(result, formatting=True, mass_mentions=True)
|
||||
results.append((f"{ctx.clean_prefix}{command}", result))
|
||||
|
||||
commands = "\n".join(results)
|
||||
|
||||
if len(commands) < 1500:
|
||||
await ctx.send(box(commands))
|
||||
if await ctx.embed_requested():
|
||||
# We need a space before the newline incase the CC preview ends in link (GH-2295)
|
||||
content = " \n".join(map("**{0[0]}** {0[1]}".format, results))
|
||||
pages = list(pagify(content, page_length=1024))
|
||||
embed_pages = []
|
||||
for idx, page in enumerate(pages, start=1):
|
||||
embed = discord.Embed(
|
||||
title=_("Custom Command List"),
|
||||
description=page,
|
||||
colour=await ctx.embed_colour(),
|
||||
)
|
||||
embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages)))
|
||||
embed_pages.append(embed)
|
||||
await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS)
|
||||
else:
|
||||
for page in pagify(commands, delims=[" ", "\n"]):
|
||||
await ctx.author.send(box(page))
|
||||
content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results))
|
||||
pages = list(map(box, pagify(content, page_length=2000, shorten_by=10)))
|
||||
await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS)
|
||||
|
||||
async def on_message(self, message):
|
||||
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
|
||||
@@ -411,11 +429,11 @@ class CustomCommands(commands.Cog):
|
||||
|
||||
async def cc_command(self, ctx, *cc_args, raw_response, **cc_kwargs) -> None:
|
||||
cc_args = (*cc_args, *cc_kwargs.values())
|
||||
results = re.findall(r"\{([^}]+)\}", raw_response)
|
||||
results = re.findall(r"{([^}]+)\}", raw_response)
|
||||
for result in results:
|
||||
param = self.transform_parameter(result, ctx.message)
|
||||
raw_response = raw_response.replace("{" + result + "}", param)
|
||||
results = re.findall(r"\{((\d+)[^\.}]*(\.[^:}]+)?[^}]*)\}", raw_response)
|
||||
results = re.findall(r"{((\d+)[^.}]*(\.[^:}]+)?[^}]*)\}", raw_response)
|
||||
if results:
|
||||
low = min(int(result[1]) for result in results)
|
||||
for result in results:
|
||||
@@ -424,9 +442,10 @@ class CustomCommands(commands.Cog):
|
||||
raw_response = raw_response.replace("{" + result[0] + "}", arg)
|
||||
await ctx.send(raw_response)
|
||||
|
||||
def prepare_args(self, raw_response) -> Mapping[str, Parameter]:
|
||||
args = re.findall(r"\{(\d+)[^:}]*(:[^\.}]*)?[^}]*\}", raw_response)
|
||||
default = [["ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD)]]
|
||||
@staticmethod
|
||||
def prepare_args(raw_response) -> Mapping[str, Parameter]:
|
||||
args = re.findall(r"{(\d+)[^:}]*(:[^.}]*)?[^}]*\}", raw_response)
|
||||
default = [("ctx", Parameter("ctx", Parameter.POSITIONAL_OR_KEYWORD))]
|
||||
if not args:
|
||||
return OrderedDict(default)
|
||||
allowed_builtins = {
|
||||
@@ -466,7 +485,7 @@ class CustomCommands(commands.Cog):
|
||||
try:
|
||||
anno = getattr(discord, anno)
|
||||
# force an AttributeError if there's no discord.py converter
|
||||
getattr(commands.converter, anno.__name__ + "Converter")
|
||||
getattr(commands, anno.__name__ + "Converter")
|
||||
except AttributeError:
|
||||
anno = allowed_builtins.get(anno.lower(), Parameter.empty)
|
||||
if (
|
||||
@@ -520,7 +539,8 @@ class CustomCommands(commands.Cog):
|
||||
# only update cooldowns if the command isn't on cooldown
|
||||
self.cooldowns.update(new_cooldowns)
|
||||
|
||||
def transform_arg(self, result, attr, obj) -> str:
|
||||
@staticmethod
|
||||
def transform_arg(result, attr, obj) -> str:
|
||||
attr = attr[1:] # strip initial dot
|
||||
if not attr:
|
||||
return str(obj)
|
||||
@@ -530,7 +550,8 @@ class CustomCommands(commands.Cog):
|
||||
return raw_result
|
||||
return str(getattr(obj, attr, raw_result))
|
||||
|
||||
def transform_parameter(self, result, message) -> str:
|
||||
@staticmethod
|
||||
def transform_parameter(result, message) -> str:
|
||||
"""
|
||||
For security reasons only specific objects are allowed
|
||||
Internals are ignored
|
||||
@@ -554,3 +575,14 @@ class CustomCommands(commands.Cog):
|
||||
else:
|
||||
return raw_result
|
||||
return str(getattr(first, second, raw_result))
|
||||
|
||||
async def get_command_names(self, guild: discord.Guild) -> Set[str]:
|
||||
"""Get all custom command names in a guild.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Set[str]
|
||||
A set of all custom command names.
|
||||
|
||||
"""
|
||||
return set(await CommandObj.get_commands(self.config.guild(guild)))
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
from .installable import Installable
|
||||
|
||||
_ = Translator("Koala", __file__)
|
||||
|
||||
|
||||
class InstalledCog(Installable):
|
||||
@classmethod
|
||||
|
||||
@@ -325,13 +325,12 @@ class Downloader(commands.Cog):
|
||||
You may only uninstall cogs which were previously installed
|
||||
by Downloader.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
real_name = cog.name
|
||||
|
||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||
if poss_installed_path.exists():
|
||||
ctx.bot.unload_extension(real_name)
|
||||
await self._delete_cog(poss_installed_path)
|
||||
# noinspection PyTypeChecker
|
||||
await self._remove_from_installed(cog)
|
||||
await ctx.send(
|
||||
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
|
||||
@@ -344,7 +343,7 @@ class Downloader(commands.Cog):
|
||||
" files manually if it is still usable."
|
||||
" Also make sure you've unloaded the cog"
|
||||
" with `{prefix}unload {cog_name}`."
|
||||
).format(cog_name=real_name)
|
||||
).format(prefix=ctx.prefix, cog_name=real_name)
|
||||
)
|
||||
|
||||
@cog.command(name="update")
|
||||
@@ -372,13 +371,18 @@ class Downloader(commands.Cog):
|
||||
await self._reinstall_libraries(installed_and_updated)
|
||||
message = _("Cog update completed successfully.")
|
||||
|
||||
cognames = [c.name for c in installed_and_updated]
|
||||
cognames = {c.name for c in installed_and_updated}
|
||||
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames)))
|
||||
else:
|
||||
await ctx.send(_("All installed cogs are already up to date."))
|
||||
return
|
||||
await ctx.send(message)
|
||||
|
||||
cognames &= set(ctx.bot.extensions.keys()) # only reload loaded cogs
|
||||
if not cognames:
|
||||
return await ctx.send(
|
||||
_("None of the updated cogs were previously loaded. Update complete.")
|
||||
)
|
||||
message = _("Would you like to reload the updated cogs?")
|
||||
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
|
||||
if not can_react:
|
||||
@@ -402,7 +406,6 @@ class Downloader(commands.Cog):
|
||||
if can_react:
|
||||
with contextlib.suppress(discord.Forbidden):
|
||||
await query.clear_reactions()
|
||||
|
||||
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||
else:
|
||||
if can_react:
|
||||
@@ -499,7 +502,7 @@ class Downloader(commands.Cog):
|
||||
if isinstance(cog_installable, Installable):
|
||||
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
|
||||
repo = self._repo_manager.get_repo(cog_installable.repo_name)
|
||||
repo_url = repo.url
|
||||
repo_url = _("Missing from installed repos") if repo is None else repo.url
|
||||
cog_name = cog_installable.name
|
||||
else:
|
||||
made_by = "26 & co."
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import cast, Iterable
|
||||
import discord
|
||||
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank, commands
|
||||
from redbot.core import Config, bank, commands, errors
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
@@ -171,7 +171,7 @@ class Economy(commands.Cog):
|
||||
|
||||
try:
|
||||
await bank.transfer_credits(from_, to, amount)
|
||||
except ValueError as e:
|
||||
except (ValueError, errors.BalanceTooHigh) as e:
|
||||
return await ctx.send(str(e))
|
||||
|
||||
await ctx.send(
|
||||
@@ -195,36 +195,35 @@ class Economy(commands.Cog):
|
||||
author = ctx.author
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
if creds.operation == "deposit":
|
||||
await bank.deposit_credits(to, creds.sum)
|
||||
await ctx.send(
|
||||
_("{author} added {num} {currency} to {user}'s account.").format(
|
||||
try:
|
||||
if creds.operation == "deposit":
|
||||
await bank.deposit_credits(to, creds.sum)
|
||||
msg = _("{author} added {num} {currency} to {user}'s account.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=to.display_name,
|
||||
)
|
||||
)
|
||||
elif creds.operation == "withdraw":
|
||||
await bank.withdraw_credits(to, creds.sum)
|
||||
await ctx.send(
|
||||
_("{author} removed {num} {currency} from {user}'s account.").format(
|
||||
elif creds.operation == "withdraw":
|
||||
await bank.withdraw_credits(to, creds.sum)
|
||||
msg = _("{author} removed {num} {currency} from {user}'s account.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=to.display_name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
await bank.set_balance(to, creds.sum)
|
||||
msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=to.display_name,
|
||||
)
|
||||
except (ValueError, errors.BalanceTooHigh) as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
await bank.set_balance(to, creds.sum)
|
||||
await ctx.send(
|
||||
_("{author} set {users}'s account balance to {num} {currency}.").format(
|
||||
author=author.display_name,
|
||||
num=creds.sum,
|
||||
currency=currency,
|
||||
user=to.display_name,
|
||||
)
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@_bank.command()
|
||||
@check_global_setting_guildowner()
|
||||
@@ -260,7 +259,18 @@ class Economy(commands.Cog):
|
||||
if await bank.is_global(): # Role payouts will not be used
|
||||
next_payday = await self.config.user(author).next_payday()
|
||||
if cur_time >= next_payday:
|
||||
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
|
||||
try:
|
||||
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
|
||||
except errors.BalanceTooHigh as exc:
|
||||
await bank.set_balance(author, exc.max_balance)
|
||||
await ctx.send(
|
||||
_(
|
||||
"You've reached the maximum amount of {currency}! (**{balance:,}**) "
|
||||
"Please spend some more \N{GRIMACING FACE}\n\n"
|
||||
"You currently have {new_balance} {currency}."
|
||||
).format(currency=credits_name, new_balance=exc.max_balance)
|
||||
)
|
||||
return
|
||||
next_payday = cur_time + await self.config.PAYDAY_TIME()
|
||||
await self.config.user(author).next_payday.set(next_payday)
|
||||
|
||||
@@ -268,7 +278,7 @@ class Economy(commands.Cog):
|
||||
await ctx.send(
|
||||
_(
|
||||
"{author.mention} Here, take some {currency}. "
|
||||
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||
"Enjoy! (+{amount} {currency}!)\n\n"
|
||||
"You currently have {new_balance} {currency}.\n\n"
|
||||
"You are currently #{pos} on the global leaderboard!"
|
||||
).format(
|
||||
@@ -297,14 +307,25 @@ class Economy(commands.Cog):
|
||||
).PAYDAY_CREDITS() # Nice variable name
|
||||
if role_credits > credit_amount:
|
||||
credit_amount = role_credits
|
||||
await bank.deposit_credits(author, credit_amount)
|
||||
try:
|
||||
await bank.deposit_credits(author, credit_amount)
|
||||
except errors.BalanceTooHigh as exc:
|
||||
await bank.set_balance(author, exc.max_balance)
|
||||
await ctx.send(
|
||||
_(
|
||||
"You've reached the maximum amount of {currency}! "
|
||||
"Please spend some more \N{GRIMACING FACE}\n\n"
|
||||
"You currently have {new_balance} {currency}."
|
||||
).format(currency=credits_name, new_balance=exc.max_balance)
|
||||
)
|
||||
return
|
||||
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
||||
await self.config.member(author).next_payday.set(next_payday)
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(
|
||||
_(
|
||||
"{author.mention} Here, take some {currency}. "
|
||||
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||
"Enjoy! (+{amount} {currency}!)\n\n"
|
||||
"You currently have {new_balance} {currency}.\n\n"
|
||||
"You are currently #{pos} on the global leaderboard!"
|
||||
).format(
|
||||
@@ -367,7 +388,7 @@ class Economy(commands.Cog):
|
||||
@guild_only_check()
|
||||
async def payouts(self, ctx: commands.Context):
|
||||
"""Show the payouts for the slot machine."""
|
||||
await ctx.author.send(SLOT_PAYOUTS_MSG())
|
||||
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
@@ -444,7 +465,21 @@ class Economy(commands.Cog):
|
||||
then = await bank.get_balance(author)
|
||||
pay = payout["payout"](bid)
|
||||
now = then - bid + pay
|
||||
await bank.set_balance(author, now)
|
||||
try:
|
||||
await bank.set_balance(author, now)
|
||||
except errors.BalanceTooHigh as exc:
|
||||
await bank.set_balance(author, exc.max_balance)
|
||||
await channel.send(
|
||||
_(
|
||||
"You've reached the maximum amount of {currency}! "
|
||||
"Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
|
||||
).format(
|
||||
currency=await bank.get_currency_name(getattr(channel, "guild", None)),
|
||||
old_balance=then,
|
||||
new_balance=exc.max_balance,
|
||||
)
|
||||
)
|
||||
return
|
||||
phrase = T_(payout["phrase"])
|
||||
else:
|
||||
then = await bank.get_balance(author)
|
||||
@@ -561,10 +596,10 @@ class Economy(commands.Cog):
|
||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||
"""Set the amount earned each payday."""
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if creds <= 0:
|
||||
if creds <= 0 or creds > bank.MAX_BALANCE:
|
||||
await ctx.send(_("Har har so funny."))
|
||||
return
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if await bank.is_global():
|
||||
await self.config.PAYDAY_CREDITS.set(creds)
|
||||
else:
|
||||
@@ -579,6 +614,9 @@ class Economy(commands.Cog):
|
||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||
"""Set the amount earned each payday for a role."""
|
||||
guild = ctx.guild
|
||||
if creds <= 0 or creds > bank.MAX_BALANCE:
|
||||
await ctx.send(_("Har har so funny."))
|
||||
return
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if await bank.is_global():
|
||||
await ctx.send(_("The bank must be per-server for per-role paydays to work."))
|
||||
|
||||
@@ -28,7 +28,7 @@ class RPSParser:
|
||||
elif argument == "scissors":
|
||||
self.choice = RPS.scissors
|
||||
else:
|
||||
raise ValueError
|
||||
self.choice = None
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
@@ -121,6 +121,8 @@ class General(commands.Cog):
|
||||
"""Play Rock Paper Scissors."""
|
||||
author = ctx.author
|
||||
player_choice = your_choice.choice
|
||||
if not player_choice:
|
||||
return await ctx.send("This isn't a valid option. Try rock, paper, or scissors.")
|
||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||
cond = {
|
||||
(RPS.rock, RPS.paper): False,
|
||||
@@ -263,12 +265,13 @@ class General(commands.Cog):
|
||||
|
||||
except aiohttp.ClientError:
|
||||
await ctx.send(
|
||||
_("No Urban dictionary entries were found, or there was an error in the process")
|
||||
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||||
)
|
||||
return
|
||||
|
||||
if data.get("error") != 404:
|
||||
|
||||
if not data["list"]:
|
||||
return await ctx.send(_("No Urban Dictionary entries were found."))
|
||||
if await ctx.embed_requested():
|
||||
# a list of embeds
|
||||
embeds = []
|
||||
@@ -303,14 +306,14 @@ class General(commands.Cog):
|
||||
else:
|
||||
messages = []
|
||||
for ud in data["list"]:
|
||||
ud.set_default("example", "N/A")
|
||||
ud.setdefault("example", "N/A")
|
||||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||
if len(description) > 2048:
|
||||
description = "{}...".format(description[:2045])
|
||||
|
||||
message = _(
|
||||
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||||
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
|
||||
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||||
).format(word=ud.pop("word").capitalize(), description=description, **ud)
|
||||
messages.append(message)
|
||||
|
||||
@@ -325,6 +328,5 @@ class General(commands.Cog):
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_("No Urban dictionary entries were found, or there was an error in the process.")
|
||||
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
def mod_or_voice_permissions(**perms):
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||
# Author is bot owner or guild owner
|
||||
return True
|
||||
|
||||
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||
mod_role = guild.get_role(await ctx.bot.db.guild(guild).mod_role())
|
||||
|
||||
if admin_role in author.roles or mod_role in author.roles:
|
||||
return True
|
||||
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(author)
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.permissions_check(pred)
|
||||
|
||||
|
||||
def admin_or_voice_permissions(**perms):
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||
return True
|
||||
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||
if admin_role in author.roles:
|
||||
return True
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(author)
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.permissions_check(pred)
|
||||
|
||||
|
||||
def bot_has_voice_permissions(**perms):
|
||||
async def pred(ctx: commands.Context):
|
||||
guild = ctx.guild
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(guild.me)
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
@@ -2,19 +2,18 @@ import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime, timedelta
|
||||
from collections import deque, defaultdict, namedtuple
|
||||
from typing import cast
|
||||
from typing import cast, Optional
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import checks, Config, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box, escape
|
||||
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions
|
||||
from redbot.core.utils.chat_formatting import box, escape, format_perms_list
|
||||
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
|
||||
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
|
||||
from .log import log
|
||||
|
||||
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
|
||||
|
||||
_ = T_ = Translator("Mod", __file__)
|
||||
|
||||
@@ -193,7 +192,7 @@ class Mod(commands.Cog):
|
||||
yes_or_no=_("Yes") if respect_hierarchy else _("No")
|
||||
)
|
||||
msg += _("Delete delay: {num_seconds}\n").format(
|
||||
num_seconds=_("{num} seconds").format(delete_delay)
|
||||
num_seconds=_("{num} seconds").format(num=delete_delay)
|
||||
if delete_delay != -1
|
||||
else _("None")
|
||||
)
|
||||
@@ -311,13 +310,15 @@ class Mod(commands.Cog):
|
||||
if not cur_setting:
|
||||
await self.settings.guild(guild).reinvite_on_unban.set(True)
|
||||
await ctx.send(
|
||||
_("Users unbanned with {command} will be reinvited.").format(f"{ctx.prefix}unban")
|
||||
_("Users unbanned with {command} will be reinvited.").format(
|
||||
command=f"{ctx.prefix}unban"
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.settings.guild(guild).reinvite_on_unban.set(False)
|
||||
await ctx.send(
|
||||
_("Users unbanned with {command} will not be reinvited.").format(
|
||||
f"{ctx.prefix}unban"
|
||||
command=f"{ctx.prefix}unban"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -748,7 +749,8 @@ class Mod(commands.Cog):
|
||||
to send the newly unbanned user
|
||||
:returns: :class:`Invite`"""
|
||||
guild = ctx.guild
|
||||
if guild.me.permissions.manage_guild:
|
||||
my_perms: discord.Permissions = guild.me.guild_permissions
|
||||
if my_perms.manage_guild or my_perms.administrator:
|
||||
if "VANITY_URL" in guild.features:
|
||||
# guild has a vanity url so use it as the one to send
|
||||
return await guild.vanity_invite()
|
||||
@@ -778,15 +780,60 @@ class Mod(commands.Cog):
|
||||
except discord.HTTPException:
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
async def _voice_perm_check(
|
||||
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
|
||||
) -> bool:
|
||||
"""Check if the bot and user have sufficient permissions for voicebans.
|
||||
|
||||
This also verifies that the user's voice state and connected
|
||||
channel are not ``None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the permissions are sufficient and the user has
|
||||
a valid voice state.
|
||||
|
||||
"""
|
||||
if user_voice_state is None or user_voice_state.channel is None:
|
||||
await ctx.send(_("That user is not in a voice channel."))
|
||||
return False
|
||||
voice_channel: discord.VoiceChannel = user_voice_state.channel
|
||||
required_perms = discord.Permissions()
|
||||
required_perms.update(**perms)
|
||||
if not voice_channel.permissions_for(ctx.me) >= required_perms:
|
||||
await ctx.send(
|
||||
_("I require the {perms} permission(s) in that user's channel to do that.").format(
|
||||
perms=format_perms_list(required_perms)
|
||||
)
|
||||
)
|
||||
return False
|
||||
if (
|
||||
ctx.permission_state is commands.PermState.NORMAL
|
||||
and not voice_channel.permissions_for(ctx.author) >= required_perms
|
||||
):
|
||||
await ctx.send(
|
||||
_(
|
||||
"You must have the {perms} permission(s) in that user's channel to use this "
|
||||
"command."
|
||||
).format(perms=format_perms_list(required_perms))
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
|
||||
@checks.admin_or_permissions(mute_members=True, deafen_members=True)
|
||||
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Ban a user from speaking and listening in the server's voice channels."""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state is None:
|
||||
await ctx.send(_("No voice state for that user!"))
|
||||
user_voice_state: discord.VoiceState = user.voice
|
||||
if (
|
||||
await self._voice_perm_check(
|
||||
ctx, user_voice_state, deafen_members=True, mute_members=True
|
||||
)
|
||||
is False
|
||||
):
|
||||
return
|
||||
needs_mute = True if user_voice_state.mute is False else False
|
||||
needs_deafen = True if user_voice_state.deaf is False else False
|
||||
@@ -821,13 +868,15 @@ class Mod(commands.Cog):
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
|
||||
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Unban a the user from speaking and listening in the server's voice channels."""
|
||||
"""Unban a user from speaking and listening in the server's voice channels."""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state is None:
|
||||
await ctx.send(_("No voice state for that user!"))
|
||||
if (
|
||||
await self._voice_perm_check(
|
||||
ctx, user_voice_state, deafen_members=True, mute_members=True
|
||||
)
|
||||
is False
|
||||
):
|
||||
return
|
||||
needs_unmute = True if user_voice_state.mute else False
|
||||
needs_undeafen = True if user_voice_state.deaf else False
|
||||
@@ -863,68 +912,89 @@ class Mod(commands.Cog):
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_nicknames=True)
|
||||
@checks.admin_or_permissions(manage_nicknames=True)
|
||||
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
|
||||
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname: str = ""):
|
||||
"""Change a user's nickname.
|
||||
|
||||
Leaving the nickname empty will remove it.
|
||||
"""
|
||||
nickname = nickname.strip()
|
||||
if nickname == "":
|
||||
me = cast(discord.Member, ctx.me)
|
||||
if not nickname:
|
||||
nickname = None
|
||||
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
|
||||
await ctx.send("Done.")
|
||||
elif not 2 <= len(nickname) <= 32:
|
||||
await ctx.send(_("Nicknames must be between 2 and 32 characters long."))
|
||||
return
|
||||
if not (
|
||||
(me.guild_permissions.manage_nicknames or me.guild_permissions.administrator)
|
||||
and me.top_role > user.top_role
|
||||
and user != ctx.guild.owner
|
||||
):
|
||||
await ctx.send(
|
||||
_(
|
||||
"I do not have permission to rename that member. They may be higher than or "
|
||||
"equal to me in the role hierarchy."
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
|
||||
except discord.Forbidden:
|
||||
# Just in case we missed something in the permissions check above
|
||||
await ctx.send(_("I do not have permission to rename that member."))
|
||||
except discord.HTTPException as exc:
|
||||
if exc.status == 400: # BAD REQUEST
|
||||
await ctx.send(_("That nickname is invalid."))
|
||||
else:
|
||||
await ctx.send(_("An unexpected error has occured."))
|
||||
else:
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
@checks.mod_or_permissions(manage_channels=True)
|
||||
async def mute(self, ctx: commands.Context):
|
||||
"""Mute users."""
|
||||
pass
|
||||
|
||||
@mute.command(name="voice")
|
||||
@commands.guild_only()
|
||||
@mod_or_voice_permissions(mute_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True)
|
||||
async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Mute a user in their current voice channel."""
|
||||
user_voice_state = user.voice
|
||||
if (
|
||||
await self._voice_perm_check(
|
||||
ctx, user_voice_state, mute_members=True, manage_channels=True
|
||||
)
|
||||
is False
|
||||
):
|
||||
return
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
if user_voice_state:
|
||||
channel = user_voice_state.channel
|
||||
if channel and channel.permissions_for(user).speak:
|
||||
overwrites = channel.overwrites_for(user)
|
||||
overwrites.speak = False
|
||||
audit_reason = get_audit_reason(ctx.author, reason)
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
|
||||
await ctx.send(
|
||||
_("Muted {user} in channel {channel.name}").format(user, channel=channel)
|
||||
channel = user_voice_state.channel
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
||||
|
||||
if success:
|
||||
await ctx.send(
|
||||
_("Muted {user} in channel {channel.name}").format(user=user, channel=channel)
|
||||
)
|
||||
try:
|
||||
await modlog.create_case(
|
||||
self.bot,
|
||||
guild,
|
||||
ctx.message.created_at,
|
||||
"vmute",
|
||||
user,
|
||||
author,
|
||||
reason,
|
||||
until=None,
|
||||
channel=channel,
|
||||
)
|
||||
try:
|
||||
await modlog.create_case(
|
||||
self.bot,
|
||||
guild,
|
||||
ctx.message.created_at,
|
||||
"boicemute",
|
||||
user,
|
||||
author,
|
||||
reason,
|
||||
until=None,
|
||||
channel=channel,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
await ctx.send(e)
|
||||
return
|
||||
elif channel.permissions_for(user).speak is False:
|
||||
await ctx.send(
|
||||
_("That user is already muted in {channel}!").format(channel=channel.name)
|
||||
)
|
||||
return
|
||||
else:
|
||||
await ctx.send(_("That user is not in a voice channel right now!"))
|
||||
except RuntimeError as e:
|
||||
await ctx.send(e)
|
||||
else:
|
||||
await ctx.send(_("No voice state for the target!"))
|
||||
return
|
||||
await ctx.send(issue)
|
||||
|
||||
@mute.command(name="channel")
|
||||
@commands.guild_only()
|
||||
@@ -937,13 +1007,7 @@ class Mod(commands.Cog):
|
||||
author = ctx.message.author
|
||||
channel = ctx.message.channel
|
||||
guild = ctx.guild
|
||||
|
||||
if reason is None:
|
||||
audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author)
|
||||
else:
|
||||
audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format(
|
||||
a=author, r=reason
|
||||
)
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
||||
|
||||
@@ -974,26 +1038,12 @@ class Mod(commands.Cog):
|
||||
"""Mutes user in the server"""
|
||||
author = ctx.message.author
|
||||
guild = ctx.guild
|
||||
if reason is None:
|
||||
audit_reason = "server mute requested by {author} (ID {author.id})".format(
|
||||
author=author
|
||||
)
|
||||
else:
|
||||
audit_reason = (
|
||||
"server mute requested by {author} (ID {author.id}). Reason: {reason}"
|
||||
).format(author=author, reason=reason)
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
mute_success = []
|
||||
for channel in guild.channels:
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
if channel.permissions_for(user).speak:
|
||||
overwrites = channel.overwrites_for(user)
|
||||
overwrites.speak = False
|
||||
audit_reason = get_audit_reason(ctx.author, reason)
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
|
||||
else:
|
||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
||||
mute_success.append((success, issue))
|
||||
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
|
||||
mute_success.append((success, issue))
|
||||
await asyncio.sleep(0.1)
|
||||
await ctx.send(_("User has been muted in this server."))
|
||||
try:
|
||||
@@ -1014,7 +1064,7 @@ class Mod(commands.Cog):
|
||||
async def mute_user(
|
||||
self,
|
||||
guild: discord.Guild,
|
||||
channel: discord.TextChannel,
|
||||
channel: discord.abc.GuildChannel,
|
||||
author: discord.Member,
|
||||
user: discord.Member,
|
||||
reason: str,
|
||||
@@ -1022,80 +1072,83 @@ class Mod(commands.Cog):
|
||||
"""Mutes the specified user in the specified channel"""
|
||||
overwrites = channel.overwrites_for(user)
|
||||
permissions = channel.permissions_for(user)
|
||||
perms_cache = await self.settings.member(user).perms_cache()
|
||||
|
||||
if overwrites.send_messages is False or permissions.send_messages is False:
|
||||
if permissions.administrator:
|
||||
return False, T_(mute_unmute_issues["is_admin"])
|
||||
|
||||
new_overs = {}
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
new_overs.update(speak=False)
|
||||
if not isinstance(channel, discord.VoiceChannel):
|
||||
new_overs.update(send_messages=False, add_reactions=False)
|
||||
|
||||
if all(getattr(permissions, p) is False for p in new_overs.keys()):
|
||||
return False, T_(mute_unmute_issues["already_muted"])
|
||||
|
||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||
return False, T_(mute_unmute_issues["hierarchy_problem"])
|
||||
|
||||
perms_cache[str(channel.id)] = {
|
||||
"send_messages": overwrites.send_messages,
|
||||
"add_reactions": overwrites.add_reactions,
|
||||
}
|
||||
overwrites.update(send_messages=False, add_reactions=False)
|
||||
old_overs = {k: getattr(overwrites, k) for k in new_overs}
|
||||
overwrites.update(**new_overs)
|
||||
try:
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
|
||||
except discord.Forbidden:
|
||||
return False, T_(mute_unmute_issues["permissions_issue"])
|
||||
else:
|
||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||
await self.settings.member(user).set_raw(
|
||||
"perms_cache", str(channel.id), value=old_overs
|
||||
)
|
||||
return True, None
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_roles=True)
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
@checks.mod_or_permissions(manage_channels=True)
|
||||
async def unmute(self, ctx: commands.Context):
|
||||
"""Unmute users."""
|
||||
pass
|
||||
|
||||
@unmute.command(name="voice")
|
||||
@commands.guild_only()
|
||||
@mod_or_voice_permissions(mute_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True)
|
||||
async def unmute_voice(
|
||||
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
|
||||
):
|
||||
"""Unmute a user in their current voice channel."""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state:
|
||||
channel = user_voice_state.channel
|
||||
if channel and channel.permissions_for(user).speak is False:
|
||||
overwrites = channel.overwrites_for(user)
|
||||
overwrites.speak = None
|
||||
audit_reason = get_audit_reason(ctx.author, reason)
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
await ctx.send(
|
||||
_("Unmuted {}#{} in channel {}").format(
|
||||
user.name, user.discriminator, channel.name
|
||||
)
|
||||
)
|
||||
try:
|
||||
await modlog.create_case(
|
||||
self.bot,
|
||||
guild,
|
||||
ctx.message.created_at,
|
||||
"voiceunmute",
|
||||
user,
|
||||
author,
|
||||
reason,
|
||||
until=None,
|
||||
channel=channel,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
await ctx.send(e)
|
||||
elif channel.permissions_for(user).speak:
|
||||
await ctx.send(_("That user is already unmuted in {}!").format(channel.name))
|
||||
return
|
||||
else:
|
||||
await ctx.send(_("That user is not in a voice channel right now!"))
|
||||
else:
|
||||
await ctx.send(_("No voice state for the target!"))
|
||||
if (
|
||||
await self._voice_perm_check(
|
||||
ctx, user_voice_state, mute_members=True, manage_channels=True
|
||||
)
|
||||
is False
|
||||
):
|
||||
return
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
channel = user_voice_state.channel
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
||||
|
||||
if success:
|
||||
await ctx.send(
|
||||
_("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel)
|
||||
)
|
||||
try:
|
||||
await modlog.create_case(
|
||||
self.bot,
|
||||
guild,
|
||||
ctx.message.created_at,
|
||||
"vunmute",
|
||||
user,
|
||||
author,
|
||||
reason,
|
||||
until=None,
|
||||
channel=channel,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
await ctx.send(e)
|
||||
else:
|
||||
await ctx.send(_("Unmute failed. Reason: {}").format(message))
|
||||
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@unmute.command(name="channel")
|
||||
@@ -1108,8 +1161,9 @@ class Mod(commands.Cog):
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
success, message = await self.unmute_user(guild, channel, author, user)
|
||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
||||
|
||||
if success:
|
||||
await ctx.send(_("User unmuted in this channel."))
|
||||
@@ -1140,16 +1194,11 @@ class Mod(commands.Cog):
|
||||
"""Unmute a user in this server."""
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
|
||||
unmute_success = []
|
||||
for channel in guild.channels:
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
if channel.permissions_for(user).speak is False:
|
||||
overwrites = channel.overwrites_for(user)
|
||||
overwrites.speak = None
|
||||
audit_reason = get_audit_reason(author, reason)
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
|
||||
success, message = await self.unmute_user(guild, channel, author, user)
|
||||
success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
|
||||
unmute_success.append((success, message))
|
||||
await asyncio.sleep(0.1)
|
||||
await ctx.send(_("User has been unmuted in this server."))
|
||||
@@ -1170,45 +1219,37 @@ class Mod(commands.Cog):
|
||||
async def unmute_user(
|
||||
self,
|
||||
guild: discord.Guild,
|
||||
channel: discord.TextChannel,
|
||||
channel: discord.abc.GuildChannel,
|
||||
author: discord.Member,
|
||||
user: discord.Member,
|
||||
reason: str,
|
||||
) -> (bool, str):
|
||||
overwrites = channel.overwrites_for(user)
|
||||
permissions = channel.permissions_for(user)
|
||||
perms_cache = await self.settings.member(user).perms_cache()
|
||||
|
||||
if overwrites.send_messages or permissions.send_messages:
|
||||
if channel.id in perms_cache:
|
||||
old_values = perms_cache[channel.id]
|
||||
else:
|
||||
old_values = {"send_messages": None, "add_reactions": None, "speak": None}
|
||||
|
||||
if all(getattr(overwrites, k) == v for k, v in old_values.items()):
|
||||
return False, T_(mute_unmute_issues["already_unmuted"])
|
||||
|
||||
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
|
||||
return False, T_(mute_unmute_issues["hierarchy_problem"])
|
||||
|
||||
if channel.id in perms_cache:
|
||||
old_values = perms_cache[channel.id]
|
||||
else:
|
||||
old_values = {"send_messages": None, "add_reactions": None}
|
||||
overwrites.update(
|
||||
send_messages=old_values["send_messages"], add_reactions=old_values["add_reactions"]
|
||||
)
|
||||
is_empty = self.are_overwrites_empty(overwrites)
|
||||
|
||||
overwrites.update(**old_values)
|
||||
try:
|
||||
if not is_empty:
|
||||
await channel.set_permissions(user, overwrite=overwrites)
|
||||
else:
|
||||
if overwrites.is_empty():
|
||||
await channel.set_permissions(
|
||||
user, overwrite=cast(discord.PermissionOverwrite, None)
|
||||
user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason
|
||||
)
|
||||
else:
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
|
||||
except discord.Forbidden:
|
||||
return False, T_(mute_unmute_issues["permissions_issue"])
|
||||
else:
|
||||
try:
|
||||
del perms_cache[channel.id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
await self.settings.member(user).perms_cache.set(perms_cache)
|
||||
await self.settings.member(user).clear_raw("perms_cache", str(channel.id))
|
||||
return True, None
|
||||
|
||||
@commands.group()
|
||||
@@ -1591,8 +1632,9 @@ class Mod(commands.Cog):
|
||||
"""
|
||||
An event for modlog case creation
|
||||
"""
|
||||
mod_channel = await modlog.get_modlog_channel(case.guild)
|
||||
if mod_channel is None:
|
||||
try:
|
||||
mod_channel = await modlog.get_modlog_channel(case.guild)
|
||||
except RuntimeError:
|
||||
return
|
||||
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
|
||||
case_content = await case.message_content(use_embeds)
|
||||
@@ -1694,20 +1736,15 @@ class Mod(commands.Cog):
|
||||
while len(nick_list) > 20:
|
||||
nick_list.pop(0)
|
||||
|
||||
@staticmethod
|
||||
def are_overwrites_empty(overwrites):
|
||||
"""There is currently no cleaner way to check if a
|
||||
PermissionOverwrite object is empty"""
|
||||
return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
|
||||
|
||||
|
||||
_ = lambda s: s
|
||||
mute_unmute_issues = {
|
||||
"already_muted": _("That user can't send messages in this channel."),
|
||||
"already_unmuted": _("That user isn't muted in this channel!"),
|
||||
"already_unmuted": _("That user isn't muted in this channel."),
|
||||
"hierarchy_problem": _(
|
||||
"I cannot let you do that. You are not higher than " "the user in the role hierarchy."
|
||||
"I cannot let you do that. You are not higher than the user in the role hierarchy."
|
||||
),
|
||||
"is_admin": _("That user cannot be muted, as they have the Administrator permission."),
|
||||
"permissions_issue": _(
|
||||
"Failed to mute user. I need the manage roles "
|
||||
"permission and the user I'm muting must be "
|
||||
|
||||
@@ -1,10 +1,133 @@
|
||||
from typing import NamedTuple, Union, Optional, cast, Type
|
||||
import itertools
|
||||
import re
|
||||
from typing import NamedTuple, Union, Optional
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("PermissionsConverters", __file__)
|
||||
|
||||
MENTION_RE = re.compile(r"^<?(?:(?:@[!&]?)?|#)(\d{15,21})>?$")
|
||||
|
||||
|
||||
def _match_id(arg: str) -> Optional[int]:
|
||||
m = MENTION_RE.match(arg)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
|
||||
|
||||
class GlobalUniqueObjectFinder(commands.Converter):
|
||||
async def convert(
|
||||
self, ctx: commands.Context, arg: str
|
||||
) -> Union[discord.Guild, discord.abc.GuildChannel, discord.abc.User, discord.Role]:
|
||||
bot: commands.Bot = ctx.bot
|
||||
_id = _match_id(arg)
|
||||
|
||||
if _id is not None:
|
||||
guild: discord.Guild = bot.get_guild(_id)
|
||||
if guild is not None:
|
||||
return guild
|
||||
channel: discord.abc.GuildChannel = bot.get_channel(_id)
|
||||
if channel is not None:
|
||||
return channel
|
||||
|
||||
user: discord.User = bot.get_user(_id)
|
||||
if user is not None:
|
||||
return user
|
||||
|
||||
for guild in bot.guilds:
|
||||
role: discord.Role = guild.get_role(_id)
|
||||
if role is not None:
|
||||
return role
|
||||
|
||||
objects = itertools.chain(
|
||||
bot.get_all_channels(),
|
||||
bot.users,
|
||||
bot.guilds,
|
||||
*(filter(lambda r: not r.is_default(), guild.roles) for guild in bot.guilds),
|
||||
)
|
||||
|
||||
maybe_matches = []
|
||||
for obj in objects:
|
||||
if obj.name == arg or str(obj) == arg:
|
||||
maybe_matches.append(obj)
|
||||
|
||||
if ctx.guild is not None:
|
||||
for member in ctx.guild.members:
|
||||
if member.nick == arg and not any(obj.id == member.id for obj in maybe_matches):
|
||||
maybe_matches.append(member)
|
||||
|
||||
if not maybe_matches:
|
||||
raise commands.BadArgument(
|
||||
_(
|
||||
'"{arg}" was not found. It must be the ID, mention, or name of a server, '
|
||||
"channel, user or role which the bot can see."
|
||||
).format(arg=arg)
|
||||
)
|
||||
elif len(maybe_matches) == 1:
|
||||
return maybe_matches[0]
|
||||
else:
|
||||
raise commands.BadArgument(
|
||||
_(
|
||||
'"{arg}" does not refer to a unique server, channel, user or role. Please use '
|
||||
"the ID for whatever/whoever you're trying to specify, or mention it/them."
|
||||
).format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class GuildUniqueObjectFinder(commands.Converter):
|
||||
async def convert(
|
||||
self, ctx: commands.Context, arg: str
|
||||
) -> Union[discord.abc.GuildChannel, discord.Member, discord.Role]:
|
||||
guild: discord.Guild = ctx.guild
|
||||
_id = _match_id(arg)
|
||||
|
||||
if _id is not None:
|
||||
channel: discord.abc.GuildChannel = guild.get_channel(_id)
|
||||
if channel is not None:
|
||||
return channel
|
||||
|
||||
member: discord.Member = guild.get_member(_id)
|
||||
if member is not None:
|
||||
return member
|
||||
|
||||
role: discord.Role = guild.get_role(_id)
|
||||
if role is not None and not role.is_default():
|
||||
return role
|
||||
|
||||
objects = itertools.chain(
|
||||
guild.channels, guild.members, filter(lambda r: not r.is_default(), guild.roles)
|
||||
)
|
||||
|
||||
maybe_matches = []
|
||||
for obj in objects:
|
||||
if obj.name == arg or str(obj) == arg:
|
||||
maybe_matches.append(obj)
|
||||
try:
|
||||
if obj.nick == arg:
|
||||
maybe_matches.append(obj)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not maybe_matches:
|
||||
raise commands.BadArgument(
|
||||
_(
|
||||
'"{arg}" was not found. It must be the ID, mention, or name of a channel, '
|
||||
"user or role in this server."
|
||||
).format(arg=arg)
|
||||
)
|
||||
elif len(maybe_matches) == 1:
|
||||
return maybe_matches[0]
|
||||
else:
|
||||
raise commands.BadArgument(
|
||||
_(
|
||||
'"{arg}" does not refer to a unique channel, user or role. Please use the ID '
|
||||
"for whatever/whoever you're trying to specify, or mention it/them."
|
||||
).format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class CogOrCommand(NamedTuple):
|
||||
type: str
|
||||
|
||||
@@ -14,7 +14,13 @@ from redbot.core.utils.chat_formatting import box
|
||||
from redbot.core.utils.menus import start_adding_reactions
|
||||
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
|
||||
|
||||
from .converters import CogOrCommand, RuleType, ClearableRuleType
|
||||
from .converters import (
|
||||
CogOrCommand,
|
||||
RuleType,
|
||||
ClearableRuleType,
|
||||
GuildUniqueObjectFinder,
|
||||
GlobalUniqueObjectFinder,
|
||||
)
|
||||
|
||||
_ = Translator("Permissions", __file__)
|
||||
|
||||
@@ -142,23 +148,20 @@ class Permissions(commands.Cog):
|
||||
if not command:
|
||||
return await ctx.send_help()
|
||||
|
||||
message = copy(ctx.message)
|
||||
message.author = user
|
||||
message.content = "{}{}".format(ctx.prefix, command)
|
||||
fake_message = copy(ctx.message)
|
||||
fake_message.author = user
|
||||
fake_message.content = "{}{}".format(ctx.prefix, command)
|
||||
|
||||
com = ctx.bot.get_command(command)
|
||||
if com is None:
|
||||
out = _("No such command")
|
||||
else:
|
||||
fake_context = await ctx.bot.get_context(fake_message)
|
||||
try:
|
||||
testcontext = await ctx.bot.get_context(message, cls=commands.Context)
|
||||
to_check = [*reversed(com.parents)] + [com]
|
||||
can = False
|
||||
for cmd in to_check:
|
||||
can = await cmd.can_run(testcontext)
|
||||
if can is False:
|
||||
break
|
||||
except commands.CheckFailure:
|
||||
can = await com.can_run(
|
||||
fake_context, check_all_parents=True, change_permission_state=False
|
||||
)
|
||||
except commands.CommandError:
|
||||
can = False
|
||||
|
||||
out = (
|
||||
@@ -275,7 +278,7 @@ class Permissions(commands.Cog):
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: commands.GlobalPermissionModel,
|
||||
who_or_what: GlobalUniqueObjectFinder,
|
||||
):
|
||||
"""Add a global rule to a command.
|
||||
|
||||
@@ -303,7 +306,7 @@ class Permissions(commands.Cog):
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: commands.GuildPermissionModel,
|
||||
who_or_what: GuildUniqueObjectFinder,
|
||||
):
|
||||
"""Add a rule to a command in this server.
|
||||
|
||||
@@ -328,7 +331,7 @@ class Permissions(commands.Cog):
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: commands.GlobalPermissionModel,
|
||||
who_or_what: GlobalUniqueObjectFinder,
|
||||
):
|
||||
"""Remove a global rule from a command.
|
||||
|
||||
@@ -351,7 +354,7 @@ class Permissions(commands.Cog):
|
||||
ctx: commands.Context,
|
||||
cog_or_command: CogOrCommand,
|
||||
*,
|
||||
who_or_what: commands.GuildPermissionModel,
|
||||
who_or_what: GuildUniqueObjectFinder,
|
||||
):
|
||||
"""Remove a server rule from a command.
|
||||
|
||||
@@ -542,7 +545,8 @@ class Permissions(commands.Cog):
|
||||
continue
|
||||
conf = self.config.custom(category)
|
||||
for cmd_name, cmd_rules in rules_dict.items():
|
||||
await conf.set_raw(cmd_name, guild_id, value=cmd_rules)
|
||||
cmd_rules = {str(model_id): rule for model_id, rule in cmd_rules.items()}
|
||||
await conf.set_raw(cmd_name, str(guild_id), value=cmd_rules)
|
||||
cmd_obj = getter(cmd_name)
|
||||
if cmd_obj is not None:
|
||||
self._load_rules_for(cmd_obj, {guild_id: cmd_rules})
|
||||
@@ -651,14 +655,14 @@ class Permissions(commands.Cog):
|
||||
if category in old_rules:
|
||||
for name, rules in old_rules[category].items():
|
||||
these_rules = new_rules.setdefault(name, {})
|
||||
guild_rules = these_rules.setdefault(guild_id, {})
|
||||
guild_rules = these_rules.setdefault(str(guild_id), {})
|
||||
# Since allow rules would take precedence if the same model ID
|
||||
# sat in both the allow and deny list, we add the deny entries
|
||||
# first and let any conflicting allow entries overwrite.
|
||||
for model_id in rules.get("deny", []):
|
||||
guild_rules[model_id] = False
|
||||
guild_rules[str(model_id)] = False
|
||||
for model_id in rules.get("allow", []):
|
||||
guild_rules[model_id] = True
|
||||
guild_rules[str(model_id)] = True
|
||||
if "default" in rules:
|
||||
default = rules["default"]
|
||||
if default == "allow":
|
||||
@@ -689,7 +693,9 @@ class Permissions(commands.Cog):
|
||||
"""
|
||||
for guild_id, guild_dict in _int_key_map(rule_dict.items()):
|
||||
for model_id, rule in _int_key_map(guild_dict.items()):
|
||||
if rule is True:
|
||||
if model_id == "default":
|
||||
cog_or_command.set_default_rule(rule, guild_id=guild_id)
|
||||
elif rule is True:
|
||||
cog_or_command.allow_for(model_id, guild_id=guild_id)
|
||||
elif rule is False:
|
||||
cog_or_command.deny_to(model_id, guild_id=guild_id)
|
||||
@@ -724,9 +730,16 @@ class Permissions(commands.Cog):
|
||||
rules.
|
||||
"""
|
||||
for guild_id, guild_dict in _int_key_map(rule_dict.items()):
|
||||
for model_id in map(int, guild_dict.keys()):
|
||||
cog_or_command.clear_rule_for(model_id, guild_id)
|
||||
for model_id in guild_dict.keys():
|
||||
if model_id == "default":
|
||||
cog_or_command.set_default_rule(None, guild_id=guild_id)
|
||||
else:
|
||||
cog_or_command.clear_rule_for(int(model_id), guild_id=guild_id)
|
||||
|
||||
|
||||
def _int_key_map(items_view: ItemsView[str, Any]) -> Iterator[Tuple[int, Any]]:
|
||||
return map(lambda tup: (int(tup[0]), tup[1]), items_view)
|
||||
def _int_key_map(items_view: ItemsView[str, Any]) -> Iterator[Tuple[Union[str, int], Any]]:
|
||||
for k, v in items_view:
|
||||
if k == "default":
|
||||
yield k, v
|
||||
else:
|
||||
yield int(k), v
|
||||
|
||||
@@ -316,7 +316,7 @@ class Reports(commands.Cog):
|
||||
self.tunnel_store[k]["msgs"] = msgs
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_members=True)
|
||||
@checks.mod_or_permissions(manage_roles=True)
|
||||
@report.command(name="interact")
|
||||
async def response(self, ctx, ticket_number: int):
|
||||
"""Open a message tunnel.
|
||||
|
||||
@@ -28,7 +28,7 @@ from . import streamtypes as _streamtypes
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
CHECK_DELAY = 60
|
||||
|
||||
@@ -320,6 +320,7 @@ class Streams(commands.Cog):
|
||||
@commands.group()
|
||||
@checks.mod()
|
||||
async def streamset(self, ctx: commands.Context):
|
||||
"""Set tokens for accessing streams."""
|
||||
pass
|
||||
|
||||
@streamset.command()
|
||||
@@ -396,9 +397,6 @@ class Streams(commands.Cog):
|
||||
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""Toggle a role mention."""
|
||||
current_setting = await self.db.role(role).mention()
|
||||
if not role.mentionable:
|
||||
await ctx.send("That role is not mentionable!")
|
||||
return
|
||||
if current_setting:
|
||||
await self.db.role(role).mention.set(False)
|
||||
await ctx.send(
|
||||
@@ -408,11 +406,17 @@ class Streams(commands.Cog):
|
||||
)
|
||||
else:
|
||||
await self.db.role(role).mention.set(True)
|
||||
await ctx.send(
|
||||
_(
|
||||
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
|
||||
).format(role=role)
|
||||
)
|
||||
msg = _(
|
||||
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
|
||||
).format(role=role)
|
||||
if not role.mentionable:
|
||||
msg += " " + _(
|
||||
"Since the role is not mentionable, it will be momentarily made mentionable "
|
||||
"when announcing a streamalert. Please make sure I have the correct "
|
||||
"permissions to manage this role, or else members of this role won't receive "
|
||||
"a notification."
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@streamset.command()
|
||||
@commands.guild_only()
|
||||
@@ -535,30 +539,46 @@ class Streams(commands.Cog):
|
||||
continue
|
||||
for channel_id in stream.channels:
|
||||
channel = self.bot.get_channel(channel_id)
|
||||
mention_str = await self._get_mention_str(channel.guild)
|
||||
mention_str, edited_roles = await self._get_mention_str(channel.guild)
|
||||
|
||||
if mention_str:
|
||||
content = _("{mention}, {stream.name} is live!").format(
|
||||
mention=mention_str, stream=stream
|
||||
)
|
||||
else:
|
||||
content = _("{stream.name} is live!").format(stream=stream.name)
|
||||
content = _("{stream.name} is live!").format(stream=stream)
|
||||
|
||||
m = await channel.send(content, embed=embed)
|
||||
stream._messages_cache.append(m)
|
||||
if edited_roles:
|
||||
for role in edited_roles:
|
||||
await role.edit(mentionable=False)
|
||||
await self.save_streams()
|
||||
|
||||
async def _get_mention_str(self, guild: discord.Guild):
|
||||
async def _get_mention_str(self, guild: discord.Guild) -> Tuple[str, List[discord.Role]]:
|
||||
"""Returns a 2-tuple with the string containing the mentions, and a list of
|
||||
all roles which need to have their `mentionable` property set back to False.
|
||||
"""
|
||||
settings = self.db.guild(guild)
|
||||
mentions = []
|
||||
edited_roles = []
|
||||
if await settings.mention_everyone():
|
||||
mentions.append("@everyone")
|
||||
if await settings.mention_here():
|
||||
mentions.append("@here")
|
||||
can_manage_roles = guild.me.guild_permissions.manage_roles
|
||||
for role in guild.roles:
|
||||
if await self.db.role(role).mention():
|
||||
if can_manage_roles and not role.mentionable:
|
||||
try:
|
||||
await role.edit(mentionable=True)
|
||||
except discord.Forbidden:
|
||||
# Might still be unable to edit role based on hierarchy
|
||||
pass
|
||||
else:
|
||||
edited_roles.append(role)
|
||||
mentions.append(role.mention)
|
||||
return " ".join(mentions)
|
||||
return " ".join(mentions), edited_roles
|
||||
|
||||
async def check_communities(self):
|
||||
for community in self.communities:
|
||||
@@ -589,12 +609,15 @@ class Streams(commands.Cog):
|
||||
emb = await community.make_embed(streams)
|
||||
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
||||
if not chn_msg:
|
||||
mentions = await self._get_mention_str(chn.guild)
|
||||
mentions, roles = await self._get_mention_str(chn.guild)
|
||||
if mentions:
|
||||
msg = await chn.send(mentions, embed=emb)
|
||||
else:
|
||||
msg = await chn.send(embed=emb)
|
||||
community._messages_cache.append(msg)
|
||||
if roles:
|
||||
for role in roles:
|
||||
await role.edit(mentionable=False)
|
||||
await self.save_communities()
|
||||
else:
|
||||
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
||||
@@ -626,8 +649,13 @@ class Streams(commands.Cog):
|
||||
raw_stream["_messages_cache"] = []
|
||||
for raw_msg in raw_msg_cache:
|
||||
chn = self.bot.get_channel(raw_msg["channel"])
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
raw_stream["_messages_cache"].append(msg)
|
||||
if chn is not None:
|
||||
try:
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
raw_stream["_messages_cache"].append(msg)
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
if token is not None:
|
||||
raw_stream["token"] = token
|
||||
@@ -646,8 +674,13 @@ class Streams(commands.Cog):
|
||||
raw_community["_messages_cache"] = []
|
||||
for raw_msg in raw_msg_cache:
|
||||
chn = self.bot.get_channel(raw_msg["channel"])
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
raw_community["_messages_cache"].append(msg)
|
||||
if chn is not None:
|
||||
try:
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
raw_community["_messages_cache"].append(msg)
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
communities.append(_class(token=token, **raw_community))
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class TriviaSession:
|
||||
async with self.ctx.typing():
|
||||
await asyncio.sleep(3)
|
||||
self.count += 1
|
||||
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
|
||||
msg = bold(_("Question number {num}!").format(num=self.count)) + "\n\n" + question
|
||||
await self.ctx.send(msg)
|
||||
continue_ = await self.wait_for_answer(answers, delay, timeout)
|
||||
if continue_ is False:
|
||||
@@ -322,9 +322,9 @@ def _parse_answers(answers):
|
||||
for answer in answers:
|
||||
if isinstance(answer, bool):
|
||||
if answer is True:
|
||||
ret.extend(["True", "Yes", _("Yes")])
|
||||
ret.extend(["True", "Yes", "On"])
|
||||
else:
|
||||
ret.extend(["False", "No", _("No")])
|
||||
ret.extend(["False", "No", "Off"])
|
||||
else:
|
||||
ret.append(str(answer))
|
||||
# Uniquify list
|
||||
|
||||
@@ -111,16 +111,14 @@ class Trivia(commands.Cog):
|
||||
await settings.allow_override.set(enabled)
|
||||
if enabled:
|
||||
await ctx.send(
|
||||
_(
|
||||
"Done. Trivia lists can now override the trivia settings for this server."
|
||||
).format(now=enabled)
|
||||
_("Done. Trivia lists can now override the trivia settings for this server.")
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_(
|
||||
"Done. Trivia lists can no longer override the trivia settings for this "
|
||||
"server."
|
||||
).format(now=enabled)
|
||||
)
|
||||
)
|
||||
|
||||
@triviaset.command(name="botplays", usage="<true_or_false>")
|
||||
@@ -506,7 +504,7 @@ class Trivia(commands.Cog):
|
||||
|
||||
with path.open(encoding="utf-8") as file:
|
||||
try:
|
||||
dict_ = yaml.load(file)
|
||||
dict_ = yaml.safe_load(file)
|
||||
except yaml.error.YAMLError as exc:
|
||||
raise InvalidListError("YAML parsing failed.") from exc
|
||||
else:
|
||||
|
||||
@@ -19,9 +19,11 @@ async def warning_points_add_check(
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions:
|
||||
# Actions are sorted in decreasing order of points.
|
||||
# The first action we find where the user is above the threshold will be the
|
||||
# highest action we can take.
|
||||
if points >= a["points"]:
|
||||
act = a
|
||||
else:
|
||||
break
|
||||
if act and act["exceed_command"] is not None: # some action needs to be taken
|
||||
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
||||
|
||||
@@ -9,7 +9,7 @@ from redbot.cogs.warnings.helpers import (
|
||||
get_command_for_dropping_points,
|
||||
warning_points_remove_check,
|
||||
)
|
||||
from redbot.core import Config, modlog, checks, commands
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import is_admin_or_superior
|
||||
@@ -34,15 +34,14 @@ class Warnings(commands.Cog):
|
||||
self.config.register_guild(**self.default_guild)
|
||||
self.config.register_member(**self.default_member)
|
||||
self.bot = bot
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self.register_warningtype())
|
||||
|
||||
@staticmethod
|
||||
async def register_warningtype():
|
||||
try:
|
||||
await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
|
||||
except RuntimeError:
|
||||
pass
|
||||
# We're not utilising modlog yet - no need to register a casetype
|
||||
# @staticmethod
|
||||
# async def register_warningtype():
|
||||
# try:
|
||||
# await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
|
||||
# except RuntimeError:
|
||||
# pass
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
|
||||
@@ -1,40 +1,152 @@
|
||||
import re as _re
|
||||
from math import inf as _inf
|
||||
from typing import (
|
||||
Any as _Any,
|
||||
ClassVar as _ClassVar,
|
||||
Dict as _Dict,
|
||||
List as _List,
|
||||
Optional as _Optional,
|
||||
Pattern as _Pattern,
|
||||
Tuple as _Tuple,
|
||||
Union as _Union,
|
||||
)
|
||||
|
||||
from .config import Config
|
||||
|
||||
__all__ = ["Config", "__version__"]
|
||||
__all__ = ["Config", "__version__", "version_info", "VersionInfo"]
|
||||
|
||||
|
||||
class VersionInfo:
|
||||
def __init__(self, major, minor, micro, releaselevel, serial):
|
||||
self._levels = ["alpha", "beta", "release candidate", "final"]
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.micro = micro
|
||||
ALPHA = "alpha"
|
||||
BETA = "beta"
|
||||
RELEASE_CANDIDATE = "release candidate"
|
||||
FINAL = "final"
|
||||
|
||||
if releaselevel not in self._levels:
|
||||
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels)))
|
||||
_VERSION_STR_PATTERN: _ClassVar[_Pattern[str]] = _re.compile(
|
||||
r"^"
|
||||
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<micro>0|[1-9]\d*)"
|
||||
r"(?:(?P<releaselevel>a|b|rc)(?P<serial>0|[1-9]\d*))?"
|
||||
r"(?:\.post(?P<post_release>0|[1-9]\d*))?"
|
||||
r"(?:\.dev(?P<dev_release>0|[1-9]\d*))?"
|
||||
r"$",
|
||||
flags=_re.IGNORECASE,
|
||||
)
|
||||
_RELEASE_LEVELS: _ClassVar[_List[str]] = [ALPHA, BETA, RELEASE_CANDIDATE, FINAL]
|
||||
_SHORT_RELEASE_LEVELS: _ClassVar[_Dict[str, str]] = {
|
||||
"a": ALPHA,
|
||||
"b": BETA,
|
||||
"rc": RELEASE_CANDIDATE,
|
||||
}
|
||||
|
||||
self.releaselevel = releaselevel
|
||||
self.serial = serial
|
||||
def __init__(
|
||||
self,
|
||||
major: int,
|
||||
minor: int,
|
||||
micro: int,
|
||||
releaselevel: str,
|
||||
serial: _Optional[int] = None,
|
||||
post_release: _Optional[int] = None,
|
||||
dev_release: _Optional[int] = None,
|
||||
) -> None:
|
||||
self.major: int = major
|
||||
self.minor: int = minor
|
||||
self.micro: int = micro
|
||||
|
||||
def __lt__(self, other):
|
||||
my_index = self._levels.index(self.releaselevel)
|
||||
other_index = self._levels.index(other.releaselevel)
|
||||
return (self.major, self.minor, self.micro, my_index, self.serial) < (
|
||||
other.major,
|
||||
other.minor,
|
||||
other.micro,
|
||||
other_index,
|
||||
other.serial,
|
||||
if releaselevel not in self._RELEASE_LEVELS:
|
||||
raise TypeError(f"'releaselevel' must be one of: {', '.join(self._RELEASE_LEVELS)}")
|
||||
|
||||
self.releaselevel: str = releaselevel
|
||||
self.serial: _Optional[int] = serial
|
||||
self.post_release: _Optional[int] = post_release
|
||||
self.dev_release: _Optional[int] = dev_release
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, version_str: str) -> "VersionInfo":
|
||||
"""Parse a string into a VersionInfo object.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the version info string is invalid.
|
||||
|
||||
"""
|
||||
match = cls._VERSION_STR_PATTERN.match(version_str)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid version string: {version_str}")
|
||||
|
||||
kwargs: _Dict[str, _Union[str, int]] = {}
|
||||
for key in ("major", "minor", "micro"):
|
||||
kwargs[key] = int(match[key])
|
||||
releaselevel = match["releaselevel"]
|
||||
if releaselevel is not None:
|
||||
kwargs["releaselevel"] = cls._SHORT_RELEASE_LEVELS[releaselevel]
|
||||
else:
|
||||
kwargs["releaselevel"] = cls.FINAL
|
||||
for key in ("serial", "post_release", "dev_release"):
|
||||
if match[key] is not None:
|
||||
kwargs[key] = int(match[key])
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls, data: _Union[_Dict[str, _Union[int, str]], _List[_Union[int, str]]]
|
||||
) -> "VersionInfo":
|
||||
if isinstance(data, _List):
|
||||
# For old versions, data was stored as a list:
|
||||
# [MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL]
|
||||
return cls(*data)
|
||||
else:
|
||||
return cls(**data)
|
||||
|
||||
def to_json(self) -> _Dict[str, _Union[int, str]]:
|
||||
return {
|
||||
"major": self.major,
|
||||
"minor": self.minor,
|
||||
"micro": self.micro,
|
||||
"releaselevel": self.releaselevel,
|
||||
"serial": self.serial,
|
||||
"post_release": self.post_release,
|
||||
"dev_release": self.dev_release,
|
||||
}
|
||||
|
||||
def __lt__(self, other: _Any) -> bool:
|
||||
if not isinstance(other, VersionInfo):
|
||||
return NotImplemented
|
||||
tups: _List[_Tuple[int, int, int, int, int, int, int]] = []
|
||||
for obj in (self, other):
|
||||
tups.append(
|
||||
(
|
||||
obj.major,
|
||||
obj.minor,
|
||||
obj.micro,
|
||||
obj._RELEASE_LEVELS.index(obj.releaselevel),
|
||||
obj.serial if obj.serial is not None else _inf,
|
||||
obj.post_release if obj.post_release is not None else -_inf,
|
||||
obj.dev_release if obj.dev_release is not None else _inf,
|
||||
)
|
||||
)
|
||||
return tups[0] < tups[1]
|
||||
|
||||
def __str__(self) -> str:
|
||||
ret = f"{self.major}.{self.minor}.{self.micro}"
|
||||
if self.releaselevel != self.FINAL:
|
||||
short = next(
|
||||
k for k, v in self._SHORT_RELEASE_LEVELS.items() if v == self.releaselevel
|
||||
)
|
||||
ret += f"{short}{self.serial}"
|
||||
if self.post_release is not None:
|
||||
ret += f".post{self.post_release}"
|
||||
if self.dev_release is not None:
|
||||
ret += f".dev{self.dev_release}"
|
||||
return ret
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"VersionInfo(major={major}, minor={minor}, micro={micro}, "
|
||||
"releaselevel={releaselevel}, serial={serial}, post={post_release}, "
|
||||
"dev={dev_release})".format(**self.to_json())
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
|
||||
self.major, self.minor, self.micro, self.releaselevel, self.serial
|
||||
)
|
||||
|
||||
def to_json(self):
|
||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||
|
||||
|
||||
__version__ = "3.0.0rc1"
|
||||
version_info = VersionInfo(3, 0, 0, "release candidate", 1)
|
||||
__version__ = "3.0.0rc3.post1"
|
||||
version_info = VersionInfo.from_str(__version__)
|
||||
|
||||
@@ -4,9 +4,10 @@ from typing import Union, List, Optional
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import Config
|
||||
from . import Config, errors
|
||||
|
||||
__all__ = [
|
||||
"MAX_BALANCE",
|
||||
"Account",
|
||||
"get_balance",
|
||||
"set_balance",
|
||||
@@ -26,6 +27,8 @@ __all__ = [
|
||||
"set_default_balance",
|
||||
]
|
||||
|
||||
MAX_BALANCE = 2 ** 63 - 1
|
||||
|
||||
_DEFAULT_GLOBAL = {
|
||||
"is_global": False,
|
||||
"bank_name": "Twentysix bank",
|
||||
@@ -170,10 +173,22 @@ async def set_balance(member: discord.Member, amount: int) -> int:
|
||||
------
|
||||
ValueError
|
||||
If attempting to set the balance to a negative number.
|
||||
BalanceTooHigh
|
||||
If attempting to set the balance to a value greater than
|
||||
``bank.MAX_BALANCE``
|
||||
|
||||
"""
|
||||
if amount < 0:
|
||||
raise ValueError("Not allowed to have negative balance.")
|
||||
if amount > MAX_BALANCE:
|
||||
currency = (
|
||||
await get_currency_name()
|
||||
if await is_global()
|
||||
else await get_currency_name(member.guild)
|
||||
)
|
||||
raise errors.BalanceTooHigh(
|
||||
user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency
|
||||
)
|
||||
if await is_global():
|
||||
group = _conf.user(member)
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import logging
|
||||
from collections import Counter
|
||||
@@ -11,10 +12,10 @@ import discord
|
||||
import sys
|
||||
from discord.ext.commands import when_mentioned_or
|
||||
|
||||
from . import Config, i18n, commands, errors
|
||||
from .cog_manager import CogManager
|
||||
from . import Config, i18n, commands
|
||||
from .rpc import RPCMixin
|
||||
from .help_formatter import Help, help as help_
|
||||
from .rpc import RPCMixin
|
||||
from .sentry import SentryManager
|
||||
from .utils import common_filters
|
||||
|
||||
@@ -110,7 +111,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
|
||||
self.main_dir = bot_dir
|
||||
|
||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
|
||||
self.cog_mgr = CogManager()
|
||||
|
||||
super().__init__(*args, formatter=Help(), **kwargs)
|
||||
|
||||
@@ -185,13 +186,23 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
async def is_admin(self, member: discord.Member):
|
||||
"""Checks if a member is an admin of their guild."""
|
||||
admin_role = await self.db.guild(member.guild).admin_role()
|
||||
return any(role.id == admin_role for role in member.roles)
|
||||
try:
|
||||
if any(role.id == admin_role for role in member.roles):
|
||||
return True
|
||||
except AttributeError: # someone passed a webhook to this
|
||||
pass
|
||||
return False
|
||||
|
||||
async def is_mod(self, member: discord.Member):
|
||||
"""Checks if a member is a mod or admin of their guild."""
|
||||
mod_role = await self.db.guild(member.guild).mod_role()
|
||||
admin_role = await self.db.guild(member.guild).admin_role()
|
||||
return any(role.id in (mod_role, admin_role) for role in member.roles)
|
||||
try:
|
||||
if any(role.id in (mod_role, admin_role) for role in member.roles):
|
||||
return True
|
||||
except AttributeError: # someone passed a webhook to this
|
||||
pass
|
||||
return False
|
||||
|
||||
async def get_context(self, message, *, cls=commands.Context):
|
||||
return await super().get_context(message, cls=cls)
|
||||
@@ -217,7 +228,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
async def load_extension(self, spec: ModuleSpec):
|
||||
name = spec.name.split(".")[-1]
|
||||
if name in self.extensions:
|
||||
raise discord.ClientException(f"there is already a package named {name} loaded")
|
||||
raise errors.PackageAlreadyLoaded(spec)
|
||||
|
||||
lib = spec.loader.load_module()
|
||||
if not hasattr(lib, "setup"):
|
||||
@@ -236,20 +247,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
for when in ("before", "after"):
|
||||
for cls in inspect.getmro(cog.__class__):
|
||||
try:
|
||||
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}")
|
||||
hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.remove_permissions_hook(hook, when)
|
||||
|
||||
try:
|
||||
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before")
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.remove_permissions_hook(hook)
|
||||
self.remove_permissions_hook(hook)
|
||||
|
||||
super().remove_cog(cogname)
|
||||
|
||||
@@ -340,8 +344,14 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
ids_to_check = [to_check.id]
|
||||
else:
|
||||
author = getattr(to_check, "author", to_check)
|
||||
ids_to_check = [r.id for r in author.roles]
|
||||
ids_to_check.append(author.id)
|
||||
try:
|
||||
ids_to_check = [r.id for r in author.roles]
|
||||
except AttributeError:
|
||||
# webhook messages are a user not member,
|
||||
# cheaper than isinstance
|
||||
return True # webhooks require significant permissions to enable.
|
||||
else:
|
||||
ids_to_check.append(author.id)
|
||||
|
||||
immune_ids = await self.db.guild(guild).autoimmune_ids()
|
||||
|
||||
@@ -390,10 +400,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
||||
)
|
||||
if not hasattr(cog, "requires"):
|
||||
commands.Cog.__init__(cog)
|
||||
|
||||
for cls in inspect.getmro(cog.__class__):
|
||||
try:
|
||||
hook = getattr(cog, f"_{cls.__name__}__permissions_hook")
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.add_permissions_hook(hook)
|
||||
|
||||
for attr in dir(cog):
|
||||
_attr = getattr(cog, attr)
|
||||
if attr == f"_{cog.__class__.__name__}__permissions_hook":
|
||||
self.add_permissions_hook(_attr)
|
||||
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
|
||||
_attr, commands.Command
|
||||
):
|
||||
|
||||
@@ -3,7 +3,7 @@ import pkgutil
|
||||
from importlib import import_module, invalidate_caches
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Union, List, Optional
|
||||
from typing import Union, List, Optional
|
||||
|
||||
import redbot.cogs
|
||||
from redbot.core.utils import deduplicate_iterables
|
||||
@@ -25,8 +25,6 @@ class NoSuchCog(ImportError):
|
||||
Different from ImportError because some ImportErrors can happen inside cogs.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CogManager:
|
||||
"""Directory manager for Red's cogs.
|
||||
@@ -39,30 +37,27 @@ class CogManager:
|
||||
|
||||
CORE_PATH = Path(redbot.cogs.__path__[0])
|
||||
|
||||
def __init__(self, paths: Tuple[str] = ()):
|
||||
def __init__(self):
|
||||
self.conf = Config.get_conf(self, 2938473984732, True)
|
||||
tmp_cog_install_path = cog_data_path(self) / "cogs"
|
||||
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
|
||||
self.conf.register_global(paths=[], install_path=str(tmp_cog_install_path))
|
||||
self._paths = [Path(p) for p in paths]
|
||||
|
||||
async def paths(self) -> Tuple[Path, ...]:
|
||||
"""Get all currently valid path directories.
|
||||
async def paths(self) -> List[Path]:
|
||||
"""Get all currently valid path directories, in order of priority
|
||||
|
||||
Returns
|
||||
-------
|
||||
`tuple` of `pathlib.Path`
|
||||
All valid cog paths.
|
||||
List[pathlib.Path]
|
||||
A list of paths where cog packages can be found. The
|
||||
install path is highest priority, followed by the
|
||||
user-defined paths, and the core path has the lowest
|
||||
priority.
|
||||
|
||||
"""
|
||||
conf_paths = [Path(p) for p in await self.conf.paths()]
|
||||
other_paths = self._paths
|
||||
|
||||
all_paths = deduplicate_iterables(conf_paths, other_paths, [self.CORE_PATH])
|
||||
|
||||
if self.install_path not in all_paths:
|
||||
all_paths.insert(0, await self.install_path())
|
||||
return tuple(p.resolve() for p in all_paths if p.is_dir())
|
||||
return deduplicate_iterables(
|
||||
[await self.install_path()], await self.user_defined_paths(), [self.CORE_PATH]
|
||||
)
|
||||
|
||||
async def install_path(self) -> Path:
|
||||
"""Get the install path for 3rd party cogs.
|
||||
@@ -73,8 +68,20 @@ class CogManager:
|
||||
The path to the directory where 3rd party cogs are stored.
|
||||
|
||||
"""
|
||||
p = Path(await self.conf.install_path())
|
||||
return p.resolve()
|
||||
return Path(await self.conf.install_path()).resolve()
|
||||
|
||||
async def user_defined_paths(self) -> List[Path]:
|
||||
"""Get a list of user-defined cog paths.
|
||||
|
||||
All paths will be absolute and unique, in order of priority.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[pathlib.Path]
|
||||
A list of user-defined paths.
|
||||
|
||||
"""
|
||||
return list(map(Path, deduplicate_iterables(await self.conf.paths())))
|
||||
|
||||
async def set_install_path(self, path: Path) -> Path:
|
||||
"""Set the install path for 3rd party cogs.
|
||||
@@ -125,11 +132,10 @@ class CogManager:
|
||||
path = Path(path)
|
||||
return path
|
||||
|
||||
async def add_path(self, path: Union[Path, str]):
|
||||
async def add_path(self, path: Union[Path, str]) -> None:
|
||||
"""Add a cog path to current list.
|
||||
|
||||
This will ignore duplicates. Does have a side effect of removing all
|
||||
invalid paths from the saved path list.
|
||||
This will ignore duplicates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -156,11 +162,12 @@ class CogManager:
|
||||
if path == self.CORE_PATH:
|
||||
raise ValueError("Cannot add the core path as an additional path.")
|
||||
|
||||
async with self.conf.paths() as paths:
|
||||
if not any(Path(p) == path for p in paths):
|
||||
paths.append(str(path))
|
||||
current_paths = await self.user_defined_paths()
|
||||
if path not in current_paths:
|
||||
current_paths.append(path)
|
||||
await self.set_paths(current_paths)
|
||||
|
||||
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
|
||||
async def remove_path(self, path: Union[Path, str]) -> None:
|
||||
"""Remove a path from the current paths list.
|
||||
|
||||
Parameters
|
||||
@@ -168,20 +175,12 @@ class CogManager:
|
||||
path : `pathlib.Path` or `str`
|
||||
Path to remove.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`tuple` of `pathlib.Path`
|
||||
Tuple of new valid paths.
|
||||
|
||||
"""
|
||||
path = self._ensure_path_obj(path).resolve()
|
||||
paths = await self.user_defined_paths()
|
||||
|
||||
paths = [Path(p) for p in await self.conf.paths()]
|
||||
if path in paths:
|
||||
paths.remove(path)
|
||||
await self.set_paths(paths)
|
||||
|
||||
return tuple(paths)
|
||||
paths.remove(path)
|
||||
await self.set_paths(paths)
|
||||
|
||||
async def set_paths(self, paths_: List[Path]):
|
||||
"""Set the current paths list.
|
||||
@@ -192,7 +191,7 @@ class CogManager:
|
||||
List of paths to set.
|
||||
|
||||
"""
|
||||
str_paths = [str(p) for p in paths_]
|
||||
str_paths = list(map(str, paths_))
|
||||
await self.conf.paths.set(str_paths)
|
||||
|
||||
async def _find_ext_cog(self, name: str) -> ModuleSpec:
|
||||
@@ -213,9 +212,9 @@ class CogManager:
|
||||
------
|
||||
NoSuchCog
|
||||
When no cog with the requested name was found.
|
||||
|
||||
"""
|
||||
resolved_paths = await self.paths()
|
||||
real_paths = [str(p) for p in resolved_paths if p != self.CORE_PATH]
|
||||
real_paths = list(map(str, [await self.install_path()] + await self.user_defined_paths()))
|
||||
|
||||
for finder, module_name, _ in pkgutil.iter_modules(real_paths):
|
||||
if name == module_name:
|
||||
@@ -287,10 +286,8 @@ class CogManager:
|
||||
return await self._find_core_cog(name)
|
||||
|
||||
async def available_modules(self) -> List[str]:
|
||||
"""Finds the names of all available modules to load.
|
||||
"""
|
||||
paths = (await self.install_path(),) + await self.paths()
|
||||
paths = [str(p) for p in paths]
|
||||
"""Finds the names of all available modules to load."""
|
||||
paths = list(map(str, await self.paths()))
|
||||
|
||||
ret = []
|
||||
for finder, module_name, _ in pkgutil.iter_modules(paths):
|
||||
@@ -314,13 +311,6 @@ _ = Translator("CogManagerUI", __file__)
|
||||
class CogManagerUI(commands.Cog):
|
||||
"""Commands to interface with Red's cog manager."""
|
||||
|
||||
@staticmethod
|
||||
async def visible_paths(ctx):
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
cog_paths = await ctx.bot.cog_mgr.paths()
|
||||
cog_paths = [p for p in cog_paths if p != install_path]
|
||||
return cog_paths
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def paths(self, ctx: commands.Context):
|
||||
@@ -330,8 +320,7 @@ class CogManagerUI(commands.Cog):
|
||||
cog_mgr = ctx.bot.cog_mgr
|
||||
install_path = await cog_mgr.install_path()
|
||||
core_path = cog_mgr.CORE_PATH
|
||||
cog_paths = await cog_mgr.paths()
|
||||
cog_paths = [p for p in cog_paths if p not in (install_path, core_path)]
|
||||
cog_paths = await cog_mgr.user_defined_paths()
|
||||
|
||||
msg = _("Install Path: {install_path}\nCore Path: {core_path}\n\n").format(
|
||||
install_path=install_path, core_path=core_path
|
||||
@@ -369,7 +358,11 @@ class CogManagerUI(commands.Cog):
|
||||
from !paths
|
||||
"""
|
||||
path_number -= 1
|
||||
cog_paths = await self.visible_paths(ctx)
|
||||
if path_number < 0:
|
||||
await ctx.send(_("Path numbers must be positive."))
|
||||
return
|
||||
|
||||
cog_paths = await ctx.bot.cog_mgr.user_defined_paths()
|
||||
try:
|
||||
to_remove = cog_paths.pop(path_number)
|
||||
except IndexError:
|
||||
@@ -388,8 +381,11 @@ class CogManagerUI(commands.Cog):
|
||||
# Doing this because in the paths command they're 1 indexed
|
||||
from_ -= 1
|
||||
to -= 1
|
||||
if from_ < 0 or to < 0:
|
||||
await ctx.send(_("Path numbers must be positive."))
|
||||
return
|
||||
|
||||
all_paths = await self.visible_paths(ctx)
|
||||
all_paths = await ctx.bot.cog_mgr.user_defined_paths()
|
||||
try:
|
||||
to_move = all_paths.pop(from_)
|
||||
except IndexError:
|
||||
|
||||
@@ -145,7 +145,7 @@ class Command(CogCommandMixin, commands.Command):
|
||||
|
||||
@property
|
||||
def parents(self) -> List["Group"]:
|
||||
"""List[Group] : Returns all parent commands of this command.
|
||||
"""List[commands.Group] : Returns all parent commands of this command.
|
||||
|
||||
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
||||
If the command has no parents, this will be an empty list.
|
||||
@@ -157,12 +157,31 @@ class Command(CogCommandMixin, commands.Command):
|
||||
cmd = cmd.parent
|
||||
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
||||
|
||||
async def can_run(self, ctx: "Context") -> bool:
|
||||
# noinspection PyMethodOverriding
|
||||
async def can_run(
|
||||
self,
|
||||
ctx: "Context",
|
||||
*,
|
||||
check_all_parents: bool = False,
|
||||
change_permission_state: bool = False,
|
||||
) -> bool:
|
||||
"""Check if this command can be run in the given context.
|
||||
|
||||
This function first checks if the command can be run using
|
||||
discord.py's method `discord.ext.commands.Command.can_run`,
|
||||
then will return the result of `Requires.verify`.
|
||||
|
||||
Keyword Arguments
|
||||
-----------------
|
||||
check_all_parents : bool
|
||||
If ``True``, this will check permissions for all of this
|
||||
command's parents and its cog as well as the command
|
||||
itself. Defaults to ``False``.
|
||||
change_permission_state : bool
|
||||
Whether or not the permission state should be changed as
|
||||
a result of this call. For most cases this should be
|
||||
``False``. Defaults to ``False``.
|
||||
|
||||
"""
|
||||
ret = await super().can_run(ctx)
|
||||
if ret is False:
|
||||
@@ -171,8 +190,21 @@ class Command(CogCommandMixin, commands.Command):
|
||||
# This is so contexts invoking other commands can be checked with
|
||||
# this command as well
|
||||
original_command = ctx.command
|
||||
original_state = ctx.permission_state
|
||||
ctx.command = self
|
||||
|
||||
if check_all_parents is True:
|
||||
# Since we're starting from the beginning, we should reset the state to normal
|
||||
ctx.permission_state = PermState.NORMAL
|
||||
for parent in reversed(self.parents):
|
||||
try:
|
||||
result = await parent.can_run(ctx, change_permission_state=True)
|
||||
except commands.CommandError:
|
||||
result = False
|
||||
|
||||
if result is False:
|
||||
return False
|
||||
|
||||
if self.parent is None and self.instance is not None:
|
||||
# For top-level commands, we need to check the cog's requires too
|
||||
ret = await self.instance.requires.verify(ctx)
|
||||
@@ -183,6 +215,17 @@ class Command(CogCommandMixin, commands.Command):
|
||||
return await self.requires.verify(ctx)
|
||||
finally:
|
||||
ctx.command = original_command
|
||||
if not change_permission_state:
|
||||
ctx.permission_state = original_state
|
||||
|
||||
async def _verify_checks(self, ctx):
|
||||
if not self.enabled:
|
||||
raise commands.DisabledCommand(f"{self.name} command is disabled")
|
||||
|
||||
if not (await self.can_run(ctx, change_permission_state=True)):
|
||||
raise commands.CheckFailure(
|
||||
f"The check functions for command {self.qualified_name} failed."
|
||||
)
|
||||
|
||||
async def do_conversion(
|
||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||
@@ -238,7 +281,9 @@ class Command(CogCommandMixin, commands.Command):
|
||||
if cmd.hidden:
|
||||
return False
|
||||
try:
|
||||
can_run = await self.can_run(ctx)
|
||||
can_run = await self.can_run(
|
||||
ctx, check_all_parents=True, change_permission_state=False
|
||||
)
|
||||
except commands.CheckFailure:
|
||||
return False
|
||||
else:
|
||||
|
||||
@@ -281,12 +281,14 @@ class Requires:
|
||||
|
||||
if isinstance(user_perms, dict):
|
||||
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
|
||||
_validate_perms_dict(user_perms)
|
||||
self.user_perms.update(**user_perms)
|
||||
else:
|
||||
self.user_perms = user_perms
|
||||
|
||||
if isinstance(bot_perms, dict):
|
||||
self.bot_perms: discord.Permissions = discord.Permissions.none()
|
||||
_validate_perms_dict(bot_perms)
|
||||
self.bot_perms.update(**bot_perms)
|
||||
else:
|
||||
self.bot_perms = bot_perms
|
||||
@@ -311,6 +313,7 @@ class Requires:
|
||||
if user_perms is None:
|
||||
func.requires.user_perms = None
|
||||
else:
|
||||
_validate_perms_dict(user_perms)
|
||||
func.requires.user_perms.update(**user_perms)
|
||||
return func
|
||||
|
||||
@@ -417,9 +420,13 @@ class Requires:
|
||||
|
||||
"""
|
||||
await self._verify_bot(ctx)
|
||||
# Owner-only commands are non-overrideable
|
||||
|
||||
# Owner should never be locked out of commands for user permissions.
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return True
|
||||
# Owner-only commands are non-overrideable, and we already checked for owner.
|
||||
if self.privilege_level is PrivilegeLevel.BOT_OWNER:
|
||||
return await ctx.bot.is_owner(ctx.author)
|
||||
return False
|
||||
|
||||
hook_result = await ctx.bot.verify_permissions_hooks(ctx)
|
||||
if hook_result is not None:
|
||||
@@ -445,7 +452,20 @@ class Requires:
|
||||
should_invoke = await self._verify_user(ctx)
|
||||
elif isinstance(next_state, dict):
|
||||
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
|
||||
next_state = next_state[await self._verify_user(ctx)]
|
||||
# We must check what would happen normally, if no explicit rules were set.
|
||||
default_rule = PermState.NORMAL
|
||||
if ctx.guild is not None:
|
||||
default_rule = self.get_default_guild_rule(guild_id=ctx.guild.id)
|
||||
if default_rule is PermState.NORMAL:
|
||||
default_rule = self.default_global_rule
|
||||
|
||||
if default_rule == PermState.ACTIVE_DENY:
|
||||
would_invoke = False
|
||||
elif default_rule == PermState.ACTIVE_ALLOW:
|
||||
would_invoke = True
|
||||
else:
|
||||
would_invoke = await self._verify_user(ctx)
|
||||
next_state = next_state[would_invoke]
|
||||
|
||||
ctx.permission_state = next_state
|
||||
return should_invoke
|
||||
@@ -584,6 +604,7 @@ def bot_has_permissions(**perms: bool):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
func.__requires_bot_perms__ = perms
|
||||
else:
|
||||
_validate_perms_dict(perms)
|
||||
func.requires.bot_perms.update(**perms)
|
||||
return func
|
||||
|
||||
@@ -595,6 +616,8 @@ def has_permissions(**perms: bool):
|
||||
|
||||
This check can be overridden by rules.
|
||||
"""
|
||||
if perms is None:
|
||||
raise TypeError("Must provide at least one keyword argument to has_permissions")
|
||||
return Requires.get_decorator(None, perms)
|
||||
|
||||
|
||||
@@ -666,3 +689,20 @@ class _IntKeyDict(Dict[int, _T]):
|
||||
if not isinstance(key, int):
|
||||
raise TypeError("Keys must be of type `int`")
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
|
||||
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
|
||||
for perm, value in perms.items():
|
||||
try:
|
||||
attr = getattr(discord.Permissions, perm)
|
||||
except AttributeError:
|
||||
attr = None
|
||||
|
||||
if attr is None or not isinstance(attr, property):
|
||||
# We reject invalid permissions
|
||||
raise TypeError(f"Unknown permission name '{perm}'")
|
||||
|
||||
if value is not True:
|
||||
# We reject any permission not specified as 'True', since this is the only value which
|
||||
# makes practical sense.
|
||||
raise TypeError(f"Permission {perm} may only be specified as 'True', not {value}")
|
||||
|
||||
@@ -39,17 +39,21 @@ class _ValueCtxManager(Awaitable[_T], AsyncContextManager[_T]):
|
||||
|
||||
async def __aenter__(self):
|
||||
self.raw_value = await self
|
||||
self.__original_value = deepcopy(self.raw_value)
|
||||
if not isinstance(self.raw_value, (list, dict)):
|
||||
raise TypeError(
|
||||
"Type of retrieved value must be mutable (i.e. "
|
||||
"list or dict) in order to use a config value as "
|
||||
"a context manager."
|
||||
)
|
||||
self.__original_value = deepcopy(self.raw_value)
|
||||
return self.raw_value
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
if self.raw_value != self.__original_value:
|
||||
if isinstance(self.raw_value, dict):
|
||||
raw_value = _str_key_dict(self.raw_value)
|
||||
else:
|
||||
raw_value = self.raw_value
|
||||
if raw_value != self.__original_value:
|
||||
await self.value_obj.set(self.raw_value)
|
||||
|
||||
|
||||
@@ -58,7 +62,7 @@ class Value:
|
||||
|
||||
Attributes
|
||||
----------
|
||||
identifiers : `tuple` of `str`
|
||||
identifiers : Tuple[str]
|
||||
This attribute provides all the keys necessary to get a specific data
|
||||
element from a json document.
|
||||
default
|
||||
@@ -69,15 +73,10 @@ class Value:
|
||||
"""
|
||||
|
||||
def __init__(self, identifiers: Tuple[str], default_value, driver):
|
||||
self._identifiers = identifiers
|
||||
self.identifiers = identifiers
|
||||
self.default = default_value
|
||||
|
||||
self.driver = driver
|
||||
|
||||
@property
|
||||
def identifiers(self):
|
||||
return tuple(str(i) for i in self._identifiers)
|
||||
|
||||
async def _get(self, default=...):
|
||||
try:
|
||||
ret = await self.driver.get(*self.identifiers)
|
||||
@@ -149,6 +148,8 @@ class Value:
|
||||
The new literal value of this attribute.
|
||||
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
value = _str_key_dict(value)
|
||||
await self.driver.set(*self.identifiers, value=value)
|
||||
|
||||
async def clear(self):
|
||||
@@ -192,7 +193,10 @@ class Group(Value):
|
||||
async def _get(self, default: Dict[str, Any] = ...) -> Dict[str, Any]:
|
||||
default = default if default is not ... else self.defaults
|
||||
raw = await super()._get(default)
|
||||
return self.nested_update(raw, default)
|
||||
if isinstance(raw, dict):
|
||||
return self.nested_update(raw, default)
|
||||
else:
|
||||
return raw
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def __getattr__(self, item: str) -> Union["Group", Value]:
|
||||
@@ -238,7 +242,7 @@ class Group(Value):
|
||||
else:
|
||||
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
||||
|
||||
async def clear_raw(self, *nested_path: str):
|
||||
async def clear_raw(self, *nested_path: Any):
|
||||
"""
|
||||
Allows a developer to clear data as if it was stored in a standard
|
||||
Python dictionary.
|
||||
@@ -254,44 +258,44 @@ class Group(Value):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nested_path : str
|
||||
nested_path : Any
|
||||
Multiple arguments that mirror the arguments passed in for nested
|
||||
dict access.
|
||||
dict access. These are casted to `str` for you.
|
||||
"""
|
||||
path = [str(p) for p in nested_path]
|
||||
await self.driver.clear(*self.identifiers, *path)
|
||||
|
||||
def is_group(self, item: str) -> bool:
|
||||
def is_group(self, item: Any) -> bool:
|
||||
"""A helper method for `__getattr__`. Most developers will have no need
|
||||
to use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item : str
|
||||
item : Any
|
||||
See `__getattr__`.
|
||||
|
||||
"""
|
||||
default = self._defaults.get(item)
|
||||
default = self._defaults.get(str(item))
|
||||
return isinstance(default, dict)
|
||||
|
||||
def is_value(self, item: str) -> bool:
|
||||
def is_value(self, item: Any) -> bool:
|
||||
"""A helper method for `__getattr__`. Most developers will have no need
|
||||
to use this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item : str
|
||||
item : Any
|
||||
See `__getattr__`.
|
||||
|
||||
"""
|
||||
try:
|
||||
default = self._defaults[item]
|
||||
default = self._defaults[str(item)]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return not isinstance(default, dict)
|
||||
|
||||
def get_attr(self, item: str):
|
||||
def get_attr(self, item: Union[int, str]):
|
||||
"""Manually get an attribute of this Group.
|
||||
|
||||
This is available to use as an alternative to using normal Python
|
||||
@@ -312,7 +316,8 @@ class Group(Value):
|
||||
Parameters
|
||||
----------
|
||||
item : str
|
||||
The name of the data field in `Config`.
|
||||
The name of the data field in `Config`. This is casted to
|
||||
`str` for you.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -320,9 +325,11 @@ class Group(Value):
|
||||
The attribute which was requested.
|
||||
|
||||
"""
|
||||
if isinstance(item, int):
|
||||
item = str(item)
|
||||
return self.__getattr__(item)
|
||||
|
||||
async def get_raw(self, *nested_path: str, default=...):
|
||||
async def get_raw(self, *nested_path: Any, default=...):
|
||||
"""
|
||||
Allows a developer to access data as if it was stored in a standard
|
||||
Python dictionary.
|
||||
@@ -345,7 +352,7 @@ class Group(Value):
|
||||
----------
|
||||
nested_path : str
|
||||
Multiple arguments that mirror the arguments passed in for nested
|
||||
dict access.
|
||||
dict access. These are casted to `str` for you.
|
||||
default
|
||||
Default argument for the value attempting to be accessed. If the
|
||||
value does not exist the default will be returned.
|
||||
@@ -410,7 +417,6 @@ class Group(Value):
|
||||
|
||||
If no defaults are passed, then the instance attribute 'defaults'
|
||||
will be used.
|
||||
|
||||
"""
|
||||
if defaults is ...:
|
||||
defaults = self.defaults
|
||||
@@ -428,7 +434,7 @@ class Group(Value):
|
||||
raise ValueError("You may only set the value of a group to be a dict.")
|
||||
await super().set(value)
|
||||
|
||||
async def set_raw(self, *nested_path: str, value):
|
||||
async def set_raw(self, *nested_path: Any, value):
|
||||
"""
|
||||
Allows a developer to set data as if it was stored in a standard
|
||||
Python dictionary.
|
||||
@@ -444,13 +450,15 @@ class Group(Value):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nested_path : str
|
||||
nested_path : Any
|
||||
Multiple arguments that mirror the arguments passed in for nested
|
||||
dict access.
|
||||
`dict` access. These are casted to `str` for you.
|
||||
value
|
||||
The value to store.
|
||||
"""
|
||||
path = [str(p) for p in nested_path]
|
||||
if isinstance(value, dict):
|
||||
value = _str_key_dict(value)
|
||||
await self.driver.set(*self.identifiers, *path, value=value)
|
||||
|
||||
|
||||
@@ -461,9 +469,11 @@ class Config:
|
||||
`get_core_conf` for Config used in the core package.
|
||||
|
||||
.. important::
|
||||
Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`)
|
||||
however the process for accessing global data is a bit different. There is no :python:`global` method
|
||||
because global data is accessed by normal attribute access::
|
||||
Most config data should be accessed through its respective
|
||||
group method (e.g. :py:meth:`guild`) however the process for
|
||||
accessing global data is a bit different. There is no
|
||||
:python:`global` method because global data is accessed by
|
||||
normal attribute access::
|
||||
|
||||
await conf.foo()
|
||||
|
||||
@@ -548,7 +558,7 @@ class Config:
|
||||
A new Config object.
|
||||
|
||||
"""
|
||||
if cog_instance is None and not cog_name is None:
|
||||
if cog_instance is None and cog_name is not None:
|
||||
cog_path_override = cog_data_path(raw_name=cog_name)
|
||||
else:
|
||||
cog_path_override = cog_data_path(cog_instance=cog_instance)
|
||||
@@ -635,11 +645,8 @@ class Config:
|
||||
def _get_defaults_dict(key: str, value) -> dict:
|
||||
"""
|
||||
Since we're allowing nested config stuff now, not storing the
|
||||
_defaults as a flat dict sounds like a good idea. May turn
|
||||
out to be an awful one but we'll see.
|
||||
:param key:
|
||||
:param value:
|
||||
:return:
|
||||
_defaults as a flat dict sounds like a good idea. May turn out
|
||||
to be an awful one but we'll see.
|
||||
"""
|
||||
ret = {}
|
||||
partial = ret
|
||||
@@ -655,15 +662,12 @@ class Config:
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def _update_defaults(to_add: dict, _partial: dict):
|
||||
def _update_defaults(to_add: Dict[str, Any], _partial: Dict[str, Any]):
|
||||
"""
|
||||
This tries to update the _defaults dictionary with the nested
|
||||
partial dict generated by _get_defaults_dict. This WILL
|
||||
throw an error if you try to have both a value and a group
|
||||
registered under the same name.
|
||||
:param to_add:
|
||||
:param _partial:
|
||||
:return:
|
||||
partial dict generated by _get_defaults_dict. This WILL
|
||||
throw an error if you try to have both a value and a group
|
||||
registered under the same name.
|
||||
"""
|
||||
for k, v in to_add.items():
|
||||
val_is_dict = isinstance(v, dict)
|
||||
@@ -679,7 +683,7 @@ class Config:
|
||||
else:
|
||||
_partial[k] = v
|
||||
|
||||
def _register_default(self, key: str, **kwargs):
|
||||
def _register_default(self, key: str, **kwargs: Any):
|
||||
if key not in self._defaults:
|
||||
self._defaults[key] = {}
|
||||
|
||||
@@ -720,8 +724,8 @@ class Config:
|
||||
**_defaults
|
||||
)
|
||||
|
||||
You can do the same thing without a :python:`_defaults` dict by using double underscore as a variable
|
||||
name separator::
|
||||
You can do the same thing without a :python:`_defaults` dict by
|
||||
using double underscore as a variable name separator::
|
||||
|
||||
# This is equivalent to the previous example
|
||||
conf.register_global(
|
||||
@@ -802,7 +806,7 @@ class Config:
|
||||
The guild's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(self.GUILD, guild.id)
|
||||
return self._get_base_group(self.GUILD, str(guild.id))
|
||||
|
||||
def channel(self, channel: discord.TextChannel) -> Group:
|
||||
"""Returns a `Group` for the given channel.
|
||||
@@ -820,7 +824,7 @@ class Config:
|
||||
The channel's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(self.CHANNEL, channel.id)
|
||||
return self._get_base_group(self.CHANNEL, str(channel.id))
|
||||
|
||||
def role(self, role: discord.Role) -> Group:
|
||||
"""Returns a `Group` for the given role.
|
||||
@@ -836,7 +840,7 @@ class Config:
|
||||
The role's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(self.ROLE, role.id)
|
||||
return self._get_base_group(self.ROLE, str(role.id))
|
||||
|
||||
def user(self, user: discord.abc.User) -> Group:
|
||||
"""Returns a `Group` for the given user.
|
||||
@@ -852,7 +856,7 @@ class Config:
|
||||
The user's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(self.USER, user.id)
|
||||
return self._get_base_group(self.USER, str(user.id))
|
||||
|
||||
def member(self, member: discord.Member) -> Group:
|
||||
"""Returns a `Group` for the given member.
|
||||
@@ -866,8 +870,9 @@ class Config:
|
||||
-------
|
||||
`Group <redbot.core.config.Group>`
|
||||
The member's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(self.MEMBER, member.guild.id, member.id)
|
||||
return self._get_base_group(self.MEMBER, str(member.guild.id), str(member.id))
|
||||
|
||||
def custom(self, group_identifier: str, *identifiers: str):
|
||||
"""Returns a `Group` for the given custom group.
|
||||
@@ -876,17 +881,17 @@ class Config:
|
||||
----------
|
||||
group_identifier : str
|
||||
Used to identify the custom group.
|
||||
|
||||
identifiers : str
|
||||
The attributes necessary to uniquely identify an entry in the
|
||||
custom group.
|
||||
custom group. These are casted to `str` for you.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Group <redbot.core.config.Group>`
|
||||
The custom group's Group object.
|
||||
|
||||
"""
|
||||
return self._get_base_group(group_identifier, *identifiers)
|
||||
return self._get_base_group(str(group_identifier), *map(str, identifiers))
|
||||
|
||||
async def _all_from_scope(self, scope: str) -> Dict[int, Dict[Any, Any]]:
|
||||
"""Get a dict of all values from a particular scope of data.
|
||||
@@ -982,7 +987,8 @@ class Config:
|
||||
"""
|
||||
return await self._all_from_scope(self.USER)
|
||||
|
||||
def _all_members_from_guild(self, group: Group, guild_data: dict) -> dict:
|
||||
@staticmethod
|
||||
def _all_members_from_guild(group: Group, guild_data: dict) -> dict:
|
||||
ret = {}
|
||||
for member_id, member_data in guild_data.items():
|
||||
new_member_data = group.defaults
|
||||
@@ -1026,7 +1032,7 @@ class Config:
|
||||
for guild_id, guild_data in dict_.items():
|
||||
ret[int(guild_id)] = self._all_members_from_guild(group, guild_data)
|
||||
else:
|
||||
group = self._get_base_group(self.MEMBER, guild.id)
|
||||
group = self._get_base_group(self.MEMBER, str(guild.id))
|
||||
try:
|
||||
guild_data = await self.driver.get(*group.identifiers)
|
||||
except KeyError:
|
||||
@@ -1054,7 +1060,8 @@ class Config:
|
||||
|
||||
"""
|
||||
if not scopes:
|
||||
group = Group(identifiers=[], defaults={}, driver=self.driver)
|
||||
# noinspection PyTypeChecker
|
||||
group = Group(identifiers=(), defaults={}, driver=self.driver)
|
||||
else:
|
||||
group = self._get_base_group(*scopes)
|
||||
await group.clear()
|
||||
@@ -1119,7 +1126,7 @@ class Config:
|
||||
|
||||
"""
|
||||
if guild is not None:
|
||||
await self._clear_scope(self.MEMBER, guild.id)
|
||||
await self._clear_scope(self.MEMBER, str(guild.id))
|
||||
return
|
||||
await self._clear_scope(self.MEMBER)
|
||||
|
||||
@@ -1127,5 +1134,34 @@ class Config:
|
||||
"""Clear all custom group data.
|
||||
|
||||
This resets all custom group data to its registered defaults.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
group_identifier : str
|
||||
The identifier for the custom group. This is casted to
|
||||
`str` for you.
|
||||
"""
|
||||
await self._clear_scope(group_identifier)
|
||||
await self._clear_scope(str(group_identifier))
|
||||
|
||||
|
||||
def _str_key_dict(value: Dict[Any, _T]) -> Dict[str, _T]:
|
||||
"""
|
||||
Recursively casts all keys in the given `dict` to `str`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : Dict[Any, Any]
|
||||
The `dict` to cast keys to `str`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Any]
|
||||
The `dict` with keys (and nested keys) casted to `str`.
|
||||
|
||||
"""
|
||||
ret = {}
|
||||
for k, v in value.items():
|
||||
if isinstance(v, dict):
|
||||
v = _str_key_dict(v)
|
||||
ret[str(k)] = v
|
||||
return ret
|
||||
|
||||
@@ -13,17 +13,21 @@ from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from random import SystemRandom
|
||||
from string import ascii_letters, digits
|
||||
from distutils.version import StrictVersion
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
import pkg_resources
|
||||
|
||||
from redbot.core import __version__
|
||||
from redbot.core import checks
|
||||
from redbot.core import i18n
|
||||
from redbot.core import commands
|
||||
from redbot.core import (
|
||||
__version__,
|
||||
version_info as red_version_info,
|
||||
VersionInfo,
|
||||
checks,
|
||||
commands,
|
||||
errors,
|
||||
i18n,
|
||||
)
|
||||
from .utils.predicates import MessagePredicate
|
||||
from .utils.chat_formatting import pagify, box, inline
|
||||
|
||||
@@ -56,7 +60,9 @@ class CoreLogic:
|
||||
self.bot.register_rpc_handler(self._version_info)
|
||||
self.bot.register_rpc_handler(self._invite_url)
|
||||
|
||||
async def _load(self, cog_names: list):
|
||||
async def _load(
|
||||
self, cog_names: Iterable[str]
|
||||
) -> Tuple[List[str], List[str], List[str], List[str]]:
|
||||
"""
|
||||
Loads cogs by name.
|
||||
Parameters
|
||||
@@ -66,11 +72,12 @@ class CoreLogic:
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
3 element tuple of loaded, failed, and not found cogs.
|
||||
4-tuple of loaded, failed, not found and already loaded cogs.
|
||||
"""
|
||||
failed_packages = []
|
||||
loaded_packages = []
|
||||
notfound_packages = []
|
||||
alreadyloaded_packages = []
|
||||
|
||||
bot = self.bot
|
||||
|
||||
@@ -95,6 +102,8 @@ class CoreLogic:
|
||||
try:
|
||||
self._cleanup_and_refresh_modules(spec.name)
|
||||
await bot.load_extension(spec)
|
||||
except errors.PackageAlreadyLoaded:
|
||||
alreadyloaded_packages.append(name)
|
||||
except Exception as e:
|
||||
log.exception("Package loading failed", exc_info=e)
|
||||
|
||||
@@ -106,9 +115,10 @@ class CoreLogic:
|
||||
await bot.add_loaded_package(name)
|
||||
loaded_packages.append(name)
|
||||
|
||||
return loaded_packages, failed_packages, notfound_packages
|
||||
return loaded_packages, failed_packages, notfound_packages, alreadyloaded_packages
|
||||
|
||||
def _cleanup_and_refresh_modules(self, module_name: str):
|
||||
@staticmethod
|
||||
def _cleanup_and_refresh_modules(module_name: str) -> None:
|
||||
"""Interally reloads modules so that changes are detected"""
|
||||
splitted = module_name.split(".")
|
||||
|
||||
@@ -120,6 +130,7 @@ class CoreLogic:
|
||||
else:
|
||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
modules = itertools.accumulate(splitted, "{}.{}".format)
|
||||
for m in modules:
|
||||
maybe_reload(m)
|
||||
@@ -128,7 +139,10 @@ class CoreLogic:
|
||||
for child_name, lib in children.items():
|
||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||
|
||||
def _get_package_strings(self, packages: list, fmt: str, other: tuple = None):
|
||||
@staticmethod
|
||||
def _get_package_strings(
|
||||
packages: List[str], fmt: str, other: Optional[Tuple[str, ...]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Gets the strings needed for the load, unload and reload commands
|
||||
"""
|
||||
@@ -144,7 +158,7 @@ class CoreLogic:
|
||||
final_string = fmt.format(**form)
|
||||
return final_string
|
||||
|
||||
async def _unload(self, cog_names: list):
|
||||
async def _unload(self, cog_names: Iterable[str]) -> Tuple[List[str], List[str]]:
|
||||
"""
|
||||
Unloads cogs with the given names.
|
||||
|
||||
@@ -172,14 +186,16 @@ class CoreLogic:
|
||||
|
||||
return unloaded_packages, failed_packages
|
||||
|
||||
async def _reload(self, cog_names):
|
||||
async def _reload(
|
||||
self, cog_names: Sequence[str]
|
||||
) -> Tuple[List[str], List[str], List[str], List[str]]:
|
||||
await self._unload(cog_names)
|
||||
|
||||
loaded, load_failed, not_found = await self._load(cog_names)
|
||||
loaded, load_failed, not_found, already_loaded = await self._load(cog_names)
|
||||
|
||||
return loaded, load_failed, not_found
|
||||
return loaded, load_failed, not_found, already_loaded
|
||||
|
||||
async def _name(self, name: str = None):
|
||||
async def _name(self, name: Optional[str] = None) -> str:
|
||||
"""
|
||||
Gets or sets the bot's username.
|
||||
|
||||
@@ -198,7 +214,7 @@ class CoreLogic:
|
||||
|
||||
return self.bot.user.name
|
||||
|
||||
async def _prefixes(self, prefixes: list = None):
|
||||
async def _prefixes(self, prefixes: Optional[Sequence[str]] = None) -> List[str]:
|
||||
"""
|
||||
Gets or sets the bot's global prefixes.
|
||||
|
||||
@@ -217,7 +233,8 @@ class CoreLogic:
|
||||
await self.bot.db.prefix.set(prefixes)
|
||||
return await self.bot.db.prefix()
|
||||
|
||||
async def _version_info(self):
|
||||
@classmethod
|
||||
async def _version_info(cls) -> Dict[str, str]:
|
||||
"""
|
||||
Version information for Red and discord.py
|
||||
|
||||
@@ -228,7 +245,7 @@ class CoreLogic:
|
||||
"""
|
||||
return {"redbot": __version__, "discordpy": discord.__version__}
|
||||
|
||||
async def _invite_url(self):
|
||||
async def _invite_url(self) -> str:
|
||||
"""
|
||||
Generates the invite URL for the bot.
|
||||
|
||||
@@ -245,11 +262,8 @@ class CoreLogic:
|
||||
class Core(commands.Cog, CoreLogic):
|
||||
"""Commands related to core functions"""
|
||||
|
||||
def __init__(self, bot):
|
||||
super().__init__(bot)
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
async def ping(self, ctx: commands.Context):
|
||||
"""Pong."""
|
||||
await ctx.send("Pong.")
|
||||
|
||||
@@ -274,7 +288,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("{}/json".format(red_pypi)) as r:
|
||||
data = await r.json()
|
||||
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
||||
outdated = VersionInfo.from_str(data["info"]["version"]) > red_version_info
|
||||
about = (
|
||||
"This is an instance of [Red, an open source Discord bot]({}) "
|
||||
"created by [Twentysix]({}) and [improved by many]({}).\n\n"
|
||||
@@ -310,7 +324,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
passed = self.get_bot_uptime()
|
||||
await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since))
|
||||
|
||||
def get_bot_uptime(self, *, brief=False):
|
||||
def get_bot_uptime(self, *, brief: bool = False):
|
||||
# Courtesy of Danny
|
||||
now = datetime.datetime.utcnow()
|
||||
delta = now - self.bot.uptime
|
||||
@@ -413,7 +427,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def traceback(self, ctx, public: bool = False):
|
||||
async def traceback(self, ctx: commands.Context, public: bool = False):
|
||||
"""Sends to the owner the last command exception that has occurred
|
||||
|
||||
If public (yes is specified), it will be sent to the chat instead"""
|
||||
@@ -430,20 +444,20 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def invite(self, ctx):
|
||||
async def invite(self, ctx: commands.Context):
|
||||
"""Show's Red's invite url"""
|
||||
await ctx.author.send(await self._invite_url())
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.is_owner()
|
||||
async def leave(self, ctx):
|
||||
async def leave(self, ctx: commands.Context):
|
||||
"""Leaves server"""
|
||||
await ctx.send("Are you sure you want me to leave this server? (y/n)")
|
||||
|
||||
pred = MessagePredicate.yes_or_no(ctx)
|
||||
try:
|
||||
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
|
||||
await self.bot.wait_for("message", check=pred)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Response timed out.")
|
||||
return
|
||||
@@ -457,7 +471,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def servers(self, ctx):
|
||||
async def servers(self, ctx: commands.Context):
|
||||
"""Lists and allows to leave servers"""
|
||||
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
|
||||
msg = ""
|
||||
@@ -499,18 +513,21 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def load(self, ctx, *, cog_name: str):
|
||||
async def load(self, ctx: commands.Context, *cogs: str):
|
||||
"""Loads packages"""
|
||||
|
||||
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||
async with ctx.typing():
|
||||
loaded, failed, not_found = await self._load(cog_names)
|
||||
loaded, failed, not_found, already_loaded = await self._load(cogs)
|
||||
|
||||
if loaded:
|
||||
fmt = "Loaded {packs}."
|
||||
formed = self._get_package_strings(loaded, fmt)
|
||||
await ctx.send(formed)
|
||||
|
||||
if already_loaded:
|
||||
fmt = "The package{plural} {packs} {other} already loaded."
|
||||
formed = self._get_package_strings(already_loaded, fmt, ("is", "are"))
|
||||
await ctx.send(formed)
|
||||
|
||||
if failed:
|
||||
fmt = (
|
||||
"Failed to load package{plural} {packs}. Check your console or "
|
||||
@@ -526,12 +543,9 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def unload(self, ctx, *, cog_name: str):
|
||||
async def unload(self, ctx: commands.Context, *cogs: str):
|
||||
"""Unloads packages"""
|
||||
|
||||
cog_names = [c.strip() for c in cog_name.split(" ")]
|
||||
|
||||
unloaded, failed = await self._unload(cog_names)
|
||||
unloaded, failed = await self._unload(cogs)
|
||||
|
||||
if unloaded:
|
||||
fmt = "Package{plural} {packs} {other} unloaded."
|
||||
@@ -545,10 +559,10 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
async def reload(self, ctx, *cogs: str):
|
||||
async def reload(self, ctx: commands.Context, *cogs: str):
|
||||
"""Reloads packages"""
|
||||
async with ctx.typing():
|
||||
loaded, failed, not_found = await self._reload(cogs)
|
||||
loaded, failed, not_found, already_loaded = await self._reload(cogs)
|
||||
|
||||
if loaded:
|
||||
fmt = "Package{plural} {packs} {other} reloaded."
|
||||
@@ -567,34 +581,30 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command(name="shutdown")
|
||||
@checks.is_owner()
|
||||
async def _shutdown(self, ctx, silently: bool = False):
|
||||
async def _shutdown(self, ctx: commands.Context, silently: bool = False):
|
||||
"""Shuts down the bot"""
|
||||
wave = "\N{WAVING HAND SIGN}"
|
||||
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
||||
try: # We don't want missing perms to stop our shutdown
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
if not silently:
|
||||
await ctx.send(_("Shutting down... ") + wave + skin)
|
||||
except:
|
||||
pass
|
||||
await ctx.bot.shutdown()
|
||||
|
||||
@commands.command(name="restart")
|
||||
@checks.is_owner()
|
||||
async def _restart(self, ctx, silently: bool = False):
|
||||
async def _restart(self, ctx: commands.Context, silently: bool = False):
|
||||
"""Attempts to restart Red
|
||||
|
||||
Makes Red quit with exit code 26
|
||||
The restart is not guaranteed: it must be dealt
|
||||
with by the process manager in use"""
|
||||
try:
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
if not silently:
|
||||
await ctx.send(_("Restarting..."))
|
||||
except:
|
||||
pass
|
||||
await ctx.bot.shutdown(restart=True)
|
||||
|
||||
@commands.group(name="set")
|
||||
async def _set(self, ctx):
|
||||
async def _set(self, ctx: commands.Context):
|
||||
"""Changes Red's settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
if ctx.guild:
|
||||
@@ -626,7 +636,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def adminrole(self, ctx, *, role: discord.Role):
|
||||
async def adminrole(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""Sets the admin role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id)
|
||||
await ctx.send(_("The admin role for this guild has been set."))
|
||||
@@ -634,7 +644,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def modrole(self, ctx, *, role: discord.Role):
|
||||
async def modrole(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""Sets the mod role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
|
||||
await ctx.send(_("The mod role for this guild has been set."))
|
||||
@@ -642,7 +652,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(aliases=["usebotcolor"])
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def usebotcolour(self, ctx):
|
||||
async def usebotcolour(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggle whether to use the bot owner-configured colour for embeds.
|
||||
|
||||
@@ -660,7 +670,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def serverfuzzy(self, ctx):
|
||||
async def serverfuzzy(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggle whether to enable fuzzy command search for the server.
|
||||
|
||||
@@ -676,7 +686,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def fuzzy(self, ctx):
|
||||
async def fuzzy(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggle whether to enable fuzzy command search in DMs.
|
||||
|
||||
@@ -692,7 +702,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command(aliases=["color"])
|
||||
@checks.is_owner()
|
||||
async def colour(self, ctx, *, colour: discord.Colour = None):
|
||||
async def colour(self, ctx: commands.Context, *, colour: discord.Colour = None):
|
||||
"""
|
||||
Sets a default colour to be used for the bot's embeds.
|
||||
|
||||
@@ -710,7 +720,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def avatar(self, ctx, url: str):
|
||||
async def avatar(self, ctx: commands.Context, url: str):
|
||||
"""Sets Red's avatar"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as r:
|
||||
@@ -734,7 +744,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(name="game")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _game(self, ctx, *, game: str = None):
|
||||
async def _game(self, ctx: commands.Context, *, game: str = None):
|
||||
"""Sets Red's playing status"""
|
||||
|
||||
if game:
|
||||
@@ -748,7 +758,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(name="listening")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _listening(self, ctx, *, listening: str = None):
|
||||
async def _listening(self, ctx: commands.Context, *, listening: str = None):
|
||||
"""Sets Red's listening status"""
|
||||
|
||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||
@@ -762,7 +772,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(name="watching")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _watching(self, ctx, *, watching: str = None):
|
||||
async def _watching(self, ctx: commands.Context, *, watching: str = None):
|
||||
"""Sets Red's watching status"""
|
||||
|
||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
|
||||
@@ -776,7 +786,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command()
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def status(self, ctx, *, status: str):
|
||||
async def status(self, ctx: commands.Context, *, status: str):
|
||||
"""Sets Red's status
|
||||
|
||||
Available statuses:
|
||||
@@ -805,7 +815,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command()
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def stream(self, ctx, streamer=None, *, stream_title=None):
|
||||
async def stream(self, ctx: commands.Context, streamer=None, *, stream_title=None):
|
||||
"""Sets Red's streaming status
|
||||
Leaving both streamer and stream_title empty will clear it."""
|
||||
|
||||
@@ -826,7 +836,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command(name="username", aliases=["name"])
|
||||
@checks.is_owner()
|
||||
async def _username(self, ctx, *, username: str):
|
||||
async def _username(self, ctx: commands.Context, *, username: str):
|
||||
"""Sets Red's username"""
|
||||
try:
|
||||
await self._name(name=username)
|
||||
@@ -845,7 +855,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(name="nickname")
|
||||
@checks.admin()
|
||||
@commands.guild_only()
|
||||
async def _nickname(self, ctx, *, nickname: str = None):
|
||||
async def _nickname(self, ctx: commands.Context, *, nickname: str = None):
|
||||
"""Sets Red's nickname"""
|
||||
try:
|
||||
await ctx.guild.me.edit(nick=nickname)
|
||||
@@ -856,7 +866,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command(aliases=["prefixes"])
|
||||
@checks.is_owner()
|
||||
async def prefix(self, ctx, *prefixes):
|
||||
async def prefix(self, ctx: commands.Context, *prefixes: str):
|
||||
"""Sets Red's global prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.send_help()
|
||||
@@ -867,7 +877,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
@_set.command(aliases=["serverprefixes"])
|
||||
@checks.admin()
|
||||
@commands.guild_only()
|
||||
async def serverprefix(self, ctx, *prefixes):
|
||||
async def serverprefix(self, ctx: commands.Context, *prefixes: str):
|
||||
"""Sets Red's server prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.bot.db.guild(ctx.guild).prefix.set([])
|
||||
@@ -879,7 +889,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command()
|
||||
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
||||
async def owner(self, ctx):
|
||||
async def owner(self, ctx: commands.Context):
|
||||
"""Sets Red's main owner"""
|
||||
# According to the Python docs this is suitable for cryptographic use
|
||||
random = SystemRandom()
|
||||
@@ -923,7 +933,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def token(self, ctx, token: str):
|
||||
async def token(self, ctx: commands.Context, token: str):
|
||||
"""Change bot token."""
|
||||
|
||||
if not isinstance(ctx.channel, discord.DMChannel):
|
||||
@@ -1068,7 +1078,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def backup(self, ctx, backup_path: str = None):
|
||||
async def backup(self, ctx: commands.Context, backup_path: str = None):
|
||||
"""Creates a backup of all data for the instance."""
|
||||
from redbot.core.data_manager import basic_config, instance_name
|
||||
from redbot.core.drivers.red_json import JSON
|
||||
@@ -1077,21 +1087,20 @@ class Core(commands.Cog, CoreLogic):
|
||||
if basic_config["STORAGE_TYPE"] == "MongoDB":
|
||||
from redbot.core.drivers.red_mongo import Mongo
|
||||
|
||||
m = Mongo("Core", **basic_config["STORAGE_DETAILS"])
|
||||
m = Mongo("Core", "0", **basic_config["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collection_names = await db.collection_names(include_system_collections=False)
|
||||
collection_names = await db.list_collection_names()
|
||||
for c_name in collection_names:
|
||||
if c_name == "Core":
|
||||
c_data_path = data_dir / basic_config["CORE_PATH_APPEND"]
|
||||
else:
|
||||
c_data_path = data_dir / basic_config["COG_PATH_APPEND"]
|
||||
output = {}
|
||||
c_data_path = data_dir / basic_config["COG_PATH_APPEND"] / c_name
|
||||
docs = await db[c_name].find().to_list(None)
|
||||
for item in docs:
|
||||
item_id = str(item.pop("_id"))
|
||||
output[item_id] = item
|
||||
target = JSON(c_name, data_path_override=c_data_path)
|
||||
await target.jsonIO._threadsafe_save_json(output)
|
||||
output = item
|
||||
target = JSON(c_name, item_id, data_path_override=c_data_path)
|
||||
await target.jsonIO._threadsafe_save_json(output)
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
instance_name, ctx.message.created_at.strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
@@ -1131,8 +1140,11 @@ class Core(commands.Cog, CoreLogic):
|
||||
tar.add(str(f), recursive=False)
|
||||
print(str(backup_file))
|
||||
await ctx.send(
|
||||
_("A backup has been made of this instance. It is at {}.").format((backup_file))
|
||||
_("A backup has been made of this instance. It is at {}.").format(backup_file)
|
||||
)
|
||||
if backup_file.stat().st_size > 8_000_000:
|
||||
await ctx.send(_("This backup is to large to send via DM."))
|
||||
return
|
||||
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
||||
|
||||
pred = MessagePredicate.yes_or_no(ctx)
|
||||
@@ -1143,10 +1155,18 @@ class Core(commands.Cog, CoreLogic):
|
||||
else:
|
||||
if pred.result is True:
|
||||
await ctx.send(_("OK, it's on its way!"))
|
||||
async with ctx.author.typing():
|
||||
await ctx.author.send(
|
||||
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
||||
try:
|
||||
async with ctx.author.typing():
|
||||
await ctx.author.send(
|
||||
_("Here's a copy of the backup"),
|
||||
file=discord.File(str(backup_file)),
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
_("I don't seem to be able to DM you. Do you have closed DMs?")
|
||||
)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I could not send the backup file."))
|
||||
else:
|
||||
await ctx.send(_("OK then."))
|
||||
else:
|
||||
@@ -1154,7 +1174,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 60, commands.BucketType.user)
|
||||
async def contact(self, ctx, *, message: str):
|
||||
async def contact(self, ctx: commands.Context, *, message: str):
|
||||
"""Sends a message to the owner"""
|
||||
guild = ctx.message.guild
|
||||
owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
|
||||
@@ -1197,7 +1217,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(
|
||||
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
||||
)
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
else:
|
||||
await ctx.send(_("Your message has been sent."))
|
||||
@@ -1209,14 +1229,14 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(
|
||||
_("I cannot send your message, I'm unable to find my owner... *sigh*")
|
||||
)
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
else:
|
||||
await ctx.send(_("Your message has been sent."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def dm(self, ctx, user_id: int, *, message: str):
|
||||
async def dm(self, ctx: commands.Context, user_id: int, *, message: str):
|
||||
"""Sends a DM to a user
|
||||
|
||||
This command needs a user id to work.
|
||||
@@ -1250,7 +1270,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
try:
|
||||
await destination.send(embed=e)
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||
)
|
||||
@@ -1260,7 +1280,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
response = "{}\nMessage:\n\n{}".format(description, message)
|
||||
try:
|
||||
await destination.send("{}\n{}".format(box(response), content))
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
await ctx.send(
|
||||
_("Sorry, I couldn't deliver your message to {}").format(destination)
|
||||
)
|
||||
@@ -1269,7 +1289,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def whitelist(self, ctx):
|
||||
async def whitelist(self, ctx: commands.Context):
|
||||
"""
|
||||
Whitelist management commands.
|
||||
"""
|
||||
@@ -1287,7 +1307,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("User added to whitelist."))
|
||||
|
||||
@whitelist.command(name="list")
|
||||
async def whitelist_list(self, ctx):
|
||||
async def whitelist_list(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists whitelisted users.
|
||||
"""
|
||||
@@ -1301,7 +1321,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(box(page))
|
||||
|
||||
@whitelist.command(name="remove")
|
||||
async def whitelist_remove(self, ctx, user: discord.User):
|
||||
async def whitelist_remove(self, ctx: commands.Context, user: discord.User):
|
||||
"""
|
||||
Removes user from whitelist.
|
||||
"""
|
||||
@@ -1318,7 +1338,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("User was not in the whitelist."))
|
||||
|
||||
@whitelist.command(name="clear")
|
||||
async def whitelist_clear(self, ctx):
|
||||
async def whitelist_clear(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears the whitelist.
|
||||
"""
|
||||
@@ -1327,19 +1347,19 @@ class Core(commands.Cog, CoreLogic):
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def blacklist(self, ctx):
|
||||
async def blacklist(self, ctx: commands.Context):
|
||||
"""
|
||||
blacklist management commands.
|
||||
"""
|
||||
pass
|
||||
|
||||
@blacklist.command(name="add")
|
||||
async def blacklist_add(self, ctx, user: discord.User):
|
||||
async def blacklist_add(self, ctx: commands.Context, user: discord.User):
|
||||
"""
|
||||
Adds a user to the blacklist.
|
||||
"""
|
||||
if await ctx.bot.is_owner(user):
|
||||
ctx.send(_("You cannot blacklist an owner!"))
|
||||
await ctx.send(_("You cannot blacklist an owner!"))
|
||||
return
|
||||
|
||||
async with ctx.bot.db.blacklist() as curr_list:
|
||||
@@ -1349,7 +1369,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("User added to blacklist."))
|
||||
|
||||
@blacklist.command(name="list")
|
||||
async def blacklist_list(self, ctx):
|
||||
async def blacklist_list(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists blacklisted users.
|
||||
"""
|
||||
@@ -1363,7 +1383,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(box(page))
|
||||
|
||||
@blacklist.command(name="remove")
|
||||
async def blacklist_remove(self, ctx, user: discord.User):
|
||||
async def blacklist_remove(self, ctx: commands.Context, user: discord.User):
|
||||
"""
|
||||
Removes user from blacklist.
|
||||
"""
|
||||
@@ -1380,7 +1400,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("User was not in the blacklist."))
|
||||
|
||||
@blacklist.command(name="clear")
|
||||
async def blacklist_clear(self, ctx):
|
||||
async def blacklist_clear(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears the blacklist.
|
||||
"""
|
||||
@@ -1390,14 +1410,14 @@ class Core(commands.Cog, CoreLogic):
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(administrator=True)
|
||||
async def localwhitelist(self, ctx):
|
||||
async def localwhitelist(self, ctx: commands.Context):
|
||||
"""
|
||||
Whitelist management commands.
|
||||
"""
|
||||
pass
|
||||
|
||||
@localwhitelist.command(name="add")
|
||||
async def localwhitelist_add(self, ctx, *, user_or_role: str):
|
||||
async def localwhitelist_add(self, ctx: commands.Context, *, user_or_role: str):
|
||||
"""
|
||||
Adds a user or role to the whitelist.
|
||||
"""
|
||||
@@ -1418,7 +1438,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("Role added to whitelist."))
|
||||
|
||||
@localwhitelist.command(name="list")
|
||||
async def localwhitelist_list(self, ctx):
|
||||
async def localwhitelist_list(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists whitelisted users and roles.
|
||||
"""
|
||||
@@ -1432,7 +1452,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(box(page))
|
||||
|
||||
@localwhitelist.command(name="remove")
|
||||
async def localwhitelist_remove(self, ctx, *, user_or_role: str):
|
||||
async def localwhitelist_remove(self, ctx: commands.Context, *, user_or_role: str):
|
||||
"""
|
||||
Removes user or role from whitelist.
|
||||
"""
|
||||
@@ -1462,7 +1482,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("Role was not in the whitelist."))
|
||||
|
||||
@localwhitelist.command(name="clear")
|
||||
async def localwhitelist_clear(self, ctx):
|
||||
async def localwhitelist_clear(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears the whitelist.
|
||||
"""
|
||||
@@ -1472,14 +1492,14 @@ class Core(commands.Cog, CoreLogic):
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(administrator=True)
|
||||
async def localblacklist(self, ctx):
|
||||
async def localblacklist(self, ctx: commands.Context):
|
||||
"""
|
||||
blacklist management commands.
|
||||
"""
|
||||
pass
|
||||
|
||||
@localblacklist.command(name="add")
|
||||
async def localblacklist_add(self, ctx, *, user_or_role: str):
|
||||
async def localblacklist_add(self, ctx: commands.Context, *, user_or_role: str):
|
||||
"""
|
||||
Adds a user or role to the blacklist.
|
||||
"""
|
||||
@@ -1492,7 +1512,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
user = True
|
||||
|
||||
if user and await ctx.bot.is_owner(obj):
|
||||
ctx.send(_("You cannot blacklist an owner!"))
|
||||
await ctx.send(_("You cannot blacklist an owner!"))
|
||||
return
|
||||
|
||||
async with ctx.bot.db.guild(ctx.guild).blacklist() as curr_list:
|
||||
@@ -1505,7 +1525,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("Role added to blacklist."))
|
||||
|
||||
@localblacklist.command(name="list")
|
||||
async def localblacklist_list(self, ctx):
|
||||
async def localblacklist_list(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists blacklisted users and roles.
|
||||
"""
|
||||
@@ -1519,7 +1539,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(box(page))
|
||||
|
||||
@localblacklist.command(name="remove")
|
||||
async def localblacklist_remove(self, ctx, *, user_or_role: str):
|
||||
async def localblacklist_remove(self, ctx: commands.Context, *, user_or_role: str):
|
||||
"""
|
||||
Removes user or role from blacklist.
|
||||
"""
|
||||
@@ -1549,7 +1569,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.send(_("Role was not in the blacklist."))
|
||||
|
||||
@localblacklist.command(name="clear")
|
||||
async def localblacklist_clear(self, ctx):
|
||||
async def localblacklist_clear(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears the blacklist.
|
||||
"""
|
||||
@@ -1689,7 +1709,7 @@ class Core(commands.Cog, CoreLogic):
|
||||
await ctx.tick()
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(manage_server=True)
|
||||
@checks.guildowner_or_permissions(manage_guild=True)
|
||||
@commands.group(name="autoimmune")
|
||||
async def autoimmune_group(self, ctx: commands.Context):
|
||||
"""
|
||||
@@ -1700,8 +1720,8 @@ class Core(commands.Cog, CoreLogic):
|
||||
@autoimmune_group.command(name="list")
|
||||
async def autoimmune_list(self, ctx: commands.Context):
|
||||
"""
|
||||
Get's the current members and roles
|
||||
|
||||
Get's the current members and roles
|
||||
|
||||
configured for automatic moderation action immunity
|
||||
"""
|
||||
ai_ids = await ctx.bot.db.guild(ctx.guild).autoimmune_ids()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from copy import deepcopy
|
||||
import hashlib
|
||||
import shutil
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
import tempfile
|
||||
from discord.utils import deprecated
|
||||
|
||||
from . import commands
|
||||
from .json_io import JsonIO
|
||||
|
||||
__all__ = [
|
||||
@@ -153,124 +153,28 @@ def core_data_path() -> Path:
|
||||
return core_path.resolve()
|
||||
|
||||
|
||||
def _find_data_files(init_location: str) -> (Path, List[Path]):
|
||||
"""
|
||||
Discovers all files in the bundled data folder of an installed cog.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
init_location
|
||||
|
||||
Returns
|
||||
-------
|
||||
(pathlib.Path, list of pathlib.Path)
|
||||
"""
|
||||
init_file = Path(init_location)
|
||||
if not init_file.is_file():
|
||||
return []
|
||||
|
||||
package_folder = init_file.parent.resolve() / "data"
|
||||
if not package_folder.is_dir():
|
||||
return []
|
||||
|
||||
all_files = list(package_folder.rglob("*"))
|
||||
|
||||
return package_folder, [p.resolve() for p in all_files if p.is_file()]
|
||||
|
||||
|
||||
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
|
||||
"""
|
||||
Filters out files from ``to_copy`` that already exist, and are the
|
||||
same, in ``data_dir``. The files that are different are copied into
|
||||
``data_dir``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
to_copy : list of pathlib.Path
|
||||
bundled_data_dir : pathlib.Path
|
||||
cog_data_dir : pathlib.Path
|
||||
"""
|
||||
|
||||
def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
|
||||
for block in bytesiter:
|
||||
hasher.update(block)
|
||||
return hasher.hexdigest() if ashexstr else hasher.digest()
|
||||
|
||||
def file_as_blockiter(afile, blocksize=65536):
|
||||
with afile:
|
||||
block = afile.read(blocksize)
|
||||
while len(block) > 0:
|
||||
yield block
|
||||
block = afile.read(blocksize)
|
||||
|
||||
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) for p in to_copy}
|
||||
|
||||
for orig, poss_existing in lookup.items():
|
||||
if not poss_existing.is_file():
|
||||
poss_existing.parent.mkdir(exist_ok=True, parents=True)
|
||||
exists_checksum = None
|
||||
else:
|
||||
exists_checksum = hash_bytestr_iter(
|
||||
file_as_blockiter(poss_existing.open("rb")), hashlib.sha256()
|
||||
)
|
||||
|
||||
orig_checksum = ...
|
||||
if exists_checksum is not None:
|
||||
orig_checksum = hash_bytestr_iter(file_as_blockiter(orig.open("rb")), hashlib.sha256())
|
||||
|
||||
if exists_checksum != orig_checksum:
|
||||
shutil.copy(str(orig), str(poss_existing))
|
||||
log.debug("Copying {} to {}".format(orig, poss_existing))
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@deprecated("bundled_data_path() without calling this function")
|
||||
def load_bundled_data(cog_instance, init_location: str):
|
||||
pass
|
||||
|
||||
|
||||
def bundled_data_path(cog_instance: commands.Cog) -> Path:
|
||||
"""
|
||||
This function copies (and overwrites) data from the ``data/`` folder
|
||||
of the installed cog.
|
||||
Get the path to the "data" directory bundled with this cog.
|
||||
|
||||
The bundled data folder must be located alongside the ``.py`` file
|
||||
which contains the cog class.
|
||||
|
||||
.. important::
|
||||
|
||||
This function MUST be called from the ``setup()`` function of your
|
||||
cog.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from redbot.core import data_manager
|
||||
>>>
|
||||
>>> def setup(bot):
|
||||
>>> cog = MyCog()
|
||||
>>> data_manager.load_bundled_data(cog, __file__)
|
||||
>>> bot.add_cog(cog)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cog_instance
|
||||
An instance of your cog class.
|
||||
init_location : str
|
||||
The ``__file__`` attribute of the file where your ``setup()``
|
||||
function exists.
|
||||
"""
|
||||
bundled_data_folder, to_copy = _find_data_files(init_location)
|
||||
|
||||
cog_data_folder = cog_data_path(cog_instance) / "bundled_data"
|
||||
|
||||
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
|
||||
|
||||
|
||||
def bundled_data_path(cog_instance) -> Path:
|
||||
"""
|
||||
The "data" directory that has been copied from installed cogs.
|
||||
|
||||
.. important::
|
||||
|
||||
You should *NEVER* write to this directory. Data manager will
|
||||
overwrite files in this directory each time `load_bundled_data`
|
||||
is called. You should instead write to the directory provided by
|
||||
`cog_data_path`.
|
||||
You should *NEVER* write to this directory.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cog_instance
|
||||
An instance of your cog. If calling from a command or method of
|
||||
your cog, this should be ``self``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -280,10 +184,10 @@ def bundled_data_path(cog_instance) -> Path:
|
||||
Raises
|
||||
------
|
||||
FileNotFoundError
|
||||
If no bundled data folder exists or if it hasn't been loaded yet.
|
||||
"""
|
||||
If no bundled data folder exists.
|
||||
|
||||
bundled_path = cog_data_path(cog_instance) / "bundled_data"
|
||||
"""
|
||||
bundled_path = Path(inspect.getfile(cog_instance.__class__)).parent / "data"
|
||||
|
||||
if not bundled_path.is_dir():
|
||||
raise FileNotFoundError("No such directory {}".format(bundled_path))
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import motor.motor_asyncio
|
||||
from .red_base import BaseDriver
|
||||
import re
|
||||
from typing import Match, Pattern
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import motor.core
|
||||
import motor.motor_asyncio
|
||||
|
||||
from .red_base import BaseDriver
|
||||
|
||||
__all__ = ["Mongo"]
|
||||
|
||||
|
||||
@@ -9,7 +14,7 @@ _conn = None
|
||||
|
||||
|
||||
def _initialize(**kwargs):
|
||||
kwargs.get("URI", "mongodb")
|
||||
uri = kwargs.get("URI", "mongodb")
|
||||
host = kwargs["HOST"]
|
||||
port = kwargs["PORT"]
|
||||
admin_user = kwargs["USERNAME"]
|
||||
@@ -80,6 +85,7 @@ class Mongo(BaseDriver):
|
||||
async def get(self, *identifiers: str):
|
||||
mongo_collection = self.get_collection()
|
||||
|
||||
identifiers = (*map(self._escape_key, identifiers),)
|
||||
dot_identifiers = ".".join(identifiers)
|
||||
|
||||
partial = await mongo_collection.find_one(
|
||||
@@ -91,10 +97,14 @@ class Mongo(BaseDriver):
|
||||
|
||||
for i in identifiers:
|
||||
partial = partial[i]
|
||||
if isinstance(partial, dict):
|
||||
return self._unescape_dict_keys(partial)
|
||||
return partial
|
||||
|
||||
async def set(self, *identifiers: str, value=None):
|
||||
dot_identifiers = ".".join(identifiers)
|
||||
dot_identifiers = ".".join(map(self._escape_key, identifiers))
|
||||
if isinstance(value, dict):
|
||||
value = self._escape_dict_keys(value)
|
||||
|
||||
mongo_collection = self.get_collection()
|
||||
|
||||
@@ -105,7 +115,7 @@ class Mongo(BaseDriver):
|
||||
)
|
||||
|
||||
async def clear(self, *identifiers: str):
|
||||
dot_identifiers = ".".join(identifiers)
|
||||
dot_identifiers = ".".join(map(self._escape_key, identifiers))
|
||||
mongo_collection = self.get_collection()
|
||||
|
||||
if len(identifiers) > 0:
|
||||
@@ -115,6 +125,62 @@ class Mongo(BaseDriver):
|
||||
else:
|
||||
await mongo_collection.delete_one({"_id": self.unique_cog_identifier})
|
||||
|
||||
@staticmethod
|
||||
def _escape_key(key: str) -> str:
|
||||
return _SPECIAL_CHAR_PATTERN.sub(_replace_with_escaped, key)
|
||||
|
||||
@staticmethod
|
||||
def _unescape_key(key: str) -> str:
|
||||
return _CHAR_ESCAPE_PATTERN.sub(_replace_with_unescaped, key)
|
||||
|
||||
@classmethod
|
||||
def _escape_dict_keys(cls, data: dict) -> dict:
|
||||
"""Recursively escape all keys in a dict."""
|
||||
ret = {}
|
||||
for key, value in data.items():
|
||||
key = cls._escape_key(key)
|
||||
if isinstance(value, dict):
|
||||
value = cls._escape_dict_keys(value)
|
||||
ret[key] = value
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def _unescape_dict_keys(cls, data: dict) -> dict:
|
||||
"""Recursively unescape all keys in a dict."""
|
||||
ret = {}
|
||||
for key, value in data.items():
|
||||
key = cls._unescape_key(key)
|
||||
if isinstance(value, dict):
|
||||
value = cls._unescape_dict_keys(value)
|
||||
ret[key] = value
|
||||
return ret
|
||||
|
||||
|
||||
_SPECIAL_CHAR_PATTERN: Pattern[str] = re.compile(r"([.$]|\\U0000002E|\\U00000024)")
|
||||
_SPECIAL_CHARS = {
|
||||
".": "\\U0000002E",
|
||||
"$": "\\U00000024",
|
||||
"\\U0000002E": "\\U&0000002E",
|
||||
"\\U00000024": "\\U&00000024",
|
||||
}
|
||||
|
||||
|
||||
def _replace_with_escaped(match: Match[str]) -> str:
|
||||
return _SPECIAL_CHARS[match[0]]
|
||||
|
||||
|
||||
_CHAR_ESCAPE_PATTERN: Pattern[str] = re.compile(r"(\\U0000002E|\\U00000024)")
|
||||
_CHAR_ESCAPES = {
|
||||
"\\U0000002E": ".",
|
||||
"\\U00000024": "$",
|
||||
"\\U&0000002E": "\\U0000002E",
|
||||
"\\U&00000024": "\\U00000024",
|
||||
}
|
||||
|
||||
|
||||
def _replace_with_unescaped(match: Match[str]) -> str:
|
||||
return _CHAR_ESCAPES[match[0]]
|
||||
|
||||
|
||||
def get_config_details():
|
||||
uri = None
|
||||
|
||||
44
redbot/core/errors.py
Normal file
44
redbot/core/errors.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import importlib.machinery
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
|
||||
from .i18n import Translator
|
||||
|
||||
_ = Translator(__name__, __file__)
|
||||
|
||||
|
||||
class RedError(Exception):
|
||||
"""Base error class for Red-related errors."""
|
||||
|
||||
|
||||
class PackageAlreadyLoaded(RedError):
|
||||
"""Raised when trying to load an already-loaded package."""
|
||||
|
||||
def __init__(self, spec: importlib.machinery.ModuleSpec, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.spec: importlib.machinery.ModuleSpec = spec
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"There is already a package named {self.spec.name.split('.')[-1]} loaded"
|
||||
|
||||
|
||||
class BankError(RedError):
|
||||
"""Base error class for bank-related errors."""
|
||||
|
||||
|
||||
class BalanceTooHigh(BankError, OverflowError):
|
||||
"""Raised when trying to set a user's balance to higher than the maximum."""
|
||||
|
||||
def __init__(
|
||||
self, user: discord.abc.User, max_balance: int, currency_name: str, *args, **kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = user
|
||||
self.max_balance = max_balance
|
||||
self.currency_name = currency_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("{user}'s balance cannot rise above {max:,} {currency}.").format(
|
||||
user=self.user, max=self.max_balance, currency=self.currency_name
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
import contextlib
|
||||
import sys
|
||||
import codecs
|
||||
import datetime
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
from distutils.version import StrictVersion
|
||||
from typing import List
|
||||
|
||||
import aiohttp
|
||||
@@ -13,9 +13,9 @@ import pkg_resources
|
||||
from colorama import Fore, Style, init
|
||||
from pkg_resources import DistributionNotFound
|
||||
|
||||
from . import __version__, commands
|
||||
from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
|
||||
from .data_manager import storage_type
|
||||
from .utils.chat_formatting import inline, bordered, humanize_list
|
||||
from .utils.chat_formatting import inline, bordered, format_perms_list
|
||||
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||
|
||||
log = logging.getLogger("red")
|
||||
@@ -105,7 +105,6 @@ def init_events(bot, cli_flags):
|
||||
|
||||
prefixes = cli_flags.prefix or (await bot.db.prefix())
|
||||
lang = await bot.db.locale()
|
||||
red_version = __version__
|
||||
red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
|
||||
dpy_version = discord.__version__
|
||||
|
||||
@@ -125,24 +124,22 @@ def init_events(bot, cli_flags):
|
||||
|
||||
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
|
||||
|
||||
try:
|
||||
with contextlib.suppress(aiohttp.ClientError, discord.HTTPException):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
if VersionInfo.from_str(data["info"]["version"]) > red_version_info:
|
||||
INFO.append(
|
||||
"Outdated version! {} is available "
|
||||
"but you're using {}".format(data["info"]["version"], red_version)
|
||||
)
|
||||
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id)
|
||||
owner = await bot.get_user_info(bot.owner_id)
|
||||
await owner.send(
|
||||
"Your Red instance is out of date! {} is the current "
|
||||
"version, however you are using {}!".format(
|
||||
data["info"]["version"], red_version
|
||||
)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
INFO2 = []
|
||||
|
||||
sentry = await bot.db.enable_sentry()
|
||||
@@ -237,18 +234,13 @@ def init_events(bot, cli_flags):
|
||||
else:
|
||||
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
|
||||
elif isinstance(error, commands.BotMissingPermissions):
|
||||
missing_perms: List[str] = []
|
||||
for perm, value in error.missing:
|
||||
if value is True:
|
||||
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||
missing_perms.append(perm_name)
|
||||
if len(missing_perms) == 1:
|
||||
if bin(error.missing.value).count("1") == 1: # Only one perm missing
|
||||
plural = ""
|
||||
else:
|
||||
plural = "s"
|
||||
await ctx.send(
|
||||
"I require the {perms} permission{plural} to execute that command.".format(
|
||||
perms=humanize_list(missing_perms), plural=plural
|
||||
perms=format_perms_list(error.missing), plural=plural
|
||||
)
|
||||
)
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
|
||||
@@ -23,6 +23,7 @@ discord.py 1.0.0a
|
||||
|
||||
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
|
||||
"""
|
||||
import contextlib
|
||||
from collections import namedtuple
|
||||
from typing import List, Optional, Union
|
||||
|
||||
@@ -224,8 +225,8 @@ class Help(dpy_formatter.HelpFormatter):
|
||||
|
||||
return ret
|
||||
|
||||
async def format_help_for(self, ctx, command_or_bot, reason: str = None):
|
||||
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED?
|
||||
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
|
||||
"""Formats the help page and handles the actual heavy lifting of how
|
||||
the help command looks like. To change the behaviour, override the
|
||||
:meth:`~.HelpFormatter.format` method.
|
||||
|
||||
@@ -244,10 +245,24 @@ class Help(dpy_formatter.HelpFormatter):
|
||||
"""
|
||||
self.context = ctx
|
||||
self.command = command_or_bot
|
||||
|
||||
# We want the permission state to be set as if the author had run the command he is
|
||||
# requesting help for. This is so the subcommands shown in the help menu correctly reflect
|
||||
# any permission rules set.
|
||||
if isinstance(self.command, commands.Command):
|
||||
with contextlib.suppress(commands.CommandError):
|
||||
await self.command.can_run(
|
||||
self.context, check_all_parents=True, change_permission_state=True
|
||||
)
|
||||
elif isinstance(self.command, commands.Cog):
|
||||
with contextlib.suppress(commands.CommandError):
|
||||
# Cog's don't have a `can_run` method, so we use the `Requires` object directly.
|
||||
await self.command.requires.verify(self.context)
|
||||
|
||||
emb = await self.format()
|
||||
|
||||
if reason:
|
||||
emb["embed"]["title"] = "{0}".format(reason)
|
||||
emb["embed"]["title"] = reason
|
||||
|
||||
ret = []
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Callable, Union
|
||||
|
||||
from . import commands
|
||||
|
||||
__all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"]
|
||||
|
||||
_current_locale = "en_us"
|
||||
@@ -219,6 +217,12 @@ class Translator(Callable[[str], str]):
|
||||
self.translations.update({untranslated: translated})
|
||||
|
||||
|
||||
# This import to be down here to avoid circular import issues.
|
||||
# This will be cleaned up at a later date
|
||||
# noinspection PyPep8
|
||||
from . import commands
|
||||
|
||||
|
||||
def cog_i18n(translator: Translator):
|
||||
"""Get a class decorator to link the translator to this cog."""
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from uuid import uuid4
|
||||
|
||||
# This is basically our old DataIO and just a base for much more elaborate classes
|
||||
@@ -69,7 +70,11 @@ class JsonIO:
|
||||
|
||||
async def _threadsafe_save_json(self, data, settings=PRETTY):
|
||||
loop = asyncio.get_event_loop()
|
||||
func = functools.partial(self._save_json, data, settings)
|
||||
# the deepcopy is needed here. otherwise,
|
||||
# the dict can change during serialization
|
||||
# and this will break the encoder.
|
||||
data_copy = deepcopy(data)
|
||||
func = functools.partial(self._save_json, data_copy, settings)
|
||||
async with self._lock:
|
||||
await loop.run_in_executor(None, func)
|
||||
|
||||
|
||||
@@ -666,29 +666,30 @@ async def register_casetypes(new_types: List[dict]) -> List[CaseType]:
|
||||
return type_list
|
||||
|
||||
|
||||
async def get_modlog_channel(guild: discord.Guild) -> Union[discord.TextChannel, None]:
|
||||
async def get_modlog_channel(guild: discord.Guild) -> discord.TextChannel:
|
||||
"""
|
||||
Get the current modlog channel
|
||||
Get the current modlog channel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
guild: `discord.Guild`
|
||||
The guild to get the modlog channel for
|
||||
The guild to get the modlog channel for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
`discord.TextChannel` or `None`
|
||||
The channel object representing the modlog channel
|
||||
`discord.TextChannel`
|
||||
The channel object representing the modlog channel.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the modlog channel is not found
|
||||
If the modlog channel is not found.
|
||||
|
||||
"""
|
||||
if hasattr(guild, "get_channel"):
|
||||
channel = guild.get_channel(await _conf.guild(guild).mod_log())
|
||||
else:
|
||||
# For unit tests only
|
||||
channel = await _conf.guild(guild).mod_log()
|
||||
if channel is None:
|
||||
raise RuntimeError("Failed to get the mod log channel!")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import itertools
|
||||
from typing import Sequence, Iterator, List
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = Translator("UtilsChatFormatting", __file__)
|
||||
@@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
|
||||
return text
|
||||
|
||||
|
||||
def humanize_list(items: Sequence[str]):
|
||||
def humanize_list(items: Sequence[str]) -> str:
|
||||
"""Get comma-separted list, with the last element joined with *and*.
|
||||
|
||||
This uses an Oxford comma, because without one, items containing
|
||||
@@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
|
||||
if len(items) == 1:
|
||||
return items[0]
|
||||
return ", ".join(items[:-1]) + _(", and ") + items[-1]
|
||||
|
||||
|
||||
def format_perms_list(perms: discord.Permissions) -> str:
|
||||
"""Format a list of permission names.
|
||||
|
||||
This will return a humanized list of the names of all enabled
|
||||
permissions in the provided `discord.Permissions` object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
perms : discord.Permissions
|
||||
The permissions object with the requested permissions to list
|
||||
enabled.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The humanized list.
|
||||
|
||||
"""
|
||||
perm_names: List[str] = []
|
||||
for perm, value in perms:
|
||||
if value is True:
|
||||
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||
perm_names.append(perm_name)
|
||||
return humanize_list(perm_names).replace("Guild", "Server")
|
||||
|
||||
@@ -73,10 +73,13 @@ async def menu(
|
||||
# noinspection PyAsyncCall
|
||||
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
|
||||
else:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
else:
|
||||
await message.edit(content=current_page)
|
||||
try:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
else:
|
||||
await message.edit(content=current_page)
|
||||
except discord.NotFound:
|
||||
return
|
||||
|
||||
try:
|
||||
react, user = await ctx.bot.wait_for(
|
||||
@@ -90,9 +93,12 @@ async def menu(
|
||||
except discord.Forbidden: # cannot remove all reactions
|
||||
for key in controls.keys():
|
||||
await message.remove_reaction(key, ctx.bot.user)
|
||||
return None
|
||||
|
||||
return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji)
|
||||
except discord.NotFound:
|
||||
return
|
||||
else:
|
||||
return await controls[react.emoji](
|
||||
ctx, pages, controls, message, page, timeout, react.emoji
|
||||
)
|
||||
|
||||
|
||||
async def next_page(
|
||||
@@ -106,10 +112,8 @@ async def next_page(
|
||||
):
|
||||
perms = message.channel.permissions_for(ctx.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == len(pages) - 1:
|
||||
page = 0 # Loop around to the first item
|
||||
else:
|
||||
@@ -128,10 +132,8 @@ async def prev_page(
|
||||
):
|
||||
perms = message.channel.permissions_for(ctx.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == 0:
|
||||
page = len(pages) - 1 # Loop around to the last item
|
||||
else:
|
||||
@@ -148,9 +150,8 @@ async def close_menu(
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
):
|
||||
if message:
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
|
||||
def start_adding_reactions(
|
||||
@@ -161,7 +162,7 @@ def start_adding_reactions(
|
||||
"""Start adding reactions to a message.
|
||||
|
||||
This is a non-blocking operation - calling this will schedule the
|
||||
reactions being added, but will the calling code will continue to
|
||||
reactions being added, but the calling code will continue to
|
||||
execute asynchronously. There is no need to await this function.
|
||||
|
||||
This is particularly useful if you wish to start waiting for a
|
||||
@@ -169,7 +170,7 @@ def start_adding_reactions(
|
||||
this is exactly what `menu` uses to do that.
|
||||
|
||||
This spawns a `asyncio.Task` object and schedules it on ``loop``.
|
||||
If ``loop`` omitted, the loop will be retreived with
|
||||
If ``loop`` omitted, the loop will be retrieved with
|
||||
`asyncio.get_event_loop`.
|
||||
|
||||
Parameters
|
||||
|
||||
@@ -2,7 +2,6 @@ import discord
|
||||
from datetime import datetime
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
import io
|
||||
import sys
|
||||
import weakref
|
||||
from typing import List, Optional
|
||||
from .common_filters import filter_mass_mentions
|
||||
@@ -151,15 +150,12 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
|
||||
"""
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
return []
|
||||
files.append(discord.File(_fp, filename=a.filename))
|
||||
max_size = 8 * 1000 * 1000
|
||||
if m.attachments and sum(a.size for a in m.attachments) <= max_size:
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
files.append(discord.File(_fp, filename=a.filename))
|
||||
return files
|
||||
|
||||
async def communicate(
|
||||
|
||||
@@ -8,18 +8,14 @@ import asyncio
|
||||
import aiohttp
|
||||
|
||||
import pkg_resources
|
||||
from pathlib import Path
|
||||
from distutils.version import StrictVersion
|
||||
from redbot.setup import (
|
||||
basic_setup,
|
||||
load_existing_config,
|
||||
remove_instance,
|
||||
remove_instance_interaction,
|
||||
create_backup,
|
||||
save_config,
|
||||
)
|
||||
from redbot.core import __version__
|
||||
from redbot.core.utils import safe_delete
|
||||
from redbot.core import __version__, version_info as red_version_info, VersionInfo
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
if sys.platform == "linux":
|
||||
@@ -390,7 +386,7 @@ async def is_outdated():
|
||||
async with session.get("{}/json".format(red_pypi)) as r:
|
||||
data = await r.json()
|
||||
new_version = data["info"]["version"]
|
||||
return StrictVersion(new_version) > StrictVersion(__version__), new_version
|
||||
return VersionInfo.from_str(new_version) > red_version_info, new_version
|
||||
|
||||
|
||||
def main_menu():
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
@@ -177,26 +176,21 @@ def basic_setup():
|
||||
async def json_to_mongo(current_data_dir: Path, storage_details: dict):
|
||||
from redbot.core.drivers.red_mongo import Mongo
|
||||
|
||||
core_data_file = list(current_data_dir.glob("core/settings.json"))[0]
|
||||
m = Mongo("Core", "0", **storage_details)
|
||||
core_data_file = current_data_dir / "core" / "settings.json"
|
||||
driver = Mongo(cog_name="Core", identifier="0", **storage_details)
|
||||
with core_data_file.open(mode="r") as f:
|
||||
core_data = json.loads(f.read())
|
||||
collection = m.get_collection()
|
||||
await collection.update_one(
|
||||
{"_id": m.unique_cog_identifier}, update={"$set": core_data["0"]}, upsert=True
|
||||
)
|
||||
data = core_data.get("0", {})
|
||||
for key, value in data.items():
|
||||
await driver.set(key, value=value)
|
||||
for p in current_data_dir.glob("cogs/**/settings.json"):
|
||||
cog_name = p.parent.stem
|
||||
with p.open(mode="r") as f:
|
||||
cog_data = json.loads(f.read())
|
||||
cog_i = None
|
||||
for ident in list(cog_data.keys()):
|
||||
cog_i = str(hash(ident))
|
||||
cog_m = Mongo(p.parent.stem, cog_i, **storage_details)
|
||||
cog_c = cog_m.get_collection()
|
||||
for ident in list(cog_data.keys()):
|
||||
await cog_c.update_one(
|
||||
{"_id": cog_m.unique_cog_identifier}, update={"$set": cog_data[cog_i]}, upsert=True
|
||||
)
|
||||
cog_data = json.load(f)
|
||||
for identifier, data in cog_data.items():
|
||||
driver = Mongo(cog_name, identifier, **storage_details)
|
||||
for key, value in data.items():
|
||||
await driver.set(key, value=value)
|
||||
|
||||
|
||||
async def mongo_to_json(current_data_dir: Path, storage_details: dict):
|
||||
|
||||
50
setup.py
50
setup.py
@@ -9,59 +9,59 @@ install_requires = [
|
||||
"aiohttp-json-rpc==0.11.2",
|
||||
"aiohttp==3.4.4",
|
||||
"appdirs==1.4.3",
|
||||
"async-timeout==3.0.0",
|
||||
"async-timeout==3.0.1",
|
||||
"attrs==18.2.0",
|
||||
"chardet==3.0.4",
|
||||
"colorama==0.3.9",
|
||||
"colorama==0.4.1",
|
||||
"discord.py>=1.0.0a0",
|
||||
"distro==1.3.0; sys_platform == 'linux'",
|
||||
"fuzzywuzzy==0.17.0",
|
||||
"idna-ssl==1.1.0",
|
||||
"idna==2.7",
|
||||
"multidict==4.4.2",
|
||||
"idna==2.8",
|
||||
"multidict==4.5.2",
|
||||
"python-levenshtein==0.12.0",
|
||||
"pyyaml==3.13",
|
||||
"raven==6.9.0",
|
||||
"raven==6.10.0",
|
||||
"raven-aiohttp==0.7.0",
|
||||
"schema==0.6.8",
|
||||
"websockets==6.0",
|
||||
"yarl==1.2.6",
|
||||
"yarl==1.3.0",
|
||||
]
|
||||
|
||||
extras_require = {
|
||||
"test": [
|
||||
"atomicwrites==1.2.1",
|
||||
"more-itertools==4.3.0",
|
||||
"pluggy==0.7.1",
|
||||
"py==1.6.0",
|
||||
"pytest==3.8.2",
|
||||
"pytest-asyncio==0.9.0",
|
||||
"six==1.11.0",
|
||||
"more-itertools==5.0.0",
|
||||
"pluggy==0.8.1",
|
||||
"py==1.7.0",
|
||||
"pytest==4.1.0",
|
||||
"pytest-asyncio==0.10.0",
|
||||
"six==1.12.0",
|
||||
],
|
||||
"mongo": ["motor==2.0.0", "pymongo==3.7.1", "dnspython==1.15.0"],
|
||||
"mongo": ["motor==2.0.0", "pymongo==3.7.2", "dnspython==1.16.0"],
|
||||
"docs": [
|
||||
"alabaster==0.7.11",
|
||||
"alabaster==0.7.12",
|
||||
"babel==2.6.0",
|
||||
"certifi==2018.8.24",
|
||||
"certifi==2018.11.29",
|
||||
"docutils==0.14",
|
||||
"imagesize==1.1.0",
|
||||
"Jinja2==2.10",
|
||||
"MarkupSafe==1.0",
|
||||
"MarkupSafe==1.1.0",
|
||||
"packaging==18.0",
|
||||
"pyparsing==2.2.2",
|
||||
"Pygments==2.2.0",
|
||||
"pytz==2018.5",
|
||||
"requests==2.19.1",
|
||||
"urllib3==1.23",
|
||||
"six==1.11.0",
|
||||
"pyparsing==2.3.0",
|
||||
"Pygments==2.3.1",
|
||||
"pytz==2018.9",
|
||||
"requests==2.21.0",
|
||||
"six==1.12.0",
|
||||
"snowballstemmer==1.2.1",
|
||||
"sphinx==1.7.9",
|
||||
"sphinx_rtd_theme==0.4.1",
|
||||
"sphinx==1.8.3",
|
||||
"sphinx_rtd_theme==0.4.2",
|
||||
"sphinxcontrib-asyncio==0.2.0",
|
||||
"sphinxcontrib-websupport==1.1.0",
|
||||
"urllib3==1.24.1",
|
||||
],
|
||||
"voice": ["red-lavalink==0.1.2"],
|
||||
"style": ["black==18.9b0", "click==7.0", "toml==0.9.6"],
|
||||
"style": ["black==18.9b0", "click==7.0", "toml==0.10.0"],
|
||||
}
|
||||
|
||||
python_requires = ">=3.6.2,<3.8"
|
||||
|
||||
@@ -3,7 +3,7 @@ from redbot.cogs.permissions.permissions import Permissions, GLOBAL
|
||||
|
||||
def test_schema_update():
|
||||
old = {
|
||||
GLOBAL: {
|
||||
str(GLOBAL): {
|
||||
"owner_models": {
|
||||
"cogs": {
|
||||
"Admin": {"allow": [78631113035100160], "deny": [96733288462286848]},
|
||||
@@ -19,7 +19,7 @@ def test_schema_update():
|
||||
},
|
||||
}
|
||||
},
|
||||
43733288462286848: {
|
||||
"43733288462286848": {
|
||||
"owner_models": {
|
||||
"cogs": {
|
||||
"Admin": {
|
||||
@@ -43,22 +43,22 @@ def test_schema_update():
|
||||
assert new == (
|
||||
{
|
||||
"Admin": {
|
||||
GLOBAL: {78631113035100160: True, 96733288462286848: False},
|
||||
43733288462286848: {24231113035100160: True, 35533288462286848: False},
|
||||
str(GLOBAL): {"78631113035100160": True, "96733288462286848": False},
|
||||
"43733288462286848": {"24231113035100160": True, "35533288462286848": False},
|
||||
},
|
||||
"Audio": {GLOBAL: {133049272517001216: True, "default": False}},
|
||||
"General": {43733288462286848: {133049272517001216: True, "default": False}},
|
||||
"Audio": {str(GLOBAL): {"133049272517001216": True, "default": False}},
|
||||
"General": {"43733288462286848": {"133049272517001216": True, "default": False}},
|
||||
},
|
||||
{
|
||||
"cleanup bot": {
|
||||
GLOBAL: {78631113035100160: True, "default": False},
|
||||
43733288462286848: {17831113035100160: True, "default": True},
|
||||
str(GLOBAL): {"78631113035100160": True, "default": False},
|
||||
"43733288462286848": {"17831113035100160": True, "default": True},
|
||||
},
|
||||
"ping": {GLOBAL: {96733288462286848: True, "default": True}},
|
||||
"ping": {str(GLOBAL): {"96733288462286848": True, "default": True}},
|
||||
"set adminrole": {
|
||||
43733288462286848: {
|
||||
87733288462286848: True,
|
||||
95433288462286848: False,
|
||||
"43733288462286848": {
|
||||
"87733288462286848": True,
|
||||
"95433288462286848": False,
|
||||
"default": True,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_trivia_lists():
|
||||
for l in list_names:
|
||||
with l.open() as f:
|
||||
try:
|
||||
dict_ = yaml.load(f)
|
||||
dict_ = yaml.safe_load(f)
|
||||
except yaml.error.YAMLError as e:
|
||||
problem_lists.append((l.stem, "YAML error:\n{!s}".format(e)))
|
||||
else:
|
||||
|
||||
@@ -475,3 +475,18 @@ async def test_get_raw_mixes_defaults(config):
|
||||
|
||||
subgroup = await config.get_raw("subgroup")
|
||||
assert subgroup == {"foo": True, "bar": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cast_str_raw(config):
|
||||
await config.set_raw(123, 456, value=True)
|
||||
assert await config.get_raw(123, 456) is True
|
||||
assert await config.get_raw("123", "456") is True
|
||||
await config.clear_raw("123", 456)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cast_str_nested(config):
|
||||
config.register_global(foo={})
|
||||
await config.foo.set({123: True, 456: {789: False}})
|
||||
assert await config.foo() == {"123": True, "456": {"789": False}}
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
from redbot import core
|
||||
from redbot.core import VersionInfo
|
||||
|
||||
|
||||
def test_version_working():
|
||||
assert hasattr(core, "__version__")
|
||||
assert core.__version__[0] == "3"
|
||||
|
||||
|
||||
# When adding more of these, ensure they are added in ascending order of precedence
|
||||
version_tests = (
|
||||
"3.0.0a32.post10.dev12",
|
||||
"3.0.0rc1.dev1",
|
||||
"3.0.0rc1",
|
||||
"3.0.0",
|
||||
"3.0.1",
|
||||
"3.0.1.post1.dev1",
|
||||
"3.0.1.post1",
|
||||
"2018.10.6b21",
|
||||
)
|
||||
|
||||
|
||||
def test_version_info_str_parsing():
|
||||
for version_str in version_tests:
|
||||
assert version_str == str(VersionInfo.from_str(version_str))
|
||||
|
||||
|
||||
def test_version_info_lt():
|
||||
for next_idx, cur in enumerate(version_tests[:-1], start=1):
|
||||
cur_test = VersionInfo.from_str(cur)
|
||||
next_test = VersionInfo.from_str(version_tests[next_idx])
|
||||
assert cur_test < next_test
|
||||
|
||||
|
||||
def test_version_info_gt():
|
||||
assert VersionInfo.from_str(version_tests[1]) > VersionInfo.from_str(version_tests[0])
|
||||
|
||||
Reference in New Issue
Block a user