aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Scragly <[email protected]>2019-01-16 23:18:57 +1000
committerGravatar Scragly <[email protected]>2019-01-16 23:18:57 +1000
commit38c13951e883a9ce8141c2d0942104672c8e65e4 (patch)
treed016ad628c2d7fcf0a774c518c723c8a75e2dec7
parentTidy, correct order of actions and notifications. (diff)
parentMerge 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.yaml5
-rw-r--r--Pipfile7
-rw-r--r--Pipfile.lock419
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/alias.py2
-rw-r--r--bot/cogs/antispam.py35
-rw-r--r--bot/cogs/bigbrother.py62
-rw-r--r--bot/cogs/bot.py3
-rw-r--r--bot/cogs/defcon.py40
-rw-r--r--bot/cogs/eval.py2
-rw-r--r--bot/cogs/events.py32
-rw-r--r--bot/cogs/filtering.py124
-rw-r--r--bot/cogs/free.py127
-rw-r--r--bot/cogs/help.py12
-rw-r--r--bot/cogs/information.py35
-rw-r--r--bot/cogs/modlog.py60
-rw-r--r--bot/cogs/reminders.py2
-rw-r--r--bot/cogs/tags.py2
-rw-r--r--bot/cogs/token_remover.py5
-rw-r--r--bot/constants.py23
-rw-r--r--bot/decorators.py36
-rw-r--r--bot/pagination.py26
-rw-r--r--bot/utils/checks.py56
-rw-r--r--config-default.yml35
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
diff --git a/Pipfile b/Pipfile
index 179b317df..3df5ea7c0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -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']