aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile.lock175
-rw-r--r--bot/cogs/doc.py214
-rw-r--r--bot/cogs/eval.py4
-rw-r--r--bot/cogs/filtering.py8
-rw-r--r--bot/cogs/moderation/scheduler.py77
-rw-r--r--bot/cogs/moderation/superstarify.py21
-rw-r--r--bot/cogs/moderation/utils.py12
-rw-r--r--bot/interpreter.py4
-rw-r--r--bot/utils/scheduling.py6
9 files changed, 374 insertions, 147 deletions
diff --git a/Pipfile.lock b/Pipfile.lock
index 95955ff89..69caf4646 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,11 +18,11 @@
"default": {
"aio-pika": {
"hashes": [
- "sha256:1dcec3e3e3309e277511dc0d7d157676d0165c174a6a745673fc9cf0510db8f0",
- "sha256:dd5a23ca26a4872ee73bd107e4c545bace572cdec2a574aeb61f4062c7774b2a"
+ "sha256:1da038b3d2c1b49e0e816d87424e702912bb77f9b5197f2bf279217915b4f7ed",
+ "sha256:29fe851374b86c997a22174c04352b5941bc1c2e36bbf542918ac18a76cfc9d3"
],
"index": "pypi",
- "version": "==6.1.3"
+ "version": "==6.3.0"
},
"aiodns": {
"hashes": [
@@ -62,10 +62,10 @@
},
"aiormq": {
"hashes": [
- "sha256:c3e4dd01a2948a75f739fb637334dbb8c6f1a4cecf74d5ed662dc3bab7f39973",
- "sha256:e220d3f9477bb2959b729b79bec815148ddb8a7686fc6c3d05d41c88ebd7c59e"
+ "sha256:afc0d46837b121585e4faec0a7646706429b4e2f5110ae8d0b5cdc3708b4b0e5",
+ "sha256:dc0fbbc7f8ad5af6a2cc18e00ccc5f925984cde3db6e8fe952c07b7ef157b5f2"
],
- "version": "==2.8.0"
+ "version": "==2.9.1"
},
"alabaster": {
"hashes": [
@@ -83,10 +83,10 @@
},
"attrs": {
"hashes": [
- "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2",
- "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396"
+ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
+ "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
- "version": "==19.2.0"
+ "version": "==19.3.0"
},
"babel": {
"hashes": [
@@ -112,36 +112,41 @@
},
"cffi": {
"hashes": [
- "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
- "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
- "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
- "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
- "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
- "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
- "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
- "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
- "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
- "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
- "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
- "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
- "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
- "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
- "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
- "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
- "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
- "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
- "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
- "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
- "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
- "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
- "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
- "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
- "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
- "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
- "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
- "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
- ],
- "version": "==1.12.3"
+ "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
+ "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
+ "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
+ "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
+ "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
+ "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
+ "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
+ "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
+ "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
+ "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
+ "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
+ "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
+ "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
+ "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
+ "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
+ "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
+ "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
+ "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
+ "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
+ "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
+ "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
+ "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
+ "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
+ "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
+ "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
+ "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
+ "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
+ "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
+ "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
+ "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
+ "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
+ "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
+ "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
+ ],
+ "version": "==1.13.2"
},
"chardet": {
"hashes": [
@@ -152,18 +157,18 @@
},
"deepdiff": {
"hashes": [
- "sha256:1123762580af0904621136d117c8397392a244d3ff0fa0a50de57a7939582476",
- "sha256:6ab13e0cbb627dadc312deaca9bef38de88a737a9bbdbfbe6e3857748219c127"
+ "sha256:3457ea7cecd51ba48015d89edbb569358af4d9b9e65e28bdb3209608420627f9",
+ "sha256:5e2343398e90538edaa59c0c99207e996a3a834fdc878c666376f632a760c35a"
],
"index": "pypi",
- "version": "==4.0.7"
+ "version": "==4.0.9"
},
"discord-py": {
"hashes": [
- "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d"
+ "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"
],
"index": "pypi",
- "version": "==1.2.3"
+ "version": "==1.2.5"
},
"docutils": {
"hashes": [
@@ -221,6 +226,7 @@
"sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4",
"sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc",
"sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1",
+ "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c",
"sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046",
"sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36",
"sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5",
@@ -231,11 +237,14 @@
"sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc",
"sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7",
"sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38",
+ "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232",
"sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5",
"sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832",
+ "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0",
"sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a",
"sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f",
"sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9",
+ "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2",
"sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692",
"sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84",
"sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79",
@@ -379,18 +388,18 @@
},
"pyparsing": {
"hashes": [
- "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
- "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
+ "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
+ "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
],
- "version": "==2.4.2"
+ "version": "==2.4.5"
},
"python-dateutil": {
"hashes": [
- "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
- "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
+ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+ "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"index": "pypi",
- "version": "==2.8.0"
+ "version": "==2.8.1"
},
"python-json-logger": {
"hashes": [
@@ -434,10 +443,10 @@
},
"six": {
"hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
+ "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
],
- "version": "==1.12.0"
+ "version": "==1.13.0"
},
"snowballstemmer": {
"hashes": [
@@ -448,18 +457,18 @@
},
"soupsieve": {
"hashes": [
- "sha256:605f89ad5fdbfefe30cdc293303665eff2d188865d4dbe4eb510bba1edfbfce3",
- "sha256:b91d676b330a0ebd5b21719cb6e9b57c57d433671f65b9c28dd3461d9a1ed0b6"
+ "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5",
+ "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"
],
- "version": "==1.9.4"
+ "version": "==1.9.5"
},
"sphinx": {
"hashes": [
- "sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845",
- "sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"
+ "sha256:31088dfb95359384b1005619827eaee3056243798c62724fd3fa4b84ee4d71bd",
+ "sha256:52286a0b9d7caa31efee301ec4300dbdab23c3b05da1c9024b4e84896fb73d79"
],
"index": "pypi",
- "version": "==2.2.0"
+ "version": "==2.2.1"
},
"sphinxcontrib-applehelp": {
"hashes": [
@@ -564,10 +573,10 @@
},
"attrs": {
"hashes": [
- "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2",
- "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396"
+ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
+ "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
- "version": "==19.2.0"
+ "version": "==19.3.0"
},
"certifi": {
"hashes": [
@@ -658,11 +667,11 @@
},
"flake8": {
"hashes": [
- "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
- "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
+ "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
+ "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
],
"index": "pypi",
- "version": "==3.7.8"
+ "version": "==3.7.9"
},
"flake8-annotations": {
"hashes": [
@@ -738,6 +747,7 @@
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
+ "markers": "python_version < '3.8'",
"version": "==0.23"
},
"mccabe": {
@@ -770,11 +780,11 @@
},
"pre-commit": {
"hashes": [
- "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f",
- "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a"
+ "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e",
+ "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50"
],
"index": "pypi",
- "version": "==1.18.3"
+ "version": "==1.20.0"
},
"pycodestyle": {
"hashes": [
@@ -799,10 +809,10 @@
},
"pyparsing": {
"hashes": [
- "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
- "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
+ "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
+ "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
],
- "version": "==2.4.2"
+ "version": "==2.4.5"
},
"pyyaml": {
"hashes": [
@@ -841,10 +851,10 @@
},
"six": {
"hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
+ "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
],
- "version": "==1.12.0"
+ "version": "==1.13.0"
},
"snowballstemmer": {
"hashes": [
@@ -862,31 +872,36 @@
},
"typed-ast": {
"hashes": [
+ "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
+ "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
+ "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
+ "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
+ "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"version": "==1.4.0"
},
"unittest-xml-reporting": {
"hashes": [
- "sha256:140982e4b58e4052d9ecb775525b246a96bfc1fc26097806e05ea06e9166dd6c",
- "sha256:d1fbc7a1b6c6680ccfe75b5e9701e5431c646970de049e687b4bb35ba4325d72"
+ "sha256:358bbdaf24a26d904cc1c26ef3078bca7fc81541e0a54c8961693cc96a6f35e0",
+ "sha256:9d28ddf6524cf0ff9293f61bd12e792de298f8561a5c945acea63fb437789e0e"
],
"index": "pypi",
- "version": "==2.5.1"
+ "version": "==2.5.2"
},
"urllib3": {
"hashes": [
@@ -898,10 +913,10 @@
},
"virtualenv": {
"hashes": [
- "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30",
- "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"
+ "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589",
+ "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136"
],
- "version": "==16.7.5"
+ "version": "==16.7.7"
},
"zipp": {
"hashes": [
diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py
index 65cabe46f..e5b3a4062 100644
--- a/bot/cogs/doc.py
+++ b/bot/cogs/doc.py
@@ -4,17 +4,20 @@ import logging
import re
import textwrap
from collections import OrderedDict
+from contextlib import suppress
from typing import Any, Callable, Optional, Tuple
import discord
from bs4 import BeautifulSoup
-from bs4.element import PageElement
+from bs4.element import PageElement, Tag
+from discord.errors import NotFound
from discord.ext import commands
from markdownify import MarkdownConverter
-from requests import ConnectionError
+from requests import ConnectTimeout, ConnectionError, HTTPError
from sphinx.ext import intersphinx
+from urllib3.exceptions import ProtocolError
-from bot.constants import MODERATION_ROLES
+from bot.constants import MODERATION_ROLES, RedirectOutput
from bot.converters import ValidPythonIdentifier, ValidURL
from bot.decorators import with_role
from bot.pagination import LinePaginator
@@ -23,10 +26,33 @@ from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
logging.getLogger('urllib3').setLevel(logging.WARNING)
-
-UNWANTED_SIGNATURE_SYMBOLS = ('[source]', '¶')
+NO_OVERRIDE_GROUPS = (
+ "2to3fixer",
+ "token",
+ "label",
+ "pdbcommand",
+ "term",
+)
+NO_OVERRIDE_PACKAGES = (
+ "python",
+)
+
+SEARCH_END_TAG_ATTRS = (
+ "data",
+ "function",
+ "class",
+ "exception",
+ "seealso",
+ "section",
+ "rubric",
+ "sphinxsidebar",
+)
+UNWANTED_SIGNATURE_SYMBOLS_RE = re.compile(r"\[source]|\\\\|¶")
WHITESPACE_AFTER_NEWLINES_RE = re.compile(r"(?<=\n\n)(\s+)")
+FAILED_REQUEST_RETRY_AMOUNT = 3
+NOT_FOUND_DELETE_DELAY = RedirectOutput.delete_delay
+
def async_cache(max_size: int = 128, arg_offset: int = 0) -> Callable:
"""
@@ -125,6 +151,7 @@ class Doc(commands.Cog):
self.base_urls = {}
self.bot = bot
self.inventories = {}
+ self.renamed_symbols = set()
self.bot.loop.create_task(self.init_refresh_inventory())
@@ -150,13 +177,32 @@ class Doc(commands.Cog):
"""
self.base_urls[package_name] = base_url
- fetch_func = functools.partial(intersphinx.fetch_inventory, config, '', inventory_url)
- for _, value in (await self.bot.loop.run_in_executor(None, fetch_func)).items():
- # Each value has a bunch of information in the form
- # `(package_name, version, relative_url, ???)`, and we only
- # need the relative documentation URL.
- for symbol, (_, _, relative_doc_url, _) in value.items():
+ package = await self._fetch_inventory(inventory_url, config)
+ if not package:
+ return None
+
+ for group, value in package.items():
+ for symbol, (package_name, _version, relative_doc_url, _) in value.items():
absolute_doc_url = base_url + relative_doc_url
+
+ if symbol in self.inventories:
+ group_name = group.split(":")[1]
+ symbol_base_url = self.inventories[symbol].split("/", 3)[2]
+ if (
+ group_name in NO_OVERRIDE_GROUPS
+ or any(package in symbol_base_url for package in NO_OVERRIDE_PACKAGES)
+ ):
+
+ symbol = f"{group_name}.{symbol}"
+ # If renamed `symbol` already exists, add library name in front to differentiate between them.
+ if symbol in self.renamed_symbols:
+ # Split `package_name` because of packages like Pillow that have spaces in them.
+ symbol = f"{package_name.split()[0]}.{symbol}"
+
+ self.inventories[symbol] = absolute_doc_url
+ self.renamed_symbols.add(symbol)
+ continue
+
self.inventories[symbol] = absolute_doc_url
log.trace(f"Fetched inventory for {package_name}.")
@@ -170,6 +216,7 @@ class Doc(commands.Cog):
# Also, reset the cache used for fetching documentation.
self.base_urls.clear()
self.inventories.clear()
+ self.renamed_symbols.clear()
async_cache.cache = OrderedDict()
# Since Intersphinx is intended to be used with Sphinx,
@@ -185,16 +232,15 @@ class Doc(commands.Cog):
]
await asyncio.gather(*coros)
- async def get_symbol_html(self, symbol: str) -> Optional[Tuple[str, str]]:
+ async def get_symbol_html(self, symbol: str) -> Optional[Tuple[list, str]]:
"""
Given a Python symbol, return its signature and description.
- Returns a tuple in the form (str, str), or `None`.
-
The first tuple element is the signature of the given symbol as a markup-free string, and
the second tuple element is the description of the given symbol with HTML markup included.
- If the given symbol could not be found, returns `None`.
+ If the given symbol is a module, returns a tuple `(None, str)`
+ else if the symbol could not be found, returns `None`.
"""
url = self.inventories.get(symbol)
if url is None:
@@ -207,21 +253,38 @@ class Doc(commands.Cog):
symbol_id = url.split('#')[-1]
soup = BeautifulSoup(html, 'lxml')
symbol_heading = soup.find(id=symbol_id)
- signature_buffer = []
+ search_html = str(soup)
if symbol_heading is None:
return None
- # Traverse the tags of the signature header and ignore any
- # unwanted symbols from it. Add all of it to a temporary buffer.
- for tag in symbol_heading.strings:
- if tag not in UNWANTED_SIGNATURE_SYMBOLS:
- signature_buffer.append(tag.replace('\\', ''))
+ if symbol_id == f"module-{symbol}":
+ # Get page content from the module headerlink to the
+ # first tag that has its class in `SEARCH_END_TAG_ATTRS`
+ start_tag = symbol_heading.find("a", attrs={"class": "headerlink"})
+ if start_tag is None:
+ return [], ""
+
+ end_tag = start_tag.find_next(self._match_end_tag)
+ if end_tag is None:
+ return [], ""
+
+ description_start_index = search_html.find(str(start_tag.parent)) + len(str(start_tag.parent))
+ description_end_index = search_html.find(str(end_tag))
+ description = search_html[description_start_index:description_end_index]
+ signatures = None
- signature = ''.join(signature_buffer)
- description = str(symbol_heading.next_sibling.next_sibling).replace('¶', '')
+ else:
+ signatures = []
+ description = str(symbol_heading.find_next_sibling("dd"))
+ description_pos = search_html.find(description)
+ # Get text of up to 3 signatures, remove unwanted symbols
+ for element in [symbol_heading] + symbol_heading.find_next_siblings("dt", limit=2):
+ signature = UNWANTED_SIGNATURE_SYMBOLS_RE.sub("", element.text)
+ if signature and search_html.find(str(element)) < description_pos:
+ signatures.append(signature)
- return signature, description
+ return signatures, description.replace('¶', '')
@async_cache(arg_offset=1)
async def get_symbol_embed(self, symbol: str) -> Optional[discord.Embed]:
@@ -234,7 +297,7 @@ class Doc(commands.Cog):
if scraped_html is None:
return None
- signature = scraped_html[0]
+ signatures = scraped_html[0]
permalink = self.inventories[symbol]
description = markdownify(scraped_html[1])
@@ -242,26 +305,42 @@ class Doc(commands.Cog):
# of a double newline (interpreted as a paragraph) before index 1000.
if len(description) > 1000:
shortened = description[:1000]
- last_paragraph_end = shortened.rfind('\n\n')
- description = description[:last_paragraph_end] + f"... [read more]({permalink})"
+ last_paragraph_end = shortened.rfind('\n\n', 100)
+ if last_paragraph_end == -1:
+ last_paragraph_end = shortened.rfind('. ')
+ description = description[:last_paragraph_end]
+
+ # If there is an incomplete code block, cut it out
+ if description.count("```") % 2:
+ codeblock_start = description.rfind('```py')
+ description = description[:codeblock_start].rstrip()
+ description += f"... [read more]({permalink})"
description = WHITESPACE_AFTER_NEWLINES_RE.sub('', description)
- if not signature:
+ if signatures is None:
+ # If symbol is a module, don't show signature.
+ embed_description = description
+
+ elif not signatures:
# It's some "meta-page", for example:
# https://docs.djangoproject.com/en/dev/ref/views/#module-django.views
- return discord.Embed(
- title=f'`{symbol}`',
- url=permalink,
- description="This appears to be a generic page not tied to a specific symbol."
- )
+ embed_description = "This appears to be a generic page not tied to a specific symbol."
- signature = textwrap.shorten(signature, 500)
- return discord.Embed(
+ else:
+ embed_description = "".join(f"```py\n{textwrap.shorten(signature, 500)}```" for signature in signatures)
+ embed_description += f"\n{description}"
+
+ embed = discord.Embed(
title=f'`{symbol}`',
url=permalink,
- description=f"```py\n{signature}```{description}"
+ description=embed_description
)
+ # Show all symbols with the same name that were renamed in the footer.
+ embed.set_footer(
+ text=", ".join(renamed for renamed in self.renamed_symbols - {symbol} if renamed.endswith(f".{symbol}"))
+ )
+ return embed
@commands.group(name='docs', aliases=('doc', 'd'), invoke_without_command=True)
async def docs_group(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None:
@@ -307,7 +386,10 @@ class Doc(commands.Cog):
description=f"Sorry, I could not find any documentation for `{symbol}`.",
colour=discord.Colour.red()
)
- await ctx.send(embed=error_embed)
+ error_message = await ctx.send(embed=error_embed)
+ with suppress(NotFound):
+ await error_message.delete(delay=NOT_FOUND_DELETE_DELAY)
+ await ctx.message.delete(delay=NOT_FOUND_DELETE_DELAY)
else:
await ctx.send(embed=doc_embed)
@@ -365,6 +447,64 @@ class Doc(commands.Cog):
await self.refresh_inventory()
await ctx.send(f"Successfully deleted `{package_name}` and refreshed inventory.")
+ @docs_group.command(name="refresh", aliases=("rfsh", "r"))
+ @with_role(*MODERATION_ROLES)
+ async def refresh_command(self, ctx: commands.Context) -> None:
+ """Refresh inventories and send differences to channel."""
+ old_inventories = set(self.base_urls)
+ with ctx.typing():
+ await self.refresh_inventory()
+ # Get differences of added and removed inventories
+ added = ', '.join(inv for inv in self.base_urls if inv not in old_inventories)
+ if added:
+ added = f"+ {added}"
+
+ removed = ', '.join(inv for inv in old_inventories if inv not in self.base_urls)
+ if removed:
+ removed = f"- {removed}"
+
+ embed = discord.Embed(
+ title="Inventories refreshed",
+ description=f"```diff\n{added}\n{removed}```" if added or removed else ""
+ )
+ await ctx.send(embed=embed)
+
+ async def _fetch_inventory(self, inventory_url: str, config: SphinxConfiguration) -> Optional[dict]:
+ """Get and return inventory from `inventory_url`. If fetching fails, return None."""
+ fetch_func = functools.partial(intersphinx.fetch_inventory, config, '', inventory_url)
+ for retry in range(1, FAILED_REQUEST_RETRY_AMOUNT+1):
+ try:
+ package = await self.bot.loop.run_in_executor(None, fetch_func)
+ except ConnectTimeout:
+ log.error(
+ f"Fetching of inventory {inventory_url} timed out,"
+ f" trying again. ({retry}/{FAILED_REQUEST_RETRY_AMOUNT})"
+ )
+ except ProtocolError:
+ log.error(
+ f"Connection lost while fetching inventory {inventory_url},"
+ f" trying again. ({retry}/{FAILED_REQUEST_RETRY_AMOUNT})"
+ )
+ except HTTPError as e:
+ log.error(f"Fetching of inventory {inventory_url} failed with status code {e.response.status_code}.")
+ return None
+ except ConnectionError:
+ log.error(f"Couldn't establish connection to inventory {inventory_url}.")
+ return None
+ else:
+ return package
+ log.error(f"Fetching of inventory {inventory_url} failed.")
+ return None
+
+ @staticmethod
+ def _match_end_tag(tag: Tag) -> bool:
+ """Matches `tag` if its class value is in `SEARCH_END_TAG_ATTRS` or the tag is table."""
+ for attr in SEARCH_END_TAG_ATTRS:
+ if attr in tag.get("class", ()):
+ return True
+
+ return tag.name == "table"
+
def setup(bot: commands.Bot) -> None:
"""Doc cog load."""
diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py
index 9ce854f2c..00b988dde 100644
--- a/bot/cogs/eval.py
+++ b/bot/cogs/eval.py
@@ -148,7 +148,7 @@ class CodeEval(Cog):
self.env.update(env)
# Ignore this code, it works
- _code = """
+ code_ = """
async def func(): # (None,) -> Any
try:
with contextlib.redirect_stdout(self.stdout):
@@ -162,7 +162,7 @@ async def func(): # (None,) -> Any
""".format(textwrap.indent(code, ' '))
try:
- exec(_code, self.env) # noqa: B102,S102
+ exec(code_, self.env) # noqa: B102,S102
func = self.env['func']
res = await func()
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 4195783f1..1e7521054 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -43,7 +43,7 @@ class Filtering(Cog):
def __init__(self, bot: Bot):
self.bot = bot
- _staff_mistake_str = "If you believe this was a mistake, please let staff know!"
+ staff_mistake_str = "If you believe this was a mistake, please let staff know!"
self.filters = {
"filter_zalgo": {
"enabled": Filter.filter_zalgo,
@@ -53,7 +53,7 @@ class Filtering(Cog):
"user_notification": Filter.notify_user_zalgo,
"notification_msg": (
"Your post has been removed for abusing Unicode character rendering (aka Zalgo text). "
- f"{_staff_mistake_str}"
+ f"{staff_mistake_str}"
)
},
"filter_invites": {
@@ -63,7 +63,7 @@ class Filtering(Cog):
"content_only": True,
"user_notification": Filter.notify_user_invites,
"notification_msg": (
- f"Per Rule 6, your invite link has been removed. {_staff_mistake_str}\n\n"
+ f"Per Rule 6, your invite link has been removed. {staff_mistake_str}\n\n"
r"Our server rules can be found here: <https://pythondiscord.com/pages/rules>"
)
},
@@ -74,7 +74,7 @@ class Filtering(Cog):
"content_only": True,
"user_notification": Filter.notify_user_domains,
"notification_msg": (
- f"Your URL has been removed because it matched a blacklisted domain. {_staff_mistake_str}"
+ f"Your URL has been removed because it matched a blacklisted domain. {staff_mistake_str}"
)
},
"watch_rich_embeds": {
diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py
index 7990df226..49b61f35e 100644
--- a/bot/cogs/moderation/scheduler.py
+++ b/bot/cogs/moderation/scheduler.py
@@ -39,6 +39,8 @@ class InfractionScheduler(Scheduler):
"""Schedule expiration for previous infractions."""
await self.bot.wait_until_ready()
+ log.trace(f"Rescheduling infractions for {self.__class__.__name__}.")
+
infractions = await self.bot.api_client.get(
'bot/infractions',
params={'active': 'true'}
@@ -59,6 +61,10 @@ class InfractionScheduler(Scheduler):
# Mark as inactive if less than a minute remains.
if delta < 60:
+ log.info(
+ "Infraction will be deactivated instead of re-applied "
+ "because less than 1 minute remains."
+ )
await self.deactivate_infraction(infraction)
return
@@ -78,6 +84,9 @@ class InfractionScheduler(Scheduler):
icon = utils.INFRACTION_ICONS[infr_type][0]
reason = infraction["reason"]
expiry = infraction["expires_at"]
+ id_ = infraction['id']
+
+ log.trace(f"Applying {infr_type} infraction #{id_} to {user}.")
if expiry:
expiry = time.format_infraction(expiry)
@@ -111,10 +120,20 @@ class InfractionScheduler(Scheduler):
log_content = ctx.author.mention
if infraction["actor"] == self.bot.user.id:
+ log.trace(
+ f"Infraction #{id_} actor is bot; including the reason in the confirmation message."
+ )
+
end_msg = f" (reason: {infraction['reason']})"
elif ctx.channel.id not in STAFF_CHANNELS:
+ log.trace(
+ f"Infraction #{id_} context is not in a staff channel; omitting infraction count."
+ )
+
end_msg = ""
else:
+ log.trace(f"Fetching total infraction count for {user}.")
+
infractions = await self.bot.api_client.get(
"bot/infractions",
params={"user__id": str(user.id)}
@@ -124,6 +143,7 @@ class InfractionScheduler(Scheduler):
# Execute the necessary actions to apply the infraction on Discord.
if action_coro:
+ log.trace(f"Awaiting the infraction #{id_} application action coroutine.")
try:
await action_coro
if expiry:
@@ -136,12 +156,16 @@ class InfractionScheduler(Scheduler):
log_content = ctx.author.mention
log_title = "failed to apply"
+ log.warning(f"Failed to apply {infr_type} infraction #{id_} to {user}.")
+
# Send a confirmation message to the invoking context.
+ log.trace(f"Sending infraction #{id_} confirmation message.")
await ctx.send(
f"{dm_result}{confirm_msg} **{infr_type}** to {user.mention}{expiry_msg}{end_msg}."
)
# Send a log message to the mod log.
+ log.trace(f"Sending apply mod log for infraction #{id_}.")
await self.mod_log.send_log_message(
icon_url=icon,
colour=Colours.soft_red,
@@ -157,9 +181,14 @@ class InfractionScheduler(Scheduler):
footer=f"ID {infraction['id']}"
)
+ log.info(f"Applied {infr_type} infraction #{id_} to {user}.")
+
async def pardon_infraction(self, ctx: Context, infr_type: str, user: MemberObject) -> None:
"""Prematurely end an infraction for a user and log the action in the mod log."""
+ log.trace(f"Pardoning {infr_type} infraction for {user}.")
+
# Check the current active infraction
+ log.trace(f"Fetching active {infr_type} infractions for {user}.")
response = await self.bot.api_client.get(
'bot/infractions',
params={
@@ -170,6 +199,7 @@ class InfractionScheduler(Scheduler):
)
if not response:
+ log.debug(f"No active {infr_type} infraction found for {user}.")
await ctx.send(f":x: There's no active {infr_type} infraction for user {user.mention}.")
return
@@ -179,12 +209,16 @@ class InfractionScheduler(Scheduler):
log_text["Member"] = f"{user.mention}(`{user.id}`)"
log_text["Actor"] = str(ctx.message.author)
log_content = None
- footer = f"ID: {response[0]['id']}"
+ id_ = response[0]['id']
+ footer = f"ID: {id_}"
# If multiple active infractions were found, mark them as inactive in the database
# and cancel their expiration tasks.
if len(response) > 1:
- log.warning(f"Found more than one active {infr_type} infraction for user {user.id}")
+ log.warning(
+ f"Found more than one active {infr_type} infraction for user {user.id}; "
+ "deactivating the extra active infractions too."
+ )
footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}"
@@ -198,15 +232,15 @@ class InfractionScheduler(Scheduler):
# 1. Discord cannot store multiple active bans or assign multiples of the same role
# 2. It would send a pardon DM for each active infraction, which is redundant
for infraction in response[1:]:
- _id = infraction['id']
+ id_ = infraction['id']
try:
# Mark infraction as inactive in the database.
await self.bot.api_client.patch(
- f"bot/infractions/{_id}",
+ f"bot/infractions/{id_}",
json={"active": False}
)
except ResponseCodeError:
- log.exception(f"Failed to deactivate infraction #{_id} ({infr_type})")
+ log.exception(f"Failed to deactivate infraction #{id_} ({infr_type})")
# This is simpler and cleaner than trying to concatenate all the errors.
log_text["Failure"] = "See bot's logs for details."
@@ -227,11 +261,16 @@ class InfractionScheduler(Scheduler):
confirm_msg = ":x: failed to pardon"
log_title = "pardon failed"
log_content = ctx.author.mention
+
+ log.warning(f"Failed to pardon {infr_type} infraction #{id_} for {user}.")
else:
confirm_msg = f":ok_hand: pardoned"
log_title = "pardoned"
+ log.info(f"Pardoned {infr_type} infraction #{id_} for {user}.")
+
# Send a confirmation message to the invoking context.
+ log.trace(f"Sending infraction #{id_} pardon confirmation message.")
await ctx.send(
f"{dm_emoji}{confirm_msg} infraction **{infr_type}** for {user.mention}. "
f"{log_text.get('Failure', '')}"
@@ -265,10 +304,10 @@ class InfractionScheduler(Scheduler):
guild = self.bot.get_guild(constants.Guild.id)
mod_role = guild.get_role(constants.Roles.moderator)
user_id = infraction["user"]
- _type = infraction["type"]
- _id = infraction["id"]
+ type_ = infraction["type"]
+ id_ = infraction["id"]
- log.debug(f"Marking infraction #{_id} as inactive (expired).")
+ log.info(f"Marking infraction #{id_} as inactive (expired).")
log_content = None
log_text = {
@@ -278,24 +317,28 @@ class InfractionScheduler(Scheduler):
}
try:
+ log.trace("Awaiting the pardon action coroutine.")
returned_log = await self._pardon_action(infraction)
+
if returned_log is not None:
log_text = {**log_text, **returned_log} # Merge the logs together
else:
raise ValueError(
- f"Attempted to deactivate an unsupported infraction #{_id} ({_type})!"
+ f"Attempted to deactivate an unsupported infraction #{id_} ({type_})!"
)
except discord.Forbidden:
- log.warning(f"Failed to deactivate infraction #{_id} ({_type}): bot lacks permissions")
+ log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions")
log_text["Failure"] = f"The bot lacks permissions to do this (role hierarchy?)"
log_content = mod_role.mention
except discord.HTTPException as e:
- log.exception(f"Failed to deactivate infraction #{_id} ({_type})")
+ log.exception(f"Failed to deactivate infraction #{id_} ({type_})")
log_text["Failure"] = f"HTTPException with code {e.code}."
log_content = mod_role.mention
# Check if the user is currently being watched by Big Brother.
try:
+ log.trace(f"Determining if user {user_id} is currently being watched by Big Brother.")
+
active_watch = await self.bot.api_client.get(
"bot/infractions",
params={
@@ -312,12 +355,13 @@ class InfractionScheduler(Scheduler):
try:
# Mark infraction as inactive in the database.
+ log.trace(f"Marking infraction #{id_} as inactive in the database.")
await self.bot.api_client.patch(
- f"bot/infractions/{_id}",
+ f"bot/infractions/{id_}",
json={"active": False}
)
except ResponseCodeError as e:
- log.exception(f"Failed to deactivate infraction #{_id} ({_type})")
+ log.exception(f"Failed to deactivate infraction #{id_} ({type_})")
log_line = f"API request failed with code {e.status}."
log_content = mod_role.mention
@@ -335,12 +379,13 @@ class InfractionScheduler(Scheduler):
if send_log:
log_title = f"expiration failed" if "Failure" in log_text else "expired"
+ log.trace(f"Sending deactivation mod log for infraction #{id_}.")
await self.mod_log.send_log_message(
- icon_url=utils.INFRACTION_ICONS[_type][1],
+ icon_url=utils.INFRACTION_ICONS[type_][1],
colour=Colours.soft_green,
- title=f"Infraction {log_title}: {_type}",
+ title=f"Infraction {log_title}: {type_}",
text="\n".join(f"{k}: {v}" for k, v in log_text.items()),
- footer=f"ID: {_id}",
+ footer=f"ID: {id_}",
content=log_content,
)
diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py
index c66222e5a..9b3c62403 100644
--- a/bot/cogs/moderation/superstarify.py
+++ b/bot/cogs/moderation/superstarify.py
@@ -34,8 +34,8 @@ class Superstarify(InfractionScheduler, Cog):
return # User didn't change their nickname. Abort!
log.trace(
- f"{before.display_name} is trying to change their nickname to {after.display_name}. "
- "Checking if the user is in superstar-prison..."
+ f"{before} ({before.display_name}) is trying to change their nickname to "
+ f"{after.display_name}. Checking if the user is in superstar-prison..."
)
active_superstarifies = await self.bot.api_client.get(
@@ -48,6 +48,7 @@ class Superstarify(InfractionScheduler, Cog):
)
if not active_superstarifies:
+ log.trace(f"{before} has no active superstar infractions.")
return
infraction = active_superstarifies[0]
@@ -132,15 +133,17 @@ class Superstarify(InfractionScheduler, Cog):
# Post the infraction to the API
reason = reason or f"old nick: {member.display_name}"
infraction = await utils.post_infraction(ctx, member, "superstar", reason, duration)
+ id_ = infraction["id"]
old_nick = member.display_name
- forced_nick = self.get_nick(infraction["id"], member.id)
+ forced_nick = self.get_nick(id_, member.id)
expiry_str = format_infraction(infraction["expires_at"])
# Apply the infraction and schedule the expiration task.
+ log.debug(f"Changing nickname of {member} to {forced_nick}.")
self.mod_log.ignore(constants.Event.member_update, member.id)
await member.edit(nick=forced_nick, reason=reason)
- self.schedule_task(ctx.bot.loop, infraction["id"], infraction)
+ self.schedule_task(ctx.bot.loop, id_, infraction)
# Send a DM to the user to notify them of their new infraction.
await utils.notify_infraction(
@@ -152,6 +155,7 @@ class Superstarify(InfractionScheduler, Cog):
)
# Send an embed with the infraction information to the invoking context.
+ log.trace(f"Sending superstar #{id_} embed.")
embed = Embed(
title="Congratulations!",
colour=constants.Colours.soft_orange,
@@ -167,6 +171,7 @@ class Superstarify(InfractionScheduler, Cog):
await ctx.send(embed=embed)
# Log to the mod log channel.
+ log.trace(f"Sending apply mod log for superstar #{id_}.")
await self.mod_log.send_log_message(
icon_url=utils.INFRACTION_ICONS["superstar"][0],
colour=Colour.gold(),
@@ -180,7 +185,7 @@ class Superstarify(InfractionScheduler, Cog):
Old nickname: `{old_nick}`
New nickname: `{forced_nick}`
"""),
- footer=f"ID {infraction['id']}"
+ footer=f"ID {id_}"
)
@command(name="unsuperstarify", aliases=("release_nick", "unstar"))
@@ -198,6 +203,10 @@ class Superstarify(InfractionScheduler, Cog):
# Don't bother sending a notification if the user left the guild.
if not user:
+ log.debug(
+ "User left the guild and therefore won't be notified about superstar "
+ f"{infraction['id']} pardon."
+ )
return {}
# DM the user about the expiration.
@@ -216,6 +225,8 @@ class Superstarify(InfractionScheduler, Cog):
@staticmethod
def get_nick(infraction_id: int, member_id: int) -> str:
"""Randomly select a nickname from the Superstarify nickname list."""
+ log.trace(f"Choosing a random nickname for superstar #{infraction_id}.")
+
rng = random.Random(str(infraction_id) + str(member_id))
return rng.choice(STAR_NAMES)
diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py
index 9179c0afb..325b9567a 100644
--- a/bot/cogs/moderation/utils.py
+++ b/bot/cogs/moderation/utils.py
@@ -37,6 +37,8 @@ def proxy_user(user_id: str) -> discord.Object:
Used when a Member or User object cannot be resolved.
"""
+ log.trace(f"Attempting to create a proxy user for the user id {user_id}.")
+
try:
user_id = int(user_id)
except ValueError:
@@ -59,6 +61,8 @@ async def post_infraction(
active: bool = True,
) -> t.Optional[dict]:
"""Posts an infraction to the API."""
+ log.trace(f"Posting {infr_type} infraction for {user} to the API.")
+
payload = {
"actor": ctx.message.author.id,
"hidden": hidden,
@@ -92,6 +96,8 @@ async def post_infraction(
async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str) -> bool:
"""Checks if a user already has an active infraction of the given type."""
+ log.trace(f"Checking if {user} has active infractions of type {infr_type}.")
+
active_infractions = await ctx.bot.api_client.get(
'bot/infractions',
params={
@@ -101,12 +107,14 @@ async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str
}
)
if active_infractions:
+ log.trace(f"{user} has active infractions of type {infr_type}.")
await ctx.send(
f":x: According to my records, this user already has a {infr_type} infraction. "
f"See infraction **#{active_infractions[0]['id']}**."
)
return True
else:
+ log.trace(f"{user} does not have active infractions of type {infr_type}.")
return False
@@ -118,6 +126,8 @@ async def notify_infraction(
icon_url: str = Icons.token_removed
) -> bool:
"""DM a user about their new infraction and return True if the DM is successful."""
+ log.trace(f"Sending {user} a DM about their {infr_type} infraction.")
+
embed = discord.Embed(
description=textwrap.dedent(f"""
**Type:** {infr_type.capitalize()}
@@ -146,6 +156,8 @@ async def notify_pardon(
icon_url: str = Icons.user_verified
) -> bool:
"""DM a user about their pardoned infraction and return True if the DM is successful."""
+ log.trace(f"Sending {user} a DM about their pardoned infraction.")
+
embed = discord.Embed(
description=content,
colour=Colours.soft_green
diff --git a/bot/interpreter.py b/bot/interpreter.py
index a42b45a2d..76a3fc293 100644
--- a/bot/interpreter.py
+++ b/bot/interpreter.py
@@ -20,8 +20,8 @@ class Interpreter(InteractiveInterpreter):
write_callable = None
def __init__(self, bot: Bot):
- _locals = {"bot": bot}
- super().__init__(_locals)
+ locals_ = {"bot": bot}
+ super().__init__(locals_)
async def run(self, code: str, ctx: Context, io: StringIO, *args, **kwargs) -> Any:
"""Execute the provided source code as the bot & return the output."""
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index 08abd91d7..ee6c0a8e6 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -36,11 +36,15 @@ class Scheduler(metaclass=CogABCMeta):
`task_data` is passed to `Scheduler._scheduled_expiration`
"""
if task_id in self.scheduled_tasks:
+ log.debug(
+ f"{self.cog_name}: did not schedule task #{task_id}; task was already scheduled."
+ )
return
task: asyncio.Task = create_task(loop, self._scheduled_task(task_data))
self.scheduled_tasks[task_id] = task
+ log.debug(f"{self.cog_name}: scheduled task #{task_id}.")
def cancel_task(self, task_id: str) -> None:
"""Un-schedules a task."""
@@ -51,7 +55,7 @@ class Scheduler(metaclass=CogABCMeta):
return
task.cancel()
- log.debug(f"{self.cog_name}: Unscheduled {task_id}.")
+ log.debug(f"{self.cog_name}: unscheduled task #{task_id}.")
del self.scheduled_tasks[task_id]