diff options
| author | 2019-01-16 23:18:57 +1000 | |
|---|---|---|
| committer | 2019-01-16 23:18:57 +1000 | |
| commit | 38c13951e883a9ce8141c2d0942104672c8e65e4 (patch) | |
| tree | d016ad628c2d7fcf0a774c518c723c8a75e2dec7 | |
| parent | Tidy, correct order of actions and notifications. (diff) | |
| parent | Merge pull request #256 from python-discord/fix-bb-emoji (diff) | |
Merge branch 'master' of https://github.com/python-discord/bot into reorder-mod-actions
# Conflicts:
# bot/cogs/modlog.py
| -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 | 124 | ||||
| -rw-r--r-- | bot/cogs/free.py | 127 | ||||
| -rw-r--r-- | bot/cogs/help.py | 12 | ||||
| -rw-r--r-- | bot/cogs/information.py | 35 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 60 | ||||
| -rw-r--r-- | bot/cogs/reminders.py | 2 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 2 | ||||
| -rw-r--r-- | bot/cogs/token_remover.py | 5 | ||||
| -rw-r--r-- | bot/constants.py | 23 | ||||
| -rw-r--r-- | bot/decorators.py | 36 | ||||
| -rw-r--r-- | bot/pagination.py | 26 | ||||
| -rw-r--r-- | bot/utils/checks.py | 56 | ||||
| -rw-r--r-- | config-default.yml | 35 |
24 files changed, 855 insertions, 296 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 0ba1e49c5..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("\\", "") @@ -240,8 +297,9 @@ class Filtering: response = await response.json() guild = response.get("guild") if guild is None: - # We don't have whitelisted Group DMs so we can - # go ahead and return a positive for any group DM + # 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(guild.get("id")) @@ -250,6 +308,28 @@ class Filtering: 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/information.py b/bot/cogs/information.py index 7a244cdbe..129166d2f 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -1,11 +1,13 @@ import logging +import random import textwrap from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import BadArgument, Bot, CommandError, Context, MissingPermissions, command -from bot.constants import Emojis, Keys, Roles, URLs +from bot.constants import Channels, Emojis, Keys, NEGATIVE_REPLIES, Roles, URLs from bot.decorators import with_role +from bot.utils.checks import with_role_check from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -121,13 +123,23 @@ class Information: await ctx.send(embed=embed) - @with_role(*MODERATION_ROLES) @command(name="user", aliases=["user_info", "member", "member_info"]) async def user_info(self, ctx: Context, user: Member = None, hidden: bool = False): """ Returns info about a user. """ + # Do a role check if this is being executed on + # someone other than the caller + if user and user != ctx.author: + if not with_role_check(ctx, *MODERATION_ROLES): + raise BadArgument("You do not have permission to use this command on users other than yourself.") + + # Non-moderators may only do this in #bot-commands + if not with_role_check(ctx, *MODERATION_ROLES): + if not ctx.channel.id == Channels.bot: + raise MissingPermissions("You can't do that here!") + # Validates hidden input hidden = str(hidden) @@ -192,6 +204,23 @@ class Information: await ctx.send(embed=embed) + @user_info.error + async def user_info_command_error(self, ctx: Context, error: CommandError): + embed = Embed(colour=Colour.red()) + + if isinstance(error, BadArgument): + embed.title = random.choice(NEGATIVE_REPLIES) + embed.description = str(error) + await ctx.send(embed=embed) + + elif isinstance(error, MissingPermissions): + embed.title = random.choice(NEGATIVE_REPLIES) + embed.description = f"Sorry, but you may only use this command within <#{Channels.bot}>." + await ctx.send(embed=embed) + + else: + log.exception(f"Unhandled error: {error}") + def setup(bot): bot.add_cog(Information(bot)) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 682fd1040..06b9ecfe6 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -104,9 +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, content: str = None, footer: str = 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: Optional[str] = None, ): embed = Embed(description=text) @@ -114,9 +124,13 @@ 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.timestamp = timestamp_override or datetime.datetime.utcnow() + + if footer: + embed.set_footer(text=footer) + + if thumbnail: embed.set_thumbnail(url=thumbnail) if ping_everyone: @@ -125,10 +139,15 @@ class ModLog: else: content = "@everyone" - if footer: - embed.set_footer(text=footer) + channel = self.bot.get_channel(channel_id) + log_message = await channel.send(content=content, embed=embed, files=files) - await self.bot.get_channel(channel_id).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: @@ -179,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 = [] @@ -667,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/tags.py b/bot/cogs/tags.py index b128b6de1..8ecd80127 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -149,7 +149,7 @@ class Tags: tags = [] - embed = Embed() + embed: Embed = Embed() embed.colour = Colour.red() tag_data = await self.get_tag_data(tag_name) 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/constants.py b/bot/constants.py index 99ef98da2..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] @@ -310,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" @@ -319,6 +333,7 @@ class Channels(metaclass=YAMLGetter): big_brother_logs: int bot: int checkpoint_test: int + defcon: int devalerts: int devlog: int devtest: int @@ -458,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 87877ecbf..710045c10 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -10,6 +10,7 @@ from discord.ext import commands from discord.ext.commands import CheckFailure, Context from bot.constants import ERROR_REPLIES +from bot.utils.checks import with_role_check, without_role_check log = logging.getLogger(__name__) @@ -47,35 +48,24 @@ def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): def with_role(*role_ids: int): - async def predicate(ctx: Context): - if not ctx.guild: # Return False in a DM - log.debug(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " - "This command is restricted by the with_role decorator. Rejecting request.") - return False - - for role in ctx.author.roles: - if role.id in role_ids: - log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.") - return True + """ + Returns True if the user has any one + of the roles in role_ids. + """ - log.debug(f"{ctx.author} does not have the required role to use " - f"the '{ctx.command.name}' command, so the request is rejected.") - return False + async def predicate(ctx: Context): + return with_role_check(ctx, *role_ids) return commands.check(predicate) def without_role(*role_ids: int): - async def predicate(ctx: Context): - if not ctx.guild: # Return False in a DM - log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " - "This command is restricted by the without_role decorator. Rejecting request.") - return False + """ + Returns True if the user does not have any + of the roles in role_ids. + """ - author_roles = [role.id for role in ctx.author.roles] - check = all(role not in author_roles for role in role_ids) - log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The result of the without_role check was {check}.") - return check + async def predicate(ctx: Context): + return without_role_check(ctx, *role_ids) return commands.check(predicate) 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/bot/utils/checks.py b/bot/utils/checks.py new file mode 100644 index 000000000..37dc657f7 --- /dev/null +++ b/bot/utils/checks.py @@ -0,0 +1,56 @@ +import logging + +from discord.ext.commands import Context + +log = logging.getLogger(__name__) + + +def with_role_check(ctx: Context, *role_ids: int) -> bool: + """ + Returns True if the user has any one + of the roles in role_ids. + """ + + if not ctx.guild: # Return False in a DM + log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " + "This command is restricted by the with_role decorator. Rejecting request.") + return False + + for role in ctx.author.roles: + if role.id in role_ids: + log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.") + return True + + log.trace(f"{ctx.author} does not have the required role to use " + f"the '{ctx.command.name}' command, so the request is rejected.") + return False + + +def without_role_check(ctx: Context, *role_ids: int) -> bool: + """ + Returns True if the user does not have any + of the roles in role_ids. + """ + + if not ctx.guild: # Return False in a DM + log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " + "This command is restricted by the without_role decorator. Rejecting request.") + return False + + author_roles = (role.id for role in ctx.author.roles) + check = all(role not in author_roles for role in role_ids) + log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. " + f"The result of the without_role check was {check}.") + return check + + +def in_channel_check(ctx: Context, channel_id: int) -> bool: + """ + Checks if the command was executed + inside of the specified channel. + """ + + check = ctx.channel.id == channel_id + log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. " + f"The result of the in_channel check was {check}.") + return check diff --git a/config-default.yml b/config-default.yml index 3a1ad8052..b6427b489 100644 --- a/config-default.yml +++ b/config-default.yml @@ -25,7 +25,7 @@ style: green_chevron: "<:greenchevron:418104310329769993>" red_chevron: "<:redchevron:418112778184818698>" white_chevron: "<:whitechevron:418110396973711363>" - bb_message: "<:bbmessage:472476937504423936>" + bb_message: "<:bbmessage:476273120999636992>" status_online: "<:status_online:470326272351010816>" status_idle: "<:status_idle:470326266625785866>" @@ -85,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 @@ -133,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? @@ -147,6 +159,8 @@ filter: - 267624335836053506 # Python Discord - 440186186024222721 # Python Discord: ModLog Emojis - 273944235143593984 # STEM + - 348658686962696195 # RLBot + - 531221516914917387 # Pallets domain_blacklist: - pornhub.com @@ -229,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"] @@ -325,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'] |