diff options
| -rw-r--r-- | .pre-commit-config.yaml | 5 | ||||
| -rw-r--r-- | Pipfile | 7 | ||||
| -rw-r--r-- | Pipfile.lock | 419 | ||||
| -rw-r--r-- | bot/__main__.py | 1 | ||||
| -rw-r--r-- | bot/cogs/alias.py | 2 | ||||
| -rw-r--r-- | bot/cogs/antispam.py | 35 | ||||
| -rw-r--r-- | bot/cogs/bigbrother.py | 62 | ||||
| -rw-r--r-- | bot/cogs/bot.py | 3 | ||||
| -rw-r--r-- | bot/cogs/defcon.py | 40 | ||||
| -rw-r--r-- | bot/cogs/eval.py | 2 | ||||
| -rw-r--r-- | bot/cogs/events.py | 32 | ||||
| -rw-r--r-- | bot/cogs/filtering.py | 134 | ||||
| -rw-r--r-- | bot/cogs/free.py | 127 | ||||
| -rw-r--r-- | bot/cogs/help.py | 12 | ||||
| -rw-r--r-- | bot/cogs/moderation.py | 133 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 65 | ||||
| -rw-r--r-- | bot/cogs/reminders.py | 2 | ||||
| -rw-r--r-- | bot/cogs/snekbox.py | 26 | ||||
| -rw-r--r-- | bot/cogs/token_remover.py | 5 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 63 | ||||
| -rw-r--r-- | bot/constants.py | 25 | ||||
| -rw-r--r-- | bot/decorators.py | 35 | ||||
| -rw-r--r-- | bot/pagination.py | 26 | ||||
| -rw-r--r-- | config-default.yml | 35 | 
24 files changed, 963 insertions, 333 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..1d75342a2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +-   repo: https://github.com/pre-commit/pre-commit-hooks +    rev: v2.0.0 +    hooks: +    - id: flake8
\ No newline at end of file @@ -4,7 +4,7 @@ verify_ssl = true  name = "pypi"  [packages] -discord-py = {git = "https://github.com/Rapptz/discord.py.git", extras = ["voice"], ref = "860d6a9ace8248dfeec18b8b159e7b757d9f56bb", editable = true} +discord-py = {git = "https://github.com/Rapptz/discord.py.git",extras = ["voice"],ref = "860d6a9ace8248dfeec18b8b159e7b757d9f56bb",editable = true}  dulwich = "*"  aiodns = "*"  logmatic-python = "*" @@ -29,6 +29,7 @@ requests = "*"  "flake8-string-format" = "*"  safety = "*"  dodgy = "*" +pre-commit = "*"  [requires]  python_version = "3.6" @@ -36,12 +37,10 @@ python_version = "3.6"  [scripts]  start = "python -m bot"  lint = "python -m flake8" - +precommit = "pre-commit install"  build = "docker build -t pythondiscord/bot:latest -f docker/bot.Dockerfile ."  push = "docker push pythondiscord/bot:latest" -  buildbase = "docker build -t pythondiscord/bot-base:latest -f docker/base.Dockerfile ."  pushbase = "docker push pythondiscord/bot-base:latest" -  buildci = "docker build -t pythondiscord/bot-ci:latest -f docker/ci.Dockerfile ."  pushci = "docker push pythondiscord/bot-ci:latest" diff --git a/Pipfile.lock b/Pipfile.lock index 506b17065..92566c3ed 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "79a3c633f145dbf93ba5b2460d3f49346495328af7302e59be326e9324785cf3" +            "sha256": "d53f89c6d3b32ccbc2dadaff1a7e9ee1bdcbd1df9cddab35def36bcceec98b27"          },          "pipfile-spec": 6,          "requires": { @@ -18,11 +18,11 @@      "default": {          "aio-pika": {              "hashes": [ -                "sha256:6438e72963e459552f196a07a081a5f6dc54d42a474292b8497bd4a59554fc85", -                "sha256:dc15b451dca6d2b1c504ab353e3f2fe7e7e252fdb1c219261b5412e1cafbc72d" +                "sha256:c3eb639f7fc5c96355e7a227380989c9e0f342bb6612e6671ea76d188813ba45", +                "sha256:ea26efd262d7c4cd4ac00fb968ede89e82c00ad331b47415e3c2353a4b91cbe0"              ],              "index": "pypi", -            "version": "==4.6.3" +            "version": "==4.9.1"          },          "aiodns": {              "hashes": [ @@ -57,6 +57,7 @@                  "sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",                  "sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"              ], +            "index": "pypi",              "version": "==3.4.4"          },          "alabaster": { @@ -89,18 +90,18 @@          },          "beautifulsoup4": {              "hashes": [ -                "sha256:194ec62a25438adcb3fdb06378b26559eda1ea8a747367d34c33cef9c7f48d57", -                "sha256:90f8e61121d6ae58362ce3bed8cd997efb00c914eae0ff3d363c32f9a9822d10", -                "sha256:f0abd31228055d698bb392a826528ea08ebb9959e6bea17c606fd9c9009db938" +                "sha256:1ed70a0e99742653953d68462378a1a8eb65dca5f7c8fa44a05a2a0b3545df67", +                "sha256:6a7f5e0efc563cd1ffeefba6d528b97aa0d313c02dd126ba6c455e5fe5bd48eb", +                "sha256:e394827904cc4923f443e8dd2e9968343669c8e1ad7a8d62d7541e780884acb8"              ], -            "version": "==4.6.3" +            "version": "==4.7.0"          },          "certifi": {              "hashes": [ -                "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", -                "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" +                "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", +                "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"              ], -            "version": "==2018.10.15" +            "version": "==2018.11.29"          },          "cffi": {              "hashes": [ @@ -188,17 +189,10 @@          },          "idna": {              "hashes": [ -                "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", -                "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" -            ], -            "version": "==2.7" -        }, -        "idna-ssl": { -            "hashes": [ -                "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" +                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", +                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"              ], -            "markers": "python_version < '3.7'", -            "version": "==1.1.0" +            "version": "==2.8"          },          "imagesize": {              "hashes": [ @@ -231,39 +225,39 @@          },          "lxml": {              "hashes": [ -                "sha256:02bc220d61f46e9b9d5a53c361ef95e9f5e1d27171cd461dddb17677ae2289a5", -                "sha256:22f253b542a342755f6cfc047fe4d3a296515cf9b542bc6e261af45a80b8caf6", -                "sha256:2f31145c7ff665b330919bfa44aacd3a0211a76ca7e7b441039d2a0b0451e415", -                "sha256:36720698c29e7a9626a0dc802ef8885f8f0239bfd1689628ecd459a061f2807f", -                "sha256:438a1b0203545521f6616132bfe0f4bca86f8a401364008b30e2b26ec408ce85", -                "sha256:4815892904c336bbaf73dafd54f45f69f4021c22b5bad7332176bbf4fb830568", -                "sha256:5be031b0f15ad63910d8e5038b489d95a79929513b3634ad4babf77100602588", -                "sha256:5c93ae37c3c588e829b037fdfbd64a6e40c901d3f93f7beed6d724c44829a3ad", -                "sha256:60842230678674cdac4a1cf0f707ef12d75b9a4fc4a565add4f710b5fcf185d5", -                "sha256:62939a8bb6758d1bf923aa1c13f0bcfa9bf5b2fc0f5fa917a6e25db5fe0cfa4e", -                "sha256:75830c06a62fe7b8fe3bbb5f269f0b308f19f3949ac81cfd40062f47c1455faf", -                "sha256:81992565b74332c7c1aff6a913a3e906771aa81c9d0c68c68113cffcae45bc53", -                "sha256:8c892fb0ee52c594d9a7751c7d7356056a9682674b92cc1c4dc968ff0f30c52f", -                "sha256:9d862e3cf4fc1f2837dedce9c42269c8c76d027e49820a548ac89fdcee1e361f", -                "sha256:a623965c086a6e91bb703d4da62dabe59fe88888e82c4117d544e11fd74835d6", -                "sha256:a7783ab7f6a508b0510490cef9f857b763d796ba7476d9703f89722928d1e113", -                "sha256:aab09fbe8abfa3b9ce62aaf45aca2d28726b1b9ee44871dbe644050a2fff4940", -                "sha256:abf181934ac3ef193832fb973fd7f6149b5c531903c2ec0f1220941d73eee601", -                "sha256:ae07fa0c115733fce1e9da96a3ac3fa24801742ca17e917e0c79d63a01eeb843", -                "sha256:b9c78242219f674ab645ec571c9a95d70f381319a23911941cd2358a8e0521cf", -                "sha256:bccb267678b870d9782c3b44d0cefe3ba0e329f9af8c946d32bf3778e7a4f271", -                "sha256:c4df4d27f4c93b2cef74579f00b1d3a31a929c7d8023f870c4b476f03a274db4", -                "sha256:caf0e50b546bb60dfa99bb18dfa6748458a83131ecdceaf5c071d74907e7e78a", -                "sha256:d3266bd3ac59ac4edcd5fa75165dee80b94a3e5c91049df5f7c057ccf097551c", -                "sha256:db0d213987bcd4e6d41710fb4532b22315b0d8fb439ff901782234456556aed1", -                "sha256:dbbd5cf7690a40a9f0a9325ab480d0fccf46d16b378eefc08e195d84299bfae1", -                "sha256:e16e07a0ec3a75b5ee61f2b1003c35696738f937dc8148fbda9fe2147ccb6e61", -                "sha256:e175a006725c7faadbe69e791877d09936c0ef2cf49d01b60a6c1efcb0e8be6f", -                "sha256:edd9c13a97f6550f9da2236126bb51c092b3b1ce6187f2bd966533ad794bbb5e", -                "sha256:fa39ea60d527fbdd94215b5e5552f1c6a912624521093f1384a491a8ad89ad8b" +                "sha256:16cf8bac33ec17049617186d63006ba49da7c5be417042877a49f0ef6d7a195d", +                "sha256:18f2d8f14cc61e66e8a45f740d15b6fc683c096f733db1f8d0ee15bcac9843de", +                "sha256:260868f69d14a64dd1de9cf92e133d2f71514d288de4906f109bdf48ca9b756a", +                "sha256:29b8acd8ecdf772266dbac491f203c71664b0b07ad4309ba2c3bb131306332fc", +                "sha256:2b05e5e06f8e8c63595472dc887d0d6e0250af754a35ba690f6a6abf2ef85691", +                "sha256:30d6ec05fb607a5b7345549f642c7c7a5b747b634f6d5e935596b910f243f96f", +                "sha256:3bf683f0237449ebc1851098f664410e3c99ba3faa8c9cc82c6acfe857df1767", +                "sha256:3ce5488121eb15513c4b239dadd67f9e7959511bd766aac6be0c35e80274f298", +                "sha256:48be0c375350a5519bb9474b42a9c0e7ab709fb45f11bfcd33de876791137896", +                "sha256:49bc343ca3b30cd860845433bb9f62448a54ff87b632175108bacbc5dc63e49e", +                "sha256:4cc7531e86a43ea66601763c5914c3d3adb297f32e4284957609b90d41825fca", +                "sha256:4e9822fad564d82035f0b6d701a890444560210f8a8648b8f15850f8fe883cd9", +                "sha256:51a9a441aefc8c93512bad5efe867d2ff086e7249ce0fc3b47c310644b352936", +                "sha256:5bbed9efc8aeb69929140f71a30e655bf496b45b766861513960e1b11168d475", +                "sha256:60a5323b2bc893ca1059d283d6695a172d51cc95a70c25b3e587e1aad5459c38", +                "sha256:7035d9361f3ceec9ccc1dd3482094d1174580e7e1bf6870b77ea758f7cad15d2", +                "sha256:76d62cc048bda0ebf476689ad3eb8e65e6827e43a7521be3b163071020667b8c", +                "sha256:78163b578e6d1836012febaa1865e095ccc7fc826964dd69a2dbfe401618a1f7", +                "sha256:83b58b2b5904d50de03a47e2f56d24e9da4cf7e3b0d66fb4510b18fca0faf910", +                "sha256:a07447e46fffa5bb4d7a0af0a6505c8517e9bd197cfd2aec79e499b6e86cde49", +                "sha256:a17d808b3edca4aaf6b295b5a388c844a0b7f79aca2d79eec5acc1461db739e3", +                "sha256:a378fd61022cf4d3b492134c3bc48204ac2ff19e0813b23e07c3dd95ae8df0bc", +                "sha256:aa7d096a44ae3d475c5ed763e24cf302d32462e78b61bba73ce1ad0efb8f522a", +                "sha256:ade8785c93a985956ba6499d5ea6d0a362e24b4a9ba07dd18920fd67cccf63ea", +                "sha256:cc039668f91d8af8c4094cfb5a67c7ae733967fdc84c0507fe271db81480d367", +                "sha256:d89f1ffe98744c4b5c11f00fb843a4e72f68a6279b5e38168167f1b3c0fdd84c", +                "sha256:e691b6ef6e27437860016bd6c32e481bdc2ed3af03289707a38b9ca422105f40", +                "sha256:e750da6ac3ca624ae3303df448664012f9b6f9dfbc5d50048ea8a12ce2f8bc29", +                "sha256:eca305b200549906ea25648463aeb1b3b220b716415183eaa99c998a846936d9", +                "sha256:f52fe795e08858192eea167290033b5ff24f50f51781cb78d989e8d63cfe73d1"              ],              "index": "pypi", -            "version": "==4.2.5" +            "version": "==4.2.6"          },          "markdownify": {              "hashes": [ @@ -307,37 +301,37 @@          },          "multidict": {              "hashes": [ -                "sha256:013eb6591ab95173fd3deb7667d80951abac80100335b3e97b5fa778c1bb4b91", -                "sha256:0bffbbbb48db35f57dfb4733e943ac8178efb31aab5601cb7b303ee228ce96af", -                "sha256:1a34aab1dfba492407c757532f665ba3282ec4a40b0d2f678bda828ef422ebb7", -                "sha256:1b4b46a33f459a2951b0fd26c2d80639810631eb99b3d846d298b02d28a3e31d", -                "sha256:1d616d80c37a388891bf760d64bc50cac7c61dbb7d7013f2373aa4b44936e9f0", -                "sha256:225aefa7befbe05bd0116ef87e8cd76cbf4ac39457a66faf7fb5f3c2d7bea19a", -                "sha256:2c9b28985ef7c830d5c7ea344d068bcdee22f8b6c251369dea98c3a814713d44", -                "sha256:39e0600f8dd72acb011d09960da560ba3451b1eca8de5557c15705afc9d35f0e", -                "sha256:3c642c40ea1ca074397698446893a45cd6059d5d071fc3ba3915c430c125320f", -                "sha256:42357c90b488fac38852bcd7b31dcd36b1e2325413960304c28b8d98e6ff5fd4", -                "sha256:6ac668f27dbdf8a69c31252f501e128a69a60b43a44e43d712fb58ce3e5dfcca", -                "sha256:713683da2e3f1dd81a920c995df5dda51f1fff2b3995f5864c3ee782fcdcb96c", -                "sha256:73b6e7853b6d3bc0eac795044e700467631dff37a5a33d3230122b03076ac2f9", -                "sha256:77534c1b9f4a5d0962392cad3f668d1a04036b807618e3357eb2c50d8b05f7f7", -                "sha256:77b579ef57e27457064bb6bb4c8e5ede866af071af60fe3576226136048c6dfa", -                "sha256:82cf28f18c935d66c15a6f82fda766a4138d21e78532a1946b8ec603019ba0b8", -                "sha256:937e8f12f9edc0d2e351c09fc3e7335a65eefb75406339d488ee46ef241f75d8", -                "sha256:985dbf59e92f475573a04598f9a00f92b4fdb64fc41f1df2ea6f33b689319537", -                "sha256:9c4fab7599ba8c0dbf829272c48c519625c2b7f5630b49925802f1af3a77f1f4", -                "sha256:9e8772be8455b49a85ad6dbf6ce433da7856ba481d6db36f53507ae540823b15", -                "sha256:a06d6d88ce3be4b54deabd078810e3c077a8b2e20f0ce541c979b5dd49337031", -                "sha256:a1da0cdc3bc45315d313af976dab900888dbb477d812997ee0e6e4ea43d325e5", -                "sha256:a6652466a4800e9fde04bf0252e914fff5f05e2a40ee1453db898149624dfe04", -                "sha256:a7f23523ea6a01f77e0c6da8aae37ab7943e35630a8d2eda7e49502f36b51b46", -                "sha256:a87429da49f4c9fb37a6a171fa38b59a99efdeabffb34b4255a7a849ffd74a20", -                "sha256:c26bb81d0d19619367a96593a097baec2d5a7b3a0cfd1e3a9470277505a465c2", -                "sha256:d4f4545edb4987f00fde44241cef436bf6471aaac7d21c6bbd497cca6049f613", -                "sha256:daabc2766a2b76b3bec2086954c48d5f215f75a335eaee1e89c8357922a3c4d5", -                "sha256:f08c1dcac70b558183b3b755b92f1135a76fd1caa04009b89ddea57a815599aa" -            ], -            "version": "==4.5.1" +                "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", +                "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", +                "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", +                "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", +                "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", +                "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", +                "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", +                "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", +                "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", +                "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", +                "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", +                "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", +                "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", +                "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", +                "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", +                "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", +                "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", +                "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", +                "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", +                "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", +                "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", +                "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", +                "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", +                "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", +                "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", +                "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", +                "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", +                "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", +                "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" +            ], +            "version": "==4.5.2"          },          "packaging": {              "hashes": [ @@ -348,78 +342,81 @@          },          "pillow": {              "hashes": [ -                "sha256:00203f406818c3f45d47bb8fe7e67d3feddb8dcbbd45a289a1de7dd789226360", -                "sha256:0616f800f348664e694dddb0b0c88d26761dd5e9f34e1ed7b7a7d2da14b40cb7", -                "sha256:1f7908aab90c92ad85af9d2fec5fc79456a89b3adcc26314d2cde0e238bd789e", -                "sha256:2ea3517cd5779843de8a759c2349a3cd8d3893e03ab47053b66d5ec6f8bc4f93", -                "sha256:48a9f0538c91fc136b3a576bee0e7cd174773dc9920b310c21dcb5519722e82c", -                "sha256:5280ebc42641a1283b7b1f2c20e5b936692198b9dd9995527c18b794850be1a8", -                "sha256:5e34e4b5764af65551647f5cc67cf5198c1d05621781d5173b342e5e55bf023b", -                "sha256:63b120421ab85cad909792583f83b6ca3584610c2fe70751e23f606a3c2e87f0", -                "sha256:696b5e0109fe368d0057f484e2e91717b49a03f1e310f857f133a4acec9f91dd", -                "sha256:870ed021a42b1b02b5fe4a739ea735f671a84128c0a666c705db2cb9abd528eb", -                "sha256:916da1c19e4012d06a372127d7140dae894806fad67ef44330e5600d77833581", -                "sha256:9303a289fa0811e1c6abd9ddebfc770556d7c3311cb2b32eff72164ddc49bc64", -                "sha256:9577888ecc0ad7d06c3746afaba339c94d62b59da16f7a5d1cff9e491f23dace", -                "sha256:987e1c94a33c93d9b209315bfda9faa54b8edfce6438a1e93ae866ba20de5956", -                "sha256:99a3bbdbb844f4fb5d6dd59fac836a40749781c1fa63c563bc216c27aef63f60", -                "sha256:99db8dc3097ceafbcff9cb2bff384b974795edeb11d167d391a02c7bfeeb6e16", -                "sha256:a5a96cf49eb580756a44ecf12949e52f211e20bffbf5a95760ac14b1e499cd37", -                "sha256:aa6ca3eb56704cdc0d876fc6047ffd5ee960caad52452fbee0f99908a141a0ae", -                "sha256:aade5e66795c94e4a2b2624affeea8979648d1b0ae3fcee17e74e2c647fc4a8a", -                "sha256:b78905860336c1d292409e3df6ad39cc1f1c7f0964e66844bbc2ebfca434d073", -                "sha256:b92f521cdc4e4a3041cc343625b699f20b0b5f976793fb45681aac1efda565f8", -                "sha256:bfde84bbd6ae5f782206d454b67b7ee8f7f818c29b99fd02bf022fd33bab14cb", -                "sha256:c2b62d3df80e694c0e4a0ed47754c9480521e25642251b3ab1dff050a4e60409", -                "sha256:c5e2be6c263b64f6f7656e23e18a4a9980cffc671442795682e8c4e4f815dd9f", -                "sha256:c99aa3c63104e0818ec566f8ff3942fb7c7a8f35f9912cb63fd8e12318b214b2", -                "sha256:dae06620d3978da346375ebf88b9e2dd7d151335ba668c995aea9ed07af7add4", -                "sha256:db5499d0710823fa4fb88206050d46544e8f0e0136a9a5f5570b026584c8fd74", -                "sha256:f36baafd82119c4a114b9518202f2a983819101dcc14b26e43fc12cbefdce00e", -                "sha256:f52b79c8796d81391ab295b04e520bda6feed54d54931708872e8f9ae9db0ea1", -                "sha256:ff8cff01582fa1a7e533cb97f628531c4014af4b5f38e33cdcfe5eec29b6d888" +                "sha256:0cd42fe2d99ec6ce23aaf00947a7b7956ad2ed4b1695fd37545c3b8eae06d95a", +                "sha256:137bed8972089d65da63fb79b4949b0f2b99e9a58f1b494e82be43ba8b0f4226", +                "sha256:14eb2b2e4f2a14f5c89fd0edf55c5af0bf1a40fdf3838d81867f26f131cd557d", +                "sha256:1fc43ce8c4fa3754222cd6831d599ad17ca2fc9868d2fb52f4e5362dfbfaf379", +                "sha256:26dfeee23a86dad6277a63d18f61f53b957cb2cd3506dbbd74b88ba2fa65b3b1", +                "sha256:2e0e582942e025cc58f669499a8e0bffde5bcc8d42b65729f294c1dac54e4672", +                "sha256:3bb8dd3ce101dd8b0b37eaae924a5bb93abb6ffdd034bf68a066a808e11768ab", +                "sha256:3f07da3874f0b085421f1d4f979785131aa9d497501d8610d82f7378b33858f8", +                "sha256:429b2b5ae5f57f8fd9ec2e012c1e7b342ff10f1a8977dc291976b9a3b4c096e1", +                "sha256:4a000fdd89d77b6b675de27e1ab91c6fba517c08f19ee83e6716b78930634e04", +                "sha256:4ccbe7cce6156391a3ecf447c79a7d4a1a0ecd3de79bdec9ca5e4f7242a306d1", +                "sha256:4d08034196db41acb7392e4fccfc0448e7a87192c41d3011ad4093eac2c31ffd", +                "sha256:6b202b1cb524bc76ed52a7eb0314f4b0a0497c7cceb9a93539b5a25800e1f2b6", +                "sha256:8563b56fa7c34f1606848c2143ea67d27cf225b9726a1b041c3d27cf85e46edc", +                "sha256:86d7421e8803d7bae2e594765c378a867b629d46b32fbfe5ed9fd95b30989feb", +                "sha256:8d4bddedcb4ab99131d9705a75720efc48b3d006122dae1a4cc329496ac47c9a", +                "sha256:a4929c6de9590635c34533609402c9da12b22bfc2feb8c0c4f38c39bab48a9ad", +                "sha256:b0736e21798448cee3e663c0df7a6dfa83d805b3f3a45e67f7457a2f019e5fca", +                "sha256:b669acba91d47395de84c9ca52a7ad393b487e5ae2e20b9b2790b22a57d479fa", +                "sha256:bba993443921f2d077195b425a3283357f52b07807d53704610c1249d20b183a", +                "sha256:bdf706a93d00547c9443b2654ae424fd54d5dece4bc4333e7035740aeb7a7cea", +                "sha256:c5aa93e55175b9cde95279ccd03c93d218976b376480222d37be41d2c9c54510", +                "sha256:cc11fd997d8ad71bb0412e983b711e49639c2ddba9b9dce04d4bdab575fe5f84", +                "sha256:d584f1c33995c3dc16a35e30ef43e0881fa0d085f0fef29cebf154ffb5643363", +                "sha256:d88f54bdefb7ddccb68efdd710d689aa6a09b875cc3e44b7e81ef54e0751e3a7", +                "sha256:de0d323072be72fa4d74f4e013cd594e3f8ee03b2e0eac5876a3249fa076ef7b", +                "sha256:f139c963c6679d236b2c45369524338eabd36a853fe23abd39ba246ab0a75aec", +                "sha256:f41c0bf667c4c1c30b873eaa8d6bb894f6d721b3e38e9c993bddd1263c02fb1f", +                "sha256:fbd0ea468b4ec04270533bf5206f1cd57746fcf226520bb133318fa276de2644", +                "sha256:fe2d2850521c467c915ff0a6e27dc64c3c04c2f66612e0072672bd1bd4854b61"              ],              "index": "pypi", -            "version": "==5.3.0" +            "version": "==5.4.0"          },          "pycares": {              "hashes": [ -                "sha256:0e81c971236bb0767354f1456e67ab6ae305f248565ce77cd413a311f9572bf5", -                "sha256:11c0ff3ccdb5a838cbd59a4e59df35d31355a80a61393bca786ca3b44569ba10", -                "sha256:170d62bd300999227e64da4fa85459728cc96e62e44780bbc86a915fdae01f78", -                "sha256:36f4c03df57c41a87eb3d642201684eb5a8bc194f4bafaa9f60ee6dc0aef8e40", -                "sha256:371ce688776da984c4105c8ca760cc60944b9b49ccf8335c71dc7669335e6173", -                "sha256:3a2234516f7db495083d8bba0ccdaabae587e62cfcd1b8154d5d0b09d3a48dfc", -                "sha256:3f288586592c697109b2b06e3988b7e17d9765887b5fc367010ee8500cbddc86", -                "sha256:40134cee03c8bbfbc644d4c0bc81796e12dd012a5257fb146c5a5417812ee5f7", -                "sha256:722f5d2c5f78d47b13b0112f6daff43ce4e08e8152319524d14f1f917cc5125e", -                "sha256:7b18fab0ed534a898552df91bc804bd62bb3a2646c11e054baca14d23663e1d6", -                "sha256:8a39d03bd99ea191f86b990ef67ecce878d6bf6518c5cde9173fb34fb36beb5e", -                "sha256:8ea263de8bf1a30b0d87150b4aa0e3203cf93bc1723ea3e7408a7d25e1299217", -                "sha256:943e2dc67ff45ab4c81d628c959837d01561d7e185080ab7a276b8ca67573fb5", -                "sha256:9d56a54c93e64b30c0d31f394d9890f175edec029cd846221728f99263cdee82", -                "sha256:b95b339c11d824f0bb789d31b91c8534916fcbdce248cccce216fa2630bb8a90", -                "sha256:bbfd9aba1e172cd2ab7b7142d49b28cf44d6451c4a66a870aff1dc3cb84849c7", -                "sha256:d8637bcc2f901aa61ec1d754abc862f9f145cb0346a0249360df4c159377018e", -                "sha256:e2446577eeea79d2179c9469d9d4ce3ab8a07d7985465c3cb91e7d74abc329b6", -                "sha256:e72fa163f37ae3b09f143cc6690a36f012d13e905d142e1beed4ec0e593ff657", -                "sha256:f32b7c63094749fbc0c1106c9a785666ec8afd49ecfe7002a30bb7c42e62b47c", -                "sha256:f50be4dd53f009cfb4b98c3c6b240e18ff9b17e3f1c320bd594bb83eddabfcb2" +                "sha256:080ae0f1b1b754be60b6ef31b9ab2915364c210eb1cb4d8e089357c89d7b9819", +                "sha256:0eccb76dff0155ddf793a589c6270e1bdbf6975b2824d18d1d23db2075d7fc96", +                "sha256:223a03d69e864a18d7bb2e0108bca5ba069ef91e5b048b953ed90ea9f50eb77f", +                "sha256:289e49f98adfd7a2ae3656df26e1d62cf49a06bbc03ced63f243c22cd8919adf", +                "sha256:292ac442a1d4ff27d41be748ec19f0c4ff47efebfb715064ba336564ea0f2071", +                "sha256:34771095123da0e54597fe3c5585a28d3799945257e51b378a20778bf33573b6", +                "sha256:34c8865f2d047be4c301ce90a916c7748be597e271c5c7932e8b9a6de85840f4", +                "sha256:36af260b215f86ebfe4a5e4aea82fd6036168a5710cbf8aad77019ab52156dda", +                "sha256:5e8e2a461717da40482b5fecf1119116234922d29660b3c3e01cbc5ba2cbf4bd", +                "sha256:61e77bd75542c56dff49434fedbafb25604997bc57dc0ebf791a5732503cb1bb", +                "sha256:691740c332f38a9035b4c6d1f0e6c8af239466ef2373a894d4393f0ea65c815d", +                "sha256:6bc0e0fdcb4cdc4ca06aa0b07e6e3560d62b2af79ef0ea4589835fcd2059012b", +                "sha256:96db5c93e2fe2e39f519efb7bb9d86aef56f5813fa0b032e47aba329fa925d57", +                "sha256:af701b22c91b3e36f65ee9f4b1bc2fe4800c8ed486eb6ef203624acbe53d026d", +                "sha256:b25bd21bba9c43d44320b719118c2ce35e4a78031f61d906caeb01316d49dafb", +                "sha256:c42f68319f8ea2322ed81c31a86c4e60547e6e90f3ebef479a7a7540bddbf268", +                "sha256:cc9a8d35af12bc5f484f3496f9cb3ab5bedfa4dcf3dfff953099453d88b659a7", +                "sha256:dfee9d198ba6d6f29aa5bf510bfb2c28a60c3f308116f114c9fd311980d3e870", +                "sha256:e1dd02e110a7a97582097ebba6713d9da28583b538c08e8a14bc82169c5d3e10", +                "sha256:e48c586c80a139c6c7fb0298b944d1c40752cf839bc8584cc793e42a8971ba6c", +                "sha256:f509762dec1a70eac32b86c098f37ac9c5d3d4a8a9098983328377c9e71543b2", +                "sha256:f8e0d61733843844f9019c911d5676818d99c4cd2c54b91de58384c7d962862b", +                "sha256:fe20280fed496deba60e0f6437b7672bdc83bf45e243bb546af47c60c85bcfbc"              ], -            "version": "==2.3.0" +            "version": "==2.4.0"          },          "pycparser": {              "hashes": [ -                "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" +                "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3", +                "sha256:db32bd592ba104f8fbb8047c18cd897f0f20d0909ba0ec5dc72a3221f6a82e15"              ],              "version": "==2.19"          },          "pygments": {              "hashes": [ -                "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", -                "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" +                "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", +                "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"              ], -            "version": "==2.2.0" +            "version": "==2.3.1"          },          "pynacl": {              "hashes": [ @@ -505,11 +502,11 @@          },          "requests": {              "hashes": [ -                "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", -                "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" +                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", +                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"              ],              "index": "pypi", -            "version": "==2.20.1" +            "version": "==2.21.0"          },          "shortuuid": {              "hashes": [ @@ -519,10 +516,10 @@          },          "six": {              "hashes": [ -                "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", -                "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" +                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", +                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"              ], -            "version": "==1.11.0" +            "version": "==1.12.0"          },          "snowballstemmer": {              "hashes": [ @@ -531,13 +528,20 @@              ],              "version": "==1.2.1"          }, +        "soupsieve": { +            "hashes": [ +                "sha256:057e08f362a255b457a5781675211556799ed3bb8807506eaac3809390bc304b", +                "sha256:f7d99b41637be2f249dfcc06ae93c13fcbbdfa7bb68b15308cdd0734e58146f1" +            ], +            "version": "==1.6.1" +        },          "sphinx": {              "hashes": [ -                "sha256:120732cbddb1b2364471c3d9f8bfd4b0c5b550862f99a65736c77f970b142aea", -                "sha256:b348790776490894e0424101af9c8413f2a86831524bd55c5f379d3e3e12ca64" +                "sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", +                "sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8"              ],              "index": "pypi", -            "version": "==1.8.2" +            "version": "==1.8.3"          },          "sphinxcontrib-websupport": {              "hashes": [ @@ -581,20 +585,29 @@          },          "yarl": {              "hashes": [ -                "sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9", -                "sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee", -                "sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308", -                "sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357", -                "sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78", -                "sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8", -                "sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1", -                "sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4", -                "sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7" -            ], -            "version": "==1.2.6" +                "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", +                "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", +                "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", +                "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", +                "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", +                "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", +                "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", +                "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", +                "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", +                "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", +                "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" +            ], +            "version": "==1.3.0"          }      },      "develop": { +        "aspy.yaml": { +            "hashes": [ +                "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", +                "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" +            ], +            "version": "==1.1.1" +        },          "attrs": {              "hashes": [                  "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", @@ -602,12 +615,26 @@              ],              "version": "==18.2.0"          }, +        "cached-property": { +            "hashes": [ +                "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", +                "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" +            ], +            "version": "==1.5.1" +        },          "certifi": {              "hashes": [ -                "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", -                "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" +                "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", +                "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"              ], -            "version": "==2018.10.15" +            "version": "==2018.11.29" +        }, +        "cfgv": { +            "hashes": [ +                "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", +                "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" +            ], +            "version": "==1.1.0"          },          "chardet": {              "hashes": [ @@ -684,12 +711,26 @@              "index": "pypi",              "version": "==0.7"          }, +        "identify": { +            "hashes": [ +                "sha256:08826e68e39e7de53cc2ddd8f6228a4e463b4bacb20565e5301c3ec690e68d27", +                "sha256:2364e24a7699fea0dc910e90740adbab43eef3746eeea4e016029c34123ce66d" +            ], +            "version": "==1.1.8" +        },          "idna": {              "hashes": [ -                "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", -                "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" +                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", +                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" +            ], +            "version": "==2.8" +        }, +        "importlib-metadata": { +            "hashes": [ +                "sha256:a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095", +                "sha256:b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"              ], -            "version": "==2.7" +            "version": "==0.8"          },          "mccabe": {              "hashes": [ @@ -698,6 +739,12 @@              ],              "version": "==0.6.1"          }, +        "nodeenv": { +            "hashes": [ +                "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" +            ], +            "version": "==1.3.3" +        },          "packaging": {              "hashes": [                  "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", @@ -705,6 +752,14 @@              ],              "version": "==18.0"          }, +        "pre-commit": { +            "hashes": [ +                "sha256:33bb9bf599c334d458fa9e311bde54e0c306a651473b6a36fdb36a61c8605c89", +                "sha256:e233f5cf3230ae9ed9ada132e9cf6890e18cc937adc669353fb64394f6e80c17" +            ], +            "index": "pypi", +            "version": "==1.13.0" +        },          "pycodestyle": {              "hashes": [                  "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", @@ -745,11 +800,11 @@          },          "requests": {              "hashes": [ -                "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", -                "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" +                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", +                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"              ],              "index": "pypi", -            "version": "==2.20.1" +            "version": "==2.21.0"          },          "safety": {              "hashes": [ @@ -761,10 +816,17 @@          },          "six": {              "hashes": [ -                "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", -                "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" +                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", +                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"              ], -            "version": "==1.11.0" +            "version": "==1.12.0" +        }, +        "toml": { +            "hashes": [ +                "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", +                "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" +            ], +            "version": "==0.10.0"          },          "urllib3": {              "hashes": [ @@ -772,6 +834,19 @@                  "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"              ],              "version": "==1.24.1" +        }, +        "virtualenv": { +            "hashes": [ +                "sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c" +            ], +            "version": "==16.2.0" +        }, +        "zipp": { +            "hashes": [ +                "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", +                "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" +            ], +            "version": "==0.3.3"          }      }  } diff --git a/bot/__main__.py b/bot/__main__.py index 3c40a3243..581fa5c8e 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -75,6 +75,7 @@ bot.load_extension("bot.cogs.tags")  bot.load_extension("bot.cogs.token_remover")  bot.load_extension("bot.cogs.utils")  bot.load_extension("bot.cogs.wolfram") +bot.load_extension("bot.cogs.free")  if has_rmq:      bot.load_extension("bot.cogs.rmq") diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 2ce4a51e3..0b848c773 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -71,7 +71,7 @@ class Alias:      @command(name="watch", hidden=True)      async def bigbrother_watch_alias( -            self, ctx, user: User, *, reason: str = None +            self, ctx, user: User, *, reason: str      ):          """          Alias for invoking <prefix>bigbrother watch user [text_channel]. diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index d5b72718c..800700a50 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -1,21 +1,18 @@ -import asyncio  import logging -import textwrap  from datetime import datetime, timedelta  from typing import List -from dateutil.relativedelta import relativedelta  from discord import Colour, Member, Message, Object, TextChannel  from discord.ext.commands import Bot  from bot import rules +from bot.cogs.moderation import Moderation  from bot.cogs.modlog import ModLog  from bot.constants import (      AntiSpam as AntiSpamConfig, Channels,      Colours, DEBUG_MODE, Event,      Guild as GuildConfig, Icons, Roles,  ) -from bot.utils.time import humanize_delta  log = logging.getLogger(__name__) @@ -44,7 +41,7 @@ WHITELISTED_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)  class AntiSpam:      def __init__(self, bot: Bot):          self.bot = bot -        self.muted_role = None +        self._muted_role = Object(Roles.muted)      @property      def mod_log(self) -> ModLog: @@ -110,8 +107,6 @@ class AntiSpam:          # Sanity check to ensure we're not lagging behind          if self.muted_role not in member.roles:              remove_role_after = AntiSpamConfig.punishment['remove_after'] -            duration_delta = relativedelta(seconds=remove_role_after) -            human_duration = humanize_delta(duration_delta)              mod_alert_message = (                  f"**Triggered by:** {member.display_name}#{member.discriminator} (`{member.id}`)\n" @@ -133,7 +128,8 @@ class AntiSpam:                  mod_alert_message += f"{content}" -            await self.mod_log.send_log_message( +            # Return the mod log message Context that we can use to post the infraction +            mod_log_ctx = await self.mod_log.send_log_message(                  icon_url=Icons.filtering,                  colour=Colour(Colours.soft_red),                  title=f"Spam detected!", @@ -143,27 +139,8 @@ class AntiSpam:                  ping_everyone=AntiSpamConfig.ping_everyone              ) -            await member.add_roles(self.muted_role, reason=reason) -            description = textwrap.dedent(f""" -                **Channel**: {msg.channel.mention} -                **User**: {msg.author.mention} (`{msg.author.id}`) -                **Reason**: {reason} -                Role will be removed after {human_duration}. -            """) - -            await self.mod_log.send_log_message( -                icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), -                title="User muted", text=description -            ) - -            await asyncio.sleep(remove_role_after) -            await member.remove_roles(self.muted_role, reason="AntiSpam mute expired") - -            await self.mod_log.send_log_message( -                icon_url=Icons.user_mute, colour=Colour(Colours.soft_green), -                title="User unmuted", -                text=f"Was muted by `AntiSpam` cog for {human_duration}." -            ) +            # Run a tempmute +            await mod_log_ctx.invoke(Moderation.tempmute, member, f"{remove_role_after}S", reason=reason)      async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]):          # Is deletion of offending messages actually enabled? diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index 29b13f038..70916cd7b 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -2,8 +2,10 @@ import asyncio  import logging  import re  from collections import defaultdict, deque +from time import strptime, struct_time  from typing import List, Union +from aiohttp import ClientError  from discord import Color, Embed, Guild, Member, Message, TextChannel, User  from discord.ext.commands import Bot, Context, group @@ -26,9 +28,11 @@ class BigBrother:      def __init__(self, bot: Bot):          self.bot = bot          self.watched_users = {}  # { user_id: log_channel_id } +        self.watch_reasons = {}  # { user_id: watch_reason }          self.channel_queues = defaultdict(lambda: defaultdict(deque))  # { user_id: { channel_id: queue(messages) }          self.last_log = [None, None, 0]  # [user_id, channel_id, message_count]          self.consuming = False +        self.infraction_watch_prefix = "bb watch: "  # Please do not change or we won't be able to find old reasons          self.bot.loop.create_task(self.get_watched_users()) @@ -62,6 +66,42 @@ class BigBrother:              data = await response.json()              self.update_cache(data) +    async def get_watch_reason(self, user_id: int) -> str: +        """ Fetches and returns the latest watch reason for a user using the infraction API """ + +        re_bb_watch = rf"^{self.infraction_watch_prefix}" +        user_id = str(user_id) + +        try: +            response = await self.bot.http_session.get( +                URLs.site_infractions_user_type.format( +                    user_id=user_id, +                    infraction_type="note", +                ), +                params={"search": re_bb_watch, "hidden": "True", "active": "False"}, +                headers=self.HEADERS +            ) +            infraction_list = await response.json() +        except ClientError: +            log.exception(f"Failed to retrieve bb watch reason for {user_id}.") +            return "(error retrieving bb reason)" + +        if infraction_list: +            latest_reason_infraction = max(infraction_list, key=self._parse_infraction_time) +            latest_reason = latest_reason_infraction['reason'][len(self.infraction_watch_prefix):] +            log.trace(f"The latest bb watch reason for {user_id}: {latest_reason}") +            return latest_reason + +        log.trace(f"No bb watch reason found for {user_id}; returning default string") +        return "(no reason specified)" + +    @staticmethod +    def _parse_infraction_time(infraction: str) -> struct_time: +        """Takes RFC1123 date_time string and returns time object for sorting purposes""" + +        date_string = infraction["inserted_at"] +        return strptime(date_string, "%a, %d %b %Y %H:%M:%S %Z") +      async def on_member_ban(self, guild: Guild, user: Union[User, Member]):          if guild.id == GuildConfig.id and user.id in self.watched_users:              url = f"{URLs.site_bigbrother_api}?user_id={user.id}" @@ -70,6 +110,7 @@ class BigBrother:              async with self.bot.http_session.delete(url, headers=self.HEADERS) as response:                  del self.watched_users[user.id]                  del self.channel_queues[user.id] +                del self.watch_reasons[user.id]                  if response.status == 204:                      await channel.send(                          f"{Emojis.bb_message}:hammer: {user} got banned, so " @@ -139,10 +180,17 @@ class BigBrother:          # Send header if user/channel are different or if message limit exceeded.          if message.author.id != last_user or message.channel.id != last_channel or msg_count > limit: +            # Retrieve watch reason from API if it's not already in the cache +            if message.author.id not in self.watch_reasons: +                log.trace(f"No watch reason for {message.author.id} found in cache; retrieving from API") +                user_watch_reason = await self.get_watch_reason(message.author.id) +                self.watch_reasons[message.author.id] = user_watch_reason +              self.last_log = [message.author.id, message.channel.id, 0]              embed = Embed(description=f"{message.author.mention} in [#{message.channel.name}]({message.jump_url})")              embed.set_author(name=message.author.nick or message.author.name, icon_url=message.author.avatar_url) +            embed.set_footer(text=f"Watch reason: {self.watch_reasons[message.author.id]}")              await destination.send(embed=embed)      @staticmethod @@ -246,15 +294,15 @@ class BigBrother:                      )                  else:                      self.watched_users[user.id] = channel +                    self.watch_reasons[user.id] = reason +                    # Add a note (shadow warning) with the reason for watching +                    reason = f"{self.infraction_watch_prefix}{reason}" +                    await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)              else:                  data = await response.json() -                reason = data.get('error_message', "no message provided") -                await ctx.send(f":x: the API returned an error: {reason}") - -        # Add a note (shadow warning) with the reason for watching -        reason = "bb watch: " + reason  # Prepend for situational awareness -        await post_infraction(ctx, user, type="warning", reason=reason, hidden=True) +                error_reason = data.get('error_message', "no message provided") +                await ctx.send(f":x: the API returned an error: {error_reason}")      @bigbrother_group.command(name='unwatch', aliases=('uw',))      @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -270,6 +318,8 @@ class BigBrother:                      del self.watched_users[user.id]                      if user.id in self.channel_queues:                          del self.channel_queues[user.id] +                    if user.id in self.watch_reasons: +                        del self.watch_reasons[user.id]                  else:                      log.warning(f"user {user.id} was unwatched but was not found in the cache") diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index b684ad886..a6d9aa278 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -372,7 +372,7 @@ class Bot:              return          # Retrieve channel and message objects for use later -        channel = self.bot.get_channel(payload.data.get("channel_id")) +        channel = self.bot.get_channel(int(payload.data.get("channel_id")))          user_message = await channel.get_message(payload.message_id)          #  Checks to see if the user has corrected their codeblock.  If it's fixed, has_fixed_codeblock will be None @@ -383,6 +383,7 @@ class Bot:              bot_message = await channel.get_message(self.codeblock_message_ids[payload.message_id])              await bot_message.delete()              del self.codeblock_message_ids[payload.message_id] +            log.trace("User's incorrect code block has been fixed.  Removing bot formatting message.")  def setup(bot): diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index c432d377c..f07d9df9f 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -5,14 +5,11 @@ from discord import Colour, Embed, Member  from discord.ext.commands import Bot, Context, group  from bot.cogs.modlog import ModLog -from bot.constants import Channels, Emojis, Icons, Keys, Roles, URLs +from bot.constants import Channels, Colours, Emojis, Event, Icons, Keys, Roles, URLs  from bot.decorators import with_role  log = logging.getLogger(__name__) -COLOUR_RED = Colour(0xcd6d6d) -COLOUR_GREEN = Colour(0x68c290) -  REJECTION_MESSAGE = """  Hi, {user} - Thanks for your interest in our server! @@ -24,6 +21,8 @@ will be resolved soon. In the meantime, please feel free to peruse the resources  <https://pythondiscord.com/>, and have a nice day!  """ +BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism" +  class Defcon:      """Time-sensitive server defense mechanisms""" @@ -66,6 +65,8 @@ class Defcon:                  self.days = timedelta(days=0)                  log.warning(f"DEFCON disabled") +            await self.update_channel_topic() +      async def on_member_join(self, member: Member):          if self.enabled and self.days.days > 0:              now = datetime.utcnow() @@ -93,7 +94,7 @@ class Defcon:                      message = f"{message}\n\nUnable to send rejection message via DM; they probably have DMs disabled."                  await self.mod_log.send_log_message( -                    Icons.defcon_denied, COLOUR_RED, "Entry denied", +                    Icons.defcon_denied, Colours.soft_red, "Entry denied",                      message, member.avatar_url_as(static_format="png")                  ) @@ -134,7 +135,7 @@ class Defcon:              )              await self.mod_log.send_log_message( -                Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", +                Icons.defcon_enabled, Colours.soft_green, "DEFCON enabled",                  f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n"                  f"**Days:** {self.days.days}\n\n"                  "**There was a problem updating the site** - This setting may be reverted when the bot is " @@ -145,11 +146,13 @@ class Defcon:              await ctx.send(f"{Emojis.defcon_enabled} DEFCON enabled.")              await self.mod_log.send_log_message( -                Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", +                Icons.defcon_enabled, Colours.soft_green, "DEFCON enabled",                  f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n"                  f"**Days:** {self.days.days}\n\n"              ) +        await self.update_channel_topic() +      @defcon_group.command(name='disable', aliases=('off', 'd'))      @with_role(Roles.admin, Roles.owner)      async def disable_command(self, ctx: Context): @@ -177,7 +180,7 @@ class Defcon:              )              await self.mod_log.send_log_message( -                Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", +                Icons.defcon_disabled, Colours.soft_red, "DEFCON disabled",                  f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n"                  "**There was a problem updating the site** - This setting may be reverted when the bot is "                  "restarted.\n\n" @@ -187,10 +190,12 @@ class Defcon:              await ctx.send(f"{Emojis.defcon_disabled} DEFCON disabled.")              await self.mod_log.send_log_message( -                Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", +                Icons.defcon_disabled, Colours.soft_red, "DEFCON disabled",                  f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)"              ) +        await self.update_channel_topic() +      @defcon_group.command(name='status', aliases=('s',))      @with_role(Roles.admin, Roles.owner)      async def status_command(self, ctx: Context): @@ -252,6 +257,23 @@ class Defcon:                  f"**Days:** {self.days.days}"              ) +        await self.update_channel_topic() + +    async def update_channel_topic(self): +        """ +        Update the #defcon channel topic with the current DEFCON status +        """ + +        if self.enabled: +            day_str = "days" if self.days.days > 1 else "day" +            new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Enabled, Threshold: {self.days.days} {day_str})" +        else: +            new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Disabled)" + +        self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) +        defcon_channel = self.bot.guilds[0].get_channel(Channels.defcon) +        await defcon_channel.edit(topic=new_topic) +  def setup(bot: Bot):      bot.add_cog(Defcon(bot)) diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py index 9e09b3aa0..8e97a35a2 100644 --- a/bot/cogs/eval.py +++ b/bot/cogs/eval.py @@ -68,7 +68,7 @@ class CodeEval:                  # then we get the length                  # and use `str.rjust()`                  # to indent it. -                start = "...:".rjust(len(self.ln) + 2) +                start = "...: ".rjust(len(str(self.ln)) + 7)              if i == len(lines) - 2:                  if line.startswith("return"): diff --git a/bot/cogs/events.py b/bot/cogs/events.py index edfc6e579..f0baecd4b 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -25,6 +25,7 @@ class Events:      def __init__(self, bot: Bot):          self.bot = bot +        self.headers = {"X-API-KEY": Keys.site_api}      @property      def mod_log(self) -> ModLog: @@ -103,6 +104,29 @@ class Events:          resp = await response.json()          return resp["data"] +    async def has_active_mute(self, user_id: str) -> bool: +        """ +        Check whether a user has any active mute infractions +        """ + +        response = await self.bot.http_session.get( +            URLs.site_infractions_user.format( +                user_id=user_id +            ), +            params={"hidden": "True"}, +            headers=self.headers +        ) +        infraction_list = await response.json() + +        # Check for active mute infractions +        if not infraction_list: +            # Short circuit +            return False + +        return any( +            infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute" +        ) +      async def on_command_error(self, ctx: Context, e: CommandError):          command = ctx.command          parent = None @@ -236,6 +260,14 @@ class Events:                  for role in RESTORE_ROLES:                      if role in old_roles: +                        # Check for mute roles that were not able to be removed and skip if present +                        if role == str(Roles.muted) and not await self.has_active_mute(str(member.id)): +                            log.debug( +                                f"User {member.id} has no active mute infraction, " +                                "their leftover muted role will not be persisted" +                            ) +                            continue +                          new_roles.append(Object(int(role)))                  for role in new_roles: diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index b6ce501fc..570d6549f 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -1,7 +1,8 @@  import logging  import re -from discord import Colour, Member, Message +import discord.errors +from discord import Colour, DMChannel, Member, Message, TextChannel  from discord.ext.commands import Bot  from bot.cogs.modlog import ModLog @@ -38,31 +39,64 @@ class Filtering:      def __init__(self, bot: Bot):          self.bot = bot +        _staff_mistake_str = "If you believe this was a mistake, please let staff know!"          self.filters = {              "filter_zalgo": {                  "enabled": Filter.filter_zalgo,                  "function": self._has_zalgo, -                "type": "filter" +                "type": "filter", +                "content_only": True, +                "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}" +                )              },              "filter_invites": {                  "enabled": Filter.filter_invites,                  "function": self._has_invites, -                "type": "filter" +                "type": "filter", +                "content_only": True, +                "user_notification": Filter.notify_user_invites, +                "notification_msg": ( +                    f"Per Rule 10, your invite link has been removed. {_staff_mistake_str}\n\n" +                    r"Our server rules can be found here: <https://pythondiscord.com/about/rules>" +                )              },              "filter_domains": {                  "enabled": Filter.filter_domains,                  "function": self._has_urls, -                "type": "filter" +                "type": "filter", +                "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}" +                ) +            }, +            "filter_rich_embeds": { +                "enabled": Filter.filter_rich_embeds, +                "function": self._has_rich_embed, +                "type": "filter", +                "content_only": False, +                "user_notification": Filter.notify_user_rich_embeds, +                "notification_msg": ( +                    "Your post has been removed because it contained a rich embed. " +                    "This indicates that you're either using an unofficial discord client or are using a self-bot, " +                    f"both of which violate Discord's Terms of Service. {_staff_mistake_str}\n\n" +                    "Please don't use a self-bot or an unofficial Discord client on our server." +                )              },              "watch_words": {                  "enabled": Filter.watch_words,                  "function": self._has_watchlist_words, -                "type": "watchlist" +                "type": "watchlist", +                "content_only": True,              },              "watch_tokens": {                  "enabled": Filter.watch_tokens,                  "function": self._has_watchlist_tokens, -                "type": "watchlist" +                "type": "watchlist", +                "content_only": True,              },          } @@ -105,22 +139,52 @@ class Filtering:          # If none of the above, we can start filtering.          if filter_message:              for filter_name, _filter in self.filters.items(): -                  # Is this specific filter enabled in the config?                  if _filter["enabled"]: -                    triggered = await _filter["function"](msg.content) +                    # Does the filter only need the message content or the full message? +                    if _filter["content_only"]: +                        triggered = await _filter["function"](msg.content) +                    else: +                        triggered = await _filter["function"](msg)                      if triggered: +                        # If this is a filter (not a watchlist), we should delete the message. +                        if _filter["type"] == "filter": +                            try: +                                # Embeds (can?) trigger both the `on_message` and `on_message_edit` +                                # event handlers, triggering filtering twice for the same message. +                                # +                                # If `on_message`-triggered filtering already deleted the message +                                # then `on_message_edit`-triggered filtering will raise exception +                                # since the message no longer exists. +                                # +                                # In addition, to avoid sending two notifications to the user, the +                                # logs, and mod_alert, we return if the message no longer exists. +                                await msg.delete() +                            except discord.errors.NotFound: +                                return + +                            # Notify the user if the filter specifies +                            if _filter["user_notification"]: +                                await self.notify_member(msg.author, _filter["notification_msg"], msg.channel) + +                        if isinstance(msg.channel, DMChannel): +                            channel_str = "via DM" +                        else: +                            channel_str = f"in {msg.channel.mention}" +                          message = (                              f"The {filter_name} {_filter['type']} was triggered "                              f"by **{msg.author.name}#{msg.author.discriminator}** " -                            f"(`{msg.author.id}`) in <#{msg.channel.id}> with [the " +                            f"(`{msg.author.id}`) {channel_str} with [the "                              f"following message]({msg.jump_url}):\n\n"                              f"{msg.content}"                          )                          log.debug(message) +                        additional_embeds = msg.embeds if filter_name == "filter_rich_embeds" else None +                          # Send pretty mod log embed to mod-alerts                          await self.mod_log.send_log_message(                              icon_url=Icons.filtering, @@ -130,12 +194,9 @@ class Filtering:                              thumbnail=msg.author.avatar_url_as(static_format="png"),                              channel_id=Channels.mod_alerts,                              ping_everyone=Filter.ping_everyone, +                            additional_embeds=additional_embeds,                          ) -                        # If this is a filter (not a watchlist), we should delete the message. -                        if _filter["type"] == "filter": -                            await msg.delete() -                          break  # We don't want multiple filters to trigger      @staticmethod @@ -217,16 +278,12 @@ class Filtering:      async def _has_invites(self, text: str) -> bool:          """ -        Returns True if the text contains an invite which -        is not on the guild_invite_whitelist in config.yml. +        Returns True if the text contains an invite which is not on the guild_invite_whitelist in +        config.yml -        Also catches a lot of common ways to try to cheat the system. +        Attempts to catch some of common ways to try to cheat the system.          """ -        # Remove spaces to prevent cases like -        # d i s c o r d . c o m / i n v i t e / s e x y t e e n s -        text = text.replace(" ", "") -          # Remove backslashes to prevent escape character aroundfuckery like          # discord\.gg/gdudes-pony-farm          text = text.replace("\\", "") @@ -238,20 +295,41 @@ class Filtering:                  f"{URLs.discord_invite_api}/{invite}"              )              response = await response.json() -            if response.get("guild") is None: -                # If we have a valid invite which is not a guild invite -                # it might be a DM channel invite -                if response.get("channel") is not None: -                    # We don't have whitelisted Group DMs so we can -                    # go ahead and return a positive for any group DM -                    return True +            guild = response.get("guild") +            if guild is None: +                # Lack of a "guild" key in the JSON response indicates either an group DM invite, an +                # expired invite, or an invalid invite. The API does not currently differentiate +                # between invalid and expired invites +                return True -            guild_id = int(response.get("guild").get("id")) +            guild_id = int(guild.get("id"))              if guild_id not in Filter.guild_invite_whitelist:                  return True          return False +    @staticmethod +    async def _has_rich_embed(msg: Message): +        """ +        Returns True if any of the embeds in the message +        are of type 'rich', returns False otherwise +        """ +        if msg.embeds: +            return any(embed.type == "rich" for embed in msg.embeds) +        return False + +    async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel): +        """ +        Notify filtered_member about a moderation action with the reason str + +        First attempts to DM the user, fall back to in-channel notification if user has DMs disabled +        """ + +        try: +            await filtered_member.send(reason) +        except discord.errors.Forbidden: +            await channel.send(f"{filtered_member.mention} {reason}") +  def setup(bot: Bot):      bot.add_cog(Filtering(bot)) diff --git a/bot/cogs/free.py b/bot/cogs/free.py new file mode 100644 index 000000000..620449f7e --- /dev/null +++ b/bot/cogs/free.py @@ -0,0 +1,127 @@ +import logging +from datetime import datetime + +from discord import Colour, Embed, Member, utils +from discord.ext import commands +from discord.ext.commands import BucketType, Context, command, cooldown + +from bot.constants import Categories, Free, Roles + + +log = logging.getLogger(__name__) + +TIMEOUT = Free.activity_timeout +RATE = Free.cooldown_rate +PER = Free.cooldown_per + + +class Free: +    """Tries to figure out which help channels are free.""" + +    PYTHON_HELP_ID = Categories.python_help + +    @command(name="free", aliases=('f',)) +    @cooldown(RATE, PER, BucketType.channel) +    async def free(self, ctx: Context, user: Member = None, seek: int = 2): +        """ +        Lists free help channels by likeliness of availability. +        :param user: accepts user mention, ID, etc. +        :param seek: How far back to check the last active message. + +        seek is used only when this command is invoked in a help channel. +        You cannot override seek without mentioning a user first. + +        When seek is 2, we are avoiding considering the last active message +        in a channel to be the one that invoked this command. + +        When seek is 3 or more, a user has been mentioned on the assumption +        that they asked if the channel is free or they asked their question +        in an active channel, and we want the message before that happened. +        """ +        free_channels = [] +        python_help = utils.get(ctx.guild.categories, id=self.PYTHON_HELP_ID) + +        if user is not None and seek == 2: +            seek = 3 +        elif not 0 < seek < 10: +            seek = 3 + +        # Iterate through all the help channels +        # to check latest activity +        for channel in python_help.channels: +            # Seek further back in the help channel +            # the command was invoked in +            if channel.id == ctx.channel.id: +                messages = await channel.history(limit=seek).flatten() +                msg = messages[seek-1] +            # Otherwise get last message +            else: +                msg = await channel.history(limit=1).next()   # noqa (False positive) + +            inactive = (datetime.utcnow() - msg.created_at).seconds +            if inactive > TIMEOUT: +                free_channels.append((inactive, channel)) + +        embed = Embed() +        embed.colour = Colour.blurple() +        embed.title = "**Looking for a free help channel?**" + +        if user is not None: +            embed.description = f"**Hey {user.mention}!**\n\n" +        else: +            embed.description = "" + +        # Display all potentially inactive channels +        # in descending order of inactivity +        if free_channels: +            embed.description += "**The following channel{0} look{1} free:**\n\n**".format( +                's' if len(free_channels) > 1 else '', +                '' if len(free_channels) > 1 else 's' +            ) + +            # Sort channels in descending order by seconds +            # Get position in list, inactivity, and channel object +            # For each channel, add to embed.description +            for i, (inactive, channel) in enumerate(sorted(free_channels, reverse=True), 1): +                minutes, seconds = divmod(inactive, 60) +                if minutes > 59: +                    hours, minutes = divmod(minutes, 60) +                    embed.description += f"{i}. {channel.mention} inactive for {hours}h{minutes}m{seconds}s\n\n" +                else: +                    embed.description += f"{i}. {channel.mention} inactive for {minutes}m{seconds}s\n\n" + +            embed.description += ("**\nThese channels aren't guaranteed to be free, " +                                  "so use your best judgement and check for yourself.") +        else: +            embed.description = ("**Doesn't look like any channels are available right now. " +                                 "You're welcome to check for yourself to be sure. " +                                 "If all channels are truly busy, please be patient " +                                 "as one will likely be available soon.**") + +        await ctx.send(embed=embed) + +    @free.error +    async def free_error(self, ctx: Context, error): +        """ +        If error raised is CommandOnCooldown, and the +        user who invoked has the helper role, reset +        the cooldown and reinvoke the command. + +        Otherwise log the error. +        """ +        helpers = ctx.guild.get_role(Roles.helpers) + +        if isinstance(error, commands.CommandOnCooldown): +            if helpers in ctx.author.roles: +                # reset cooldown so second invocation +                # doesn't bring us back here. +                ctx.command.reset_cooldown(ctx) +                # return to avoid needlessly logging the error +                return await ctx.reinvoke() + +        log.exception(error)  # Don't ignore other errors + + +def setup(bot): +    bot.add_cog(Free()) +    log.info("Cog loaded: Free") diff --git a/bot/cogs/help.py b/bot/cogs/help.py index d30ff0dfb..ded068123 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -6,6 +6,7 @@ from contextlib import suppress  from discord import Colour, Embed, HTTPException  from discord.ext import commands +from discord.ext.commands import CheckFailure  from fuzzywuzzy import fuzz, process  from bot import constants @@ -14,6 +15,7 @@ from bot.pagination import (      LEFT_EMOJI, LinePaginator, RIGHT_EMOJI,  ) +  REACTIONS = {      FIRST_EMOJI: 'first',      LEFT_EMOJI: 'back', @@ -427,7 +429,15 @@ class HelpSession:                      # see if the user can run the command                      strikeout = '' -                    can_run = await command.can_run(self._ctx) + +                    # Patch to make the !help command work outside of #bot-commands again +                    # This probably needs a proper rewrite, but this will make it work in +                    # the mean time. +                    try: +                        can_run = await command.can_run(self._ctx) +                    except CheckFailure: +                        can_run = False +                      if not can_run:                          # skip if we don't show commands they can't run                          if self._only_can_run: diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 6e958b912..ac08d3dd4 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -82,7 +82,7 @@ class Moderation(Scheduler):          :param reason: The reason for the warning.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Warning",              reason=reason @@ -92,12 +92,29 @@ class Moderation(Scheduler):          if response_object is None:              return +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: warned {user.mention}" +          if reason is None: -            result_message = f":ok_hand: warned {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: warned {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "warning") + +        # Send a message to the mod log +        await self.mod_log.send_log_message( +            icon_url=Icons.user_warn, +            colour=Colour(Colours.soft_red), +            title="Member warned", +            thumbnail=user.avatar_url_as(static_format="png"), +            text=textwrap.dedent(f""" +                Member: {user.mention} (`{user.id}`) +                Actor: {ctx.message.author} +                Reason: {reason} +            """) +        )      @with_role(*MODERATION_ROLES)      @command(name="kick") @@ -108,7 +125,7 @@ class Moderation(Scheduler):          :param reason: The reason for the kick.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Kick",              reason=reason @@ -121,12 +138,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_remove, user.id)          await user.kick(reason=reason) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: kicked {user.mention}" +          if reason is None: -            result_message = f":ok_hand: kicked {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: kicked {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "kick")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -150,7 +171,7 @@ class Moderation(Scheduler):          :param reason: The reason for the ban.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Ban",              duration="Permanent", @@ -165,12 +186,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_remove, user.id)          await ctx.guild.ban(user, reason=reason, delete_message_days=0) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: permanently banned {user.mention}" +          if reason is None: -            result_message = f":ok_hand: permanently banned {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: permanently banned {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "ban")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -194,7 +219,7 @@ class Moderation(Scheduler):          :param reason: The reason for the mute.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Mute",              duration="Permanent", @@ -209,12 +234,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_update, user.id)          await user.add_roles(self._muted_role, reason=reason) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: permanently muted {user.mention}" +          if reason is None: -            result_message = f":ok_hand: permanently muted {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: permanently muted {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "mute")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -242,7 +271,7 @@ class Moderation(Scheduler):          :param reason: The reason for the temporary mute.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Mute",              duration=duration, @@ -262,12 +291,16 @@ class Moderation(Scheduler):          loop = asyncio.get_event_loop()          self.schedule_task(loop, infraction_object["id"], infraction_object) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}" +          if reason is None: -            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "mute")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -294,7 +327,7 @@ class Moderation(Scheduler):          :param reason: The reason for the temporary ban.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Ban",              duration=duration, @@ -316,12 +349,16 @@ class Moderation(Scheduler):          loop = asyncio.get_event_loop()          self.schedule_task(loop, infraction_object["id"], infraction_object) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}" +          if reason is None: -            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "ban")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -361,6 +398,19 @@ class Moderation(Scheduler):          await ctx.send(result_message) +        # Send a message to the mod log +        await self.mod_log.send_log_message( +            icon_url=Icons.user_warn, +            colour=Colour(Colours.soft_red), +            title="Member shadow warned", +            thumbnail=user.avatar_url_as(static_format="png"), +            text=textwrap.dedent(f""" +                Member: {user.mention} (`{user.id}`) +                Actor: {ctx.message.author} +                Reason: {reason} +            """) +        ) +      @with_role(*MODERATION_ROLES)      @command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick'])      async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None): @@ -603,7 +653,18 @@ class Moderation(Scheduler):              if infraction_object["expires_at"] is not None:                  self.cancel_expiration(infraction_object["id"]) -            await ctx.send(f":ok_hand: Un-muted {user.mention}.") +            notified = await self.notify_pardon( +                user=user, +                title="You have been unmuted.", +                content="You may now send messages in the server.", +                icon_url=Icons.user_unmute +            ) + +            dm_result = ":incoming_envelope: " if notified else "" +            await ctx.send(f"{dm_result}:ok_hand: Un-muted {user.mention}.") + +            if not notified: +                await self.log_notify_failure(user, ctx.author, "unmute")              # Send a log message to the mod log              await self.mod_log.send_log_message( @@ -617,13 +678,6 @@ class Moderation(Scheduler):                      Intended expiry: {infraction_object['expires_at']}                  """)              ) - -            await self.notify_pardon( -                user=user, -                title="You have been unmuted.", -                content="You may now send messages in the server.", -                icon_url=Icons.user_unmute -            )          except Exception:              log.exception("There was an error removing an infraction.")              await ctx.send(":x: There was an error removing the infraction.") @@ -1093,7 +1147,7 @@ class Moderation(Scheduler):          embed.title = f"Please review our rules over at {RULES_URL}"          embed.url = RULES_URL -        await self.send_private_embed(user, embed) +        return await self.send_private_embed(user, embed)      async def notify_pardon(              self, user: Union[User, Member], title: str, content: str, icon_url: str = Icons.user_verified @@ -1114,7 +1168,7 @@ class Moderation(Scheduler):          embed.set_author(name=title, icon_url=icon_url) -        await self.send_private_embed(user, embed) +        return await self.send_private_embed(user, embed)      async def send_private_embed(self, user: Union[User, Member], embed: Embed):          """ @@ -1129,11 +1183,22 @@ class Moderation(Scheduler):          try:              await user.send(embed=embed) +            return True          except (HTTPException, Forbidden):              log.debug(                  f"Infraction-related information could not be sent to user {user} ({user.id}). "                  "They've probably just disabled private messages."              ) +            return False + +    async def log_notify_failure(self, target: str, actor: Member, infraction_type: str): +        await self.mod_log.send_log_message( +            icon_url=Icons.token_removed, +            content=actor.mention, +            colour=Colour(Colours.soft_red), +            title="Notification Failed", +            text=f"Direct message was unable to be sent.\nUser: {target.mention}\nType: {infraction_type}" +        )      # endregion diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 1d1546d5b..55611c5e4 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -104,8 +104,19 @@ class ModLog:                  self._ignored[event].append(item)      async def send_log_message( -            self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, thumbnail: str = None, -            channel_id: int = Channels.modlog, ping_everyone: bool = False, files: List[File] = None +            self, +            icon_url: Optional[str], +            colour: Colour, +            title: Optional[str], +            text: str, +            thumbnail: Optional[str] = None, +            channel_id: int = Channels.modlog, +            ping_everyone: bool = False, +            files: Optional[List[File]] = None, +            content: Optional[str] = None, +            additional_embeds: Optional[List[Embed]] = None, +            timestamp_override: Optional[datetime.datetime] = None, +            footer_override: Optional[str] = None,      ):          embed = Embed(description=text) @@ -113,17 +124,30 @@ class ModLog:              embed.set_author(name=title, icon_url=icon_url)          embed.colour = colour -        embed.timestamp = datetime.datetime.utcnow() -        if thumbnail is not None: -            embed.set_thumbnail(url=thumbnail) +        embed.timestamp = timestamp_override or datetime.datetime.utcnow() + +        if footer_override: +            embed.set_footer(text=footer_override) -        content = None +        if thumbnail: +            embed.set_thumbnail(url=thumbnail)          if ping_everyone: -            content = "@everyone" +            if content: +                content = f"@everyone\n{content}" +            else: +                content = "@everyone" -        await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files) +        channel = self.bot.get_channel(channel_id) +        log_message = await channel.send(content=content, embed=embed, files=files) + +        if additional_embeds: +            await channel.send("With the following embed(s):") +            for additional_embed in additional_embeds: +                await channel.send(embed=additional_embed) + +        return await self.bot.get_context(log_message)  # Optionally return for use with antispam      async def on_guild_channel_create(self, channel: GUILD_CHANNEL):          if channel.guild.id != GuildConstant.id: @@ -174,6 +198,10 @@ class ModLog:          if before.guild.id != GuildConstant.id:              return +        if before.id in self._ignored[Event.guild_channel_update]: +            self._ignored[Event.guild_channel_update].remove(before.id) +            return +          diff = DeepDiff(before, after)          changes = []          done = [] @@ -662,14 +690,27 @@ class ModLog:                  f"{after.clean_content}"              ) +        if before.edited_at: +            # Message was previously edited, to assist with self-bot detection, use the edited_at +            # datetime as the baseline and create a human-readable delta between this edit event +            # and the last time the message was edited +            timestamp = before.edited_at +            delta = humanize_delta(relativedelta(after.edited_at, before.edited_at)) +            footer = f"Last edited {delta} ago" +        else: +            # Message was not previously edited, use the created_at datetime as the baseline, no +            # delta calculation needed +            timestamp = before.created_at +            footer = None +          await self.send_log_message( -            Icons.message_edit, Colour.blurple(), "Message edited (Before)", -            before_response, channel_id=Channels.message_log +            Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, +            channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer          )          await self.send_log_message( -            Icons.message_edit, Colour.blurple(), "Message edited (After)", -            after_response, channel_id=Channels.message_log +            Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response, +            channel_id=Channels.message_log, timestamp_override=after.edited_at          )      async def on_raw_message_edit(self, event: RawMessageUpdateEvent): diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index f6ed111dc..ddf5cc1f3 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -398,7 +398,7 @@ class Reminders(Scheduler):          )          if not failed: -            self.cancel_reminder(response_data["reminder_id"]) +            await self._delete_reminder(response_data["reminder_id"])  def setup(bot: Bot): diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 1b51da843..cb0454249 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -6,12 +6,12 @@ import textwrap  from discord import Colour, Embed  from discord.ext.commands import ( -    Bot, CommandError, Context, MissingPermissions, -    NoPrivateMessage, check, command, guild_only +    Bot, CommandError, Context, NoPrivateMessage, command, guild_only  )  from bot.cogs.rmq import RMQ  from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, Roles, URLs +from bot.decorators import InChannelCheckFailure, in_channel  from bot.utils.messages import wait_for_deletion @@ -51,22 +51,8 @@ RAW_CODE_REGEX = re.compile(      r"\s*$",                                # any trailing whitespace until the end of the string      re.DOTALL                               # "." also matches newlines  ) -BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) -WHITELISTED_CHANNELS = (Channels.bot,) -WHITELISTED_CHANNELS_STRING = ', '.join(f"<#{channel_id}>" for channel_id in WHITELISTED_CHANNELS) - - -async def channel_is_whitelisted_or_author_can_bypass(ctx: Context): -    """ -    Checks that the author is either helper or above -    or the channel is a whitelisted channel. -    """ -    if ctx.channel.id in WHITELISTED_CHANNELS: -        return True -    if any(r.id in BYPASS_ROLES for r in ctx.author.roles): -        return True -    raise MissingPermissions("You are not allowed to do that here.") +BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)  class Snekbox: @@ -84,7 +70,7 @@ class Snekbox:      @command(name='eval', aliases=('e',))      @guild_only() -    @check(channel_is_whitelisted_or_author_can_bypass) +    @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES)      async def eval_command(self, ctx: Context, *, code: str = None):          """          Run some code. get the result back. We've done our best to make this safe, but do let us know if you @@ -205,9 +191,9 @@ class Snekbox:              embed.description = "You're not allowed to use this command in private messages."              await ctx.send(embed=embed) -        elif isinstance(error, MissingPermissions): +        elif isinstance(error, InChannelCheckFailure):              embed.title = random.choice(NEGATIVE_REPLIES) -            embed.description = f"Sorry, but you may only use this command within {WHITELISTED_CHANNELS_STRING}." +            embed.description = str(error)              await ctx.send(embed=embed)          else: diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index 8277513a7..c1a0e18ba 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -16,8 +16,9 @@ log = logging.getLogger(__name__)  DELETION_MESSAGE_TEMPLATE = (      "Hey {mention}! I noticed you posted a seemingly valid Discord API " -    "token in your message and have removed your message to prevent abuse. " -    "We recommend regenerating your token regardless, which you can do here: " +    "token in your message and have removed your message. " +    "We **strongly recommend** regenerating your token as it's probably " +    "been compromised. You can do that here: "      "<https://discordapp.com/developers/applications/me>\n"      "Feel free to re-post it with the token removed. "      "If you believe this was a mistake, please let us know!" diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index b101b8816..65c729414 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,16 +1,20 @@  import logging +import random +import re +import unicodedata  from email.parser import HeaderParser  from io import StringIO -  from discord import Colour, Embed  from discord.ext.commands import AutoShardedBot, Context, command -from bot.constants import Roles -from bot.decorators import with_role +from bot.constants import Channels, NEGATIVE_REPLIES, Roles +from bot.decorators import InChannelCheckFailure, in_channel  log = logging.getLogger(__name__) +BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) +  class Utils:      """ @@ -24,7 +28,6 @@ class Utils:          self.base_github_pep_url = "https://raw.githubusercontent.com/python/peps/master/pep-"      @command(name='pep', aliases=('get_pep', 'p')) -    @with_role(Roles.verified)      async def pep_command(self, ctx: Context, pep_number: str):          """          Fetches information about a PEP and sends it to the channel. @@ -87,6 +90,58 @@ class Utils:          await ctx.message.channel.send(embed=pep_embed) +    @command() +    @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) +    async def charinfo(self, ctx, *, characters: str): +        """ +        Shows you information on up to 25 unicode characters. +        """ + +        match = re.match(r"<(a?):(\w+):(\d+)>", characters) +        if match: +            embed = Embed( +                title="Non-Character Detected", +                description=( +                    "Only unicode characters can be processed, but a custom Discord emoji " +                    "was found. Please remove it and try again." +                ) +            ) +            embed.colour = Colour.red() +            return await ctx.send(embed=embed) + +        if len(characters) > 25: +            embed = Embed(title=f"Too many characters ({len(characters)}/25)") +            embed.colour = Colour.red() +            return await ctx.send(embed=embed) + +        def get_info(char): +            digit = f"{ord(char):x}" +            if len(digit) <= 4: +                u_code = f"\\u{digit:>04}" +            else: +                u_code = f"\\U{digit:>08}" +            url = f"https://www.compart.com/en/unicode/U+{digit:>04}" +            name = f"[{unicodedata.name(char, '')}]({url})" +            info = f"`{u_code.ljust(10)}`: {name} - {char}" +            return info, u_code + +        charlist, rawlist = zip(*(get_info(c) for c in characters)) + +        embed = Embed(description="\n".join(charlist)) +        embed.set_author(name="Character Info") + +        if len(characters) > 1: +            embed.add_field(name='Raw', value=f"`{''.join(rawlist)}`", inline=False) + +        await ctx.send(embed=embed) + +    async def __error(self, ctx, error): +        embed = Embed(colour=Colour.red()) +        if isinstance(error, InChannelCheckFailure): +            embed.title = random.choice(NEGATIVE_REPLIES) +            embed.description = str(error) +            await ctx.send(embed=embed) +  def setup(bot):      bot.add_cog(Utils(bot)) diff --git a/bot/constants.py b/bot/constants.py index 5e7927ed9..be713cef2 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -201,9 +201,16 @@ class Filter(metaclass=YAMLGetter):      filter_zalgo: bool      filter_invites: bool      filter_domains: bool +    filter_rich_embeds: bool      watch_words: bool      watch_tokens: bool +    # Notifications are not expected for "watchlist" type filters +    notify_user_zalgo: bool +    notify_user_invites: bool +    notify_user_domains: bool +    notify_user_rich_embeds: bool +      ping_everyone: bool      guild_invite_whitelist: List[int]      domain_blacklist: List[str] @@ -292,6 +299,8 @@ class Icons(metaclass=YAMLGetter):      user_unmute: str      user_verified: str +    user_warn: str +      pencil: str      remind_blurple: str @@ -308,6 +317,13 @@ class CleanMessages(metaclass=YAMLGetter):      message_limit: int +class Categories(metaclass=YAMLGetter): +    section = "guild" +    subsection = "categories" + +    python_help: int + +  class Channels(metaclass=YAMLGetter):      section = "guild"      subsection = "channels" @@ -317,6 +333,7 @@ class Channels(metaclass=YAMLGetter):      big_brother_logs: int      bot: int      checkpoint_test: int +    defcon: int      devalerts: int      devlog: int      devtest: int @@ -456,6 +473,14 @@ class BigBrother(metaclass=YAMLGetter):      header_message_limit: int +class Free(metaclass=YAMLGetter): +    section = 'free' + +    activity_timeout: int +    cooldown_rate: int +    cooldown_per: float + +  # Debug mode  DEBUG_MODE = True if 'local' in os.environ.get("SITE_URL", "local") else False diff --git a/bot/decorators.py b/bot/decorators.py index 630c2267f..80311243d 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -1,12 +1,13 @@  import logging  import random +import typing  from asyncio import Lock  from functools import wraps  from weakref import WeakValueDictionary  from discord import Colour, Embed  from discord.ext import commands -from discord.ext.commands import Context +from discord.ext.commands import CheckFailure, Context  from bot.constants import ERROR_REPLIES  from bot.utils.checks import in_channel_check, with_role_check, without_role_check @@ -14,6 +15,38 @@ from bot.utils.checks import in_channel_check, with_role_check, without_role_che  log = logging.getLogger(__name__) +class InChannelCheckFailure(CheckFailure): +    pass + + +def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): +    """ +    Checks that the message is in a whitelisted channel or optionally has a bypass role. +    """ +    def predicate(ctx: Context): +        if ctx.channel.id in channels: +            log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                      f"The command was used in a whitelisted channel.") +            return True + +        if bypass_roles: +            if any(r.id in bypass_roles for r in ctx.author.roles): +                log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                          f"The command was not used in a whitelisted channel, " +                          f"but the author had a role to bypass the in_channel check.") +                return True + +        log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                  f"The in_channel check failed.") + +        channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) +        raise InChannelCheckFailure( +            f"Sorry, but you may only use this command within {channels_str}." +        ) + +    return commands.check(predicate) + +  def with_role(*role_ids: int):      """      Returns True if the user has any one diff --git a/bot/pagination.py b/bot/pagination.py index 0d8e8aaa3..72cfd83ef 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -17,6 +17,10 @@ PAGINATION_EMOJI = [FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMO  log = logging.getLogger(__name__) +class EmptyPaginatorEmbed(Exception): +    pass + +  class LinePaginator(Paginator):      """      A class that aids in paginating code blocks for Discord messages. @@ -96,7 +100,8 @@ class LinePaginator(Paginator):      async def paginate(cls, lines: Iterable[str], ctx: Context, embed: Embed,                         prefix: str = "", suffix: str = "", max_lines: Optional[int] = None, max_size: int = 500,                         empty: bool = True, restrict_to_user: User = None, timeout: int = 300, -                       footer_text: str = None): +                       footer_text: str = None, +                       exception_on_empty_embed: bool = False):          """          Use a paginator and set of reactions to provide pagination over a set of lines. The reactions are used to          switch page, or to finish with pagination. @@ -151,6 +156,14 @@ class LinePaginator(Paginator):          paginator = cls(prefix=prefix, suffix=suffix, max_size=max_size, max_lines=max_lines)          current_page = 0 +        if not lines: +            if exception_on_empty_embed: +                log.exception(f"Pagination asked for empty lines iterable") +                raise EmptyPaginatorEmbed("No lines to paginate") + +            log.debug("No lines to add to paginator, adding '(nothing to display)' message") +            lines.append("(nothing to display)") +          for line in lines:              try:                  paginator.add_line(line, empty=empty) @@ -315,7 +328,8 @@ class ImagePaginator(Paginator):      @classmethod      async def paginate(cls, pages: List[Tuple[str, str]], ctx: Context, embed: Embed, -                       prefix: str = "", suffix: str = "", timeout: int = 300): +                       prefix: str = "", suffix: str = "", timeout: int = 300, +                       exception_on_empty_embed: bool = False):          """          Use a paginator and set of reactions to provide          pagination over a set of title/image pairs.The reactions are @@ -361,6 +375,14 @@ class ImagePaginator(Paginator):          paginator = cls(prefix=prefix, suffix=suffix)          current_page = 0 +        if not pages: +            if exception_on_empty_embed: +                log.exception(f"Pagination asked for empty image list") +                raise EmptyPaginatorEmbed("No images to paginate") + +            log.debug("No images to add to paginator, adding '(no images to display)' message") +            pages.append(("(no images to display)", "")) +          for text, image_url in pages:              paginator.add_line(text)              paginator.add_image(image_url) diff --git a/config-default.yml b/config-default.yml index e7145289d..bb49a46e1 100644 --- a/config-default.yml +++ b/config-default.yml @@ -72,6 +72,8 @@ style:          user_unmute:   "https://cdn.discordapp.com/emojis/472472639206719508.png"          user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" +        user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" +          pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png"          remind_blurple: "https://cdn.discordapp.com/emojis/477907609215827968.png" @@ -83,12 +85,16 @@ style:  guild:      id: 267624335836053506 +    categories: +        python_help:                      356013061213126657 +      channels:          admins:            &ADMINS        365960823622991872          announcements:                    354619224620138496          big_brother_logs:  &BBLOGS        468507907357409333          bot:                              267659945086812160          checkpoint_test:                  422077681434099723 +        defcon:                           464469101889454091          devalerts:                        460181980097675264          devlog:            &DEVLOG        409308876241108992          devtest:           &DEVTEST       414574275865870337 @@ -131,11 +137,19 @@ guild:  filter:      # What do we filter? -    filter_zalgo:   false -    filter_invites: true -    filter_domains: true -    watch_words:    true -    watch_tokens:   true +    filter_zalgo:       false +    filter_invites:     true +    filter_domains:     true +    filter_rich_embeds: false +    watch_words:        true +    watch_tokens:       true + +    # Notify user on filter? +    # Notifications are not expected for "watchlist" type filters +    notify_user_zalgo:       false +    notify_user_invites:     true +    notify_user_domains:     false +    notify_user_rich_embeds: true      # Filter configuration      ping_everyone: true  # Ping @everyone when we send a mod-alert? @@ -145,6 +159,8 @@ filter:          - 267624335836053506  # Python Discord          - 440186186024222721  # Python Discord: ModLog Emojis          - 273944235143593984  # STEM +        - 348658686962696195  # RLBot +        - 531221516914917387  # Pallets      domain_blacklist:          - pornhub.com @@ -227,6 +243,7 @@ urls:      site_infractions_type:              !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"]      site_infractions_by_id:             !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"]      site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"] +    site_infractions_user_type:         !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"]      site_logs_api:                      !JOIN [*SCHEMA, *API, "/bot/logs"]      site_logs_view:                     !JOIN [*SCHEMA, *DOMAIN, "/bot/logs"]      site_names_api:                     !JOIN [*SCHEMA, *API, "/bot/snake_names"] @@ -323,5 +340,13 @@ big_brother:      header_message_limit: 15 +free: +    # Seconds to elapse for a channel +    # to be considered inactive. +    activity_timeout: 600 +    cooldown_rate: 1 +    cooldown_per: 60.0 + +  config:      required_keys: ['bot.token']  |