aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock260
-rw-r--r--bot/constants.py13
-rw-r--r--bot/converters.py17
-rw-r--r--bot/exts/backend/error_handler.py2
-rw-r--r--bot/exts/filters/filtering.py66
-rw-r--r--bot/exts/info/information.py13
-rw-r--r--bot/exts/info/pypi.py9
-rw-r--r--bot/exts/info/tags.py7
-rw-r--r--bot/exts/moderation/defcon.py315
-rw-r--r--bot/exts/moderation/slowmode.py4
-rw-r--r--bot/log.py49
-rw-r--r--bot/utils/time.py42
-rw-r--r--config-default.yml15
14 files changed, 510 insertions, 303 deletions
diff --git a/Pipfile b/Pipfile
index 0a94fb888..024aa6eff 100644
--- a/Pipfile
+++ b/Pipfile
@@ -28,6 +28,7 @@ sphinx = "~=2.2"
statsd = "~=3.3"
arrow = "~=0.17"
emoji = "~=0.6"
+python-json-logger = "~=2.0"
[dev-packages]
coverage = "~=5.0"
diff --git a/Pipfile.lock b/Pipfile.lock
index f8cedb08f..dc7f6f21f 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "228ae55fe5700ac3827ba6b661933b60b1d06f44fea8bcbe8c5a769fa10ab2fd"
+ "sha256": "81ca9d1891e71de1c3f71958f082e1a8cad71e5b3ca425dc561d0ae74664fdb0"
},
"pipfile-spec": 6,
"requires": {
@@ -18,11 +18,11 @@
"default": {
"aio-pika": {
"hashes": [
- "sha256:9773440a89840941ac3099a7720bf9d51e8764a484066b82ede4d395660ff430",
- "sha256:a8065be3c722eb8f9fff8c0e7590729e7782202cdb9363d9830d7d5d47b45c7c"
+ "sha256:1d4305a5f78af3857310b4fe48348cdcf6c097e0e275ea88c2cd08570531a369",
+ "sha256:e69afef8695f47c5d107bbdba21bdb845d5c249acb3be53ef5c2d497b02657c0"
],
"index": "pypi",
- "version": "==6.7.1"
+ "version": "==6.8.0"
},
"aiodns": {
"hashes": [
@@ -96,6 +96,7 @@
"sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573",
"sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.3.1"
},
"alabaster": {
@@ -122,6 +123,7 @@
"sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af"
],
"index": "pypi",
+ "markers": "python_version ~= '3.7'",
"version": "==0.1.4"
},
"async-timeout": {
@@ -129,6 +131,7 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
+ "markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
@@ -136,6 +139,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"babel": {
@@ -143,6 +147,7 @@
"sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5",
"sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9.0"
},
"beautifulsoup4": {
@@ -215,7 +220,6 @@
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
- "index": "pypi",
"markers": "sys_platform == 'win32'",
"version": "==0.4.4"
},
@@ -248,6 +252,7 @@
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.16"
},
"emoji": {
@@ -330,6 +335,7 @@
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"humanfriendly": {
@@ -337,6 +343,7 @@
"sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d",
"sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==9.1"
},
"idna": {
@@ -344,6 +351,7 @@
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"imagesize": {
@@ -351,6 +359,7 @@
"sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
"sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.0"
},
"jinja2": {
@@ -358,6 +367,7 @@
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3"
},
"lxml": {
@@ -466,15 +476,16 @@
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"more-itertools": {
"hashes": [
- "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330",
- "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"
+ "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced",
+ "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"
],
"index": "pypi",
- "version": "==8.6.0"
+ "version": "==8.7.0"
},
"multidict": {
"hashes": [
@@ -516,12 +527,14 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"ordered-set": {
"hashes": [
"sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"
],
+ "markers": "python_version >= '3.5'",
"version": "==4.0.2"
},
"packaging": {
@@ -529,6 +542,7 @@
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.9"
},
"pamqp": {
@@ -577,6 +591,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pygments": {
@@ -584,6 +599,7 @@
"sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0",
"sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"
],
+ "markers": "python_version >= '3.5'",
"version": "==2.8.0"
},
"pyparsing": {
@@ -591,6 +607,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"python-dateutil": {
@@ -601,6 +618,13 @@
"index": "pypi",
"version": "==2.8.1"
},
+ "python-json-logger": {
+ "hashes": [
+ "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398"
+ ],
+ "index": "pypi",
+ "version": "==2.0.1"
+ },
"pytz": {
"hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
@@ -610,28 +634,37 @@
},
"pyyaml": {
"hashes": [
- "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
- "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
- "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
- "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
- "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
- "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
- "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
- "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
- "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
- "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
- "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
- "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
- "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
+ "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
+ "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
+ "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
+ "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
+ "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
+ "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
+ "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
+ "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
+ "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
+ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
+ "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
+ "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
+ "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
+ "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
+ "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
+ "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
+ "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
+ "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
+ "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
+ "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
+ "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"
],
"index": "pypi",
- "version": "==5.3.1"
+ "version": "==5.4.1"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.5.3"
},
"requests": {
@@ -644,17 +677,18 @@
},
"sentry-sdk": {
"hashes": [
- "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0",
- "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f"
+ "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237",
+ "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"
],
"index": "pypi",
- "version": "==0.19.5"
+ "version": "==0.20.3"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"snowballstemmer": {
@@ -692,6 +726,7 @@
"sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
"sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.2"
},
"sphinxcontrib-devhelp": {
@@ -699,6 +734,7 @@
"sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
"sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.2"
},
"sphinxcontrib-htmlhelp": {
@@ -706,6 +742,7 @@
"sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
"sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.3"
},
"sphinxcontrib-jsmath": {
@@ -713,6 +750,7 @@
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
@@ -720,6 +758,7 @@
"sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
"sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.3"
},
"sphinxcontrib-serializinghtml": {
@@ -727,6 +766,7 @@
"sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc",
"sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.1.4"
},
"statsd": {
@@ -750,6 +790,7 @@
"sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80",
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.3"
},
"yarl": {
@@ -792,6 +833,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
+ "markers": "python_version >= '3.6'",
"version": "==1.6.3"
}
},
@@ -808,6 +850,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"certifi": {
@@ -822,6 +865,7 @@
"sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d",
"sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"
],
+ "markers": "python_full_version >= '3.6.1'",
"version": "==3.2.0"
},
"chardet": {
@@ -833,58 +877,61 @@
},
"coverage": {
"hashes": [
- "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
- "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
- "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
- "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
- "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
- "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
- "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
- "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
- "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
- "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
- "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
- "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
- "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
- "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
- "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
- "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
- "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
- "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
- "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
- "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
- "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
- "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
- "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
- "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
- "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
- "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
- "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
- "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
- "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
- "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
- "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
- "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
- "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
- "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
- "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
- "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
- "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
- "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
- "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
- "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
- "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
- "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
- "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
- "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
- "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
- "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
- "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
- "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
- "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
+ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
+ "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
+ "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
+ "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
+ "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
+ "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
+ "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
+ "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
+ "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
+ "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
+ "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
+ "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
+ "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
+ "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
+ "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
+ "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
+ "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
+ "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
+ "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
+ "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
+ "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
+ "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
+ "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
+ "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
+ "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
+ "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
+ "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
+ "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
+ "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
+ "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
+ "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
+ "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
+ "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
+ "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
+ "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
+ "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
+ "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
+ "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
+ "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
+ "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
+ "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
+ "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
+ "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
+ "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
+ "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
+ "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
+ "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
+ "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
+ "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
+ "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
+ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
+ "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
],
"index": "pypi",
- "version": "==5.3.1"
+ "version": "==5.5"
},
"coveralls": {
"hashes": [
@@ -924,11 +971,11 @@
},
"flake8-annotations": {
"hashes": [
- "sha256:3a377140556aecf11fa9f3bb18c10db01f5ea56dc79a730e2ec9b4f1f49e2055",
- "sha256:e17947a48a5b9f632fe0c72682fc797c385e451048e7dfb20139f448a074cb3e"
+ "sha256:8968ff12f296433028ad561c680ccc03a7cd62576d100c3f1475e058b3c11b43",
+ "sha256:bd0505616c0d85ebb45c6052d339c69f320d3f87fa079ab4e91a4f234a863d05"
],
"index": "pypi",
- "version": "==2.5.0"
+ "version": "==2.6.0"
},
"flake8-bugbear": {
"hashes": [
@@ -986,16 +1033,18 @@
},
"identify": {
"hashes": [
- "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc",
- "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3"
+ "sha256:2179e7359471ab55729f201b3fdf7dc2778e221f868410fedcb0987b791ba552",
+ "sha256:2a5fdf2f5319cc357eda2550bea713a404392495961022cf2462624ce62f0f46"
],
- "version": "==1.5.14"
+ "markers": "python_full_version >= '3.6.1'",
+ "version": "==2.1.0"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"mccabe": {
@@ -1022,17 +1071,18 @@
},
"pre-commit": {
"hashes": [
- "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0",
- "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4"
+ "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e",
+ "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a"
],
"index": "pypi",
- "version": "==2.9.3"
+ "version": "==2.10.1"
},
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pydocstyle": {
@@ -1040,6 +1090,7 @@
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
],
+ "markers": "python_version >= '3.5'",
"version": "==5.1.1"
},
"pyflakes": {
@@ -1047,26 +1098,35 @@
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"pyyaml": {
"hashes": [
- "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
- "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
- "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
- "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
- "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
- "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
- "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
- "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
- "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
- "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
- "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
- "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
- "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
+ "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
+ "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
+ "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
+ "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
+ "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
+ "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
+ "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
+ "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
+ "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
+ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
+ "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
+ "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
+ "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
+ "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
+ "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
+ "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
+ "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
+ "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
+ "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
+ "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
+ "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"
],
"index": "pypi",
- "version": "==5.3.1"
+ "version": "==5.4.1"
},
"requests": {
"hashes": [
@@ -1081,6 +1141,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"snowballstemmer": {
@@ -1095,6 +1156,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"urllib3": {
@@ -1102,6 +1164,7 @@
"sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80",
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.3"
},
"virtualenv": {
@@ -1109,6 +1172,7 @@
"sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d",
"sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.4.2"
}
}
diff --git a/bot/constants.py b/bot/constants.py
index 7cf31e835..394d59a73 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -280,9 +280,9 @@ class Emojis(metaclass=YAMLGetter):
badge_staff: str
badge_verified_bot_developer: str
- defcon_disabled: str # noqa: E704
- defcon_enabled: str # noqa: E704
- defcon_updated: str # noqa: E704
+ defcon_shutdown: str # noqa: E704
+ defcon_unshutdown: str # noqa: E704
+ defcon_update: str # noqa: E704
failmail: str
@@ -319,9 +319,9 @@ class Icons(metaclass=YAMLGetter):
crown_red: str
defcon_denied: str # noqa: E704
- defcon_disabled: str # noqa: E704
- defcon_enabled: str # noqa: E704
- defcon_updated: str # noqa: E704
+ defcon_shutdown: str # noqa: E704
+ defcon_unshutdown: str # noqa: E704
+ defcon_update: str # noqa: E704
filtering: str
@@ -487,6 +487,7 @@ class Roles(metaclass=YAMLGetter):
admins: int
core_developers: int
+ devops: int
helpers: int
moderators: int
owners: int
diff --git a/bot/converters.py b/bot/converters.py
index 80ce99459..67525cd4d 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -16,6 +16,7 @@ from discord.utils import DISCORD_EPOCH, snowflake_time
from bot.api import ResponseCodeError
from bot.constants import URLs
from bot.utils.regex import INVITE_RE
+from bot.utils.time import parse_duration_string
log = logging.getLogger(__name__)
@@ -301,16 +302,6 @@ class TagContentConverter(Converter):
class DurationDelta(Converter):
"""Convert duration strings into dateutil.relativedelta.relativedelta objects."""
- duration_parser = re.compile(
- r"((?P<years>\d+?) ?(years|year|Y|y) ?)?"
- r"((?P<months>\d+?) ?(months|month|m) ?)?"
- r"((?P<weeks>\d+?) ?(weeks|week|W|w) ?)?"
- r"((?P<days>\d+?) ?(days|day|D|d) ?)?"
- r"((?P<hours>\d+?) ?(hours|hour|H|h) ?)?"
- r"((?P<minutes>\d+?) ?(minutes|minute|M) ?)?"
- r"((?P<seconds>\d+?) ?(seconds|second|S|s))?"
- )
-
async def convert(self, ctx: Context, duration: str) -> relativedelta:
"""
Converts a `duration` string to a relativedelta object.
@@ -326,13 +317,9 @@ class DurationDelta(Converter):
The units need to be provided in descending order of magnitude.
"""
- match = self.duration_parser.fullmatch(duration)
- if not match:
+ if not (delta := parse_duration_string(duration)):
raise BadArgument(f"`{duration}` is not a valid duration string.")
- duration_dict = {unit: int(amount) for unit, amount in match.groupdict(default=0).items()}
- delta = relativedelta(**duration_dict)
-
return delta
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index d2cce5558..9cb54cdab 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -239,10 +239,12 @@ class ErrorHandler(Cog):
elif isinstance(e, errors.BadUnionArgument):
embed = self._get_error_embed("Bad argument", f"{e}\n{e.errors[-1]}")
await ctx.send(embed=embed)
+ await prepared_help_command
self.bot.stats.incr("errors.bad_union_argument")
elif isinstance(e, errors.ArgumentParsingError):
embed = self._get_error_embed("Argument parsing error", str(e))
await ctx.send(embed=embed)
+ prepared_help_command.close()
self.bot.stats.incr("errors.argument_parsing_error")
else:
embed = self._get_error_embed(
diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py
index 3527bf8bb..c90b18dcb 100644
--- a/bot/exts/filters/filtering.py
+++ b/bot/exts/filters/filtering.py
@@ -2,7 +2,7 @@ import asyncio
import logging
import re
from datetime import datetime, timedelta
-from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Union
+from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union
import dateutil
import discord.errors
@@ -137,6 +137,10 @@ class Filtering(Cog):
"""Fetch items from the filter_list_cache."""
return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"].keys()
+ def _get_filterlist_value(self, list_type: str, value: Any, *, allowed: bool) -> dict:
+ """Fetch one specific value from filter_list_cache."""
+ return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"][value]
+
@staticmethod
def _expand_spoilers(text: str) -> str:
"""Return a string containing all interpretations of a spoilered message."""
@@ -236,7 +240,13 @@ class Filtering(Cog):
# We also do not need to worry about filters that take the full message,
# since all we have is an arbitrary string.
if _filter["enabled"] and _filter["content_only"]:
- match = await _filter["function"](result)
+ filter_result = await _filter["function"](result)
+ reason = None
+
+ if isinstance(filter_result, tuple):
+ match, reason = filter_result
+ else:
+ match = filter_result
if match:
# If this is a filter (not a watchlist), we set the variable so we know
@@ -245,7 +255,7 @@ class Filtering(Cog):
filter_triggered = True
stats = self._add_stats(filter_name, match, result)
- await self._send_log(filter_name, _filter, msg, stats, is_eval=True)
+ await self._send_log(filter_name, _filter, msg, stats, reason, is_eval=True)
break # We don't want multiple filters to trigger
@@ -267,9 +277,17 @@ class Filtering(Cog):
# Does the filter only need the message content or the full message?
if _filter["content_only"]:
- match = await _filter["function"](msg.content)
+ payload = msg.content
+ else:
+ payload = msg
+
+ result = await _filter["function"](payload)
+ reason = None
+
+ if isinstance(result, tuple):
+ match, reason = result
else:
- match = await _filter["function"](msg)
+ match = result
if match:
is_private = msg.channel.type is discord.ChannelType.private
@@ -316,7 +334,7 @@ class Filtering(Cog):
log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}")
stats = self._add_stats(filter_name, match, msg.content)
- await self._send_log(filter_name, _filter, msg, stats)
+ await self._send_log(filter_name, _filter, msg, stats, reason)
break # We don't want multiple filters to trigger
@@ -326,6 +344,7 @@ class Filtering(Cog):
_filter: Dict[str, Any],
msg: discord.Message,
stats: Stats,
+ reason: Optional[str] = None,
*,
is_eval: bool = False,
) -> None:
@@ -339,6 +358,7 @@ class Filtering(Cog):
ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True)
eval_msg = "using !eval " if is_eval else ""
+ footer = f"Reason: {reason}" if reason else None
message = (
f"The {filter_name} {_filter['type']} was triggered by {format_user(msg.author)} "
f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n"
@@ -357,6 +377,7 @@ class Filtering(Cog):
channel_id=Channels.mod_alerts,
ping_everyone=ping_everyone,
additional_embeds=stats.additional_embeds,
+ footer=footer,
)
def _add_stats(self, name: str, match: FilterMatch, content: str) -> Stats:
@@ -381,13 +402,14 @@ class Filtering(Cog):
if name == "filter_invites" and match is not True:
additional_embeds = []
for _, data in match.items():
+ reason = f"Reason: {data['reason']} | " if data.get('reason') else ""
embed = discord.Embed(description=(
f"**Members:**\n{data['members']}\n"
f"**Active:**\n{data['active']}"
))
embed.set_author(name=data["name"])
embed.set_thumbnail(url=data["icon"])
- embed.set_footer(text=f"Guild ID: {data['id']}")
+ embed.set_footer(text=f"{reason}Guild ID: {data['id']}")
additional_embeds.append(embed)
elif name == "watch_rich_embeds":
@@ -411,39 +433,46 @@ class Filtering(Cog):
and not msg.author.bot # Author not a bot
)
- async def _has_watch_regex_match(self, text: str) -> Union[bool, re.Match]:
+ async def _has_watch_regex_match(self, text: str) -> Tuple[Union[bool, re.Match], Optional[str]]:
"""
Return True if `text` matches any regex from `word_watchlist` or `token_watchlist` configs.
`word_watchlist`'s patterns are placed between word boundaries while `token_watchlist` is
matched as-is. Spoilers are expanded, if any, and URLs are ignored.
+ Second return value is a reason written to database about blacklist entry (can be None).
"""
if SPOILER_RE.search(text):
text = self._expand_spoilers(text)
# Make sure it's not a URL
if URL_RE.search(text):
- return False
+ return False, None
watchlist_patterns = self._get_filterlist_items('filter_token', allowed=False)
for pattern in watchlist_patterns:
match = re.search(pattern, text, flags=re.IGNORECASE)
if match:
- return match
+ return match, self._get_filterlist_value('filter_token', pattern, allowed=False)['comment']
+
+ return False, None
- async def _has_urls(self, text: str) -> bool:
- """Returns True if the text contains one of the blacklisted URLs from the config file."""
+ async def _has_urls(self, text: str) -> Tuple[bool, Optional[str]]:
+ """
+ Returns True if the text contains one of the blacklisted URLs from the config file.
+
+ Second return value is a reason of URL blacklisting (can be None).
+ """
if not URL_RE.search(text):
- return False
+ return False, None
text = text.lower()
domain_blacklist = self._get_filterlist_items("domain_name", allowed=False)
for url in domain_blacklist:
if url.lower() in text:
- return True
+ return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"]
- return False
+ return False, None
@staticmethod
async def _has_zalgo(text: str) -> bool:
@@ -500,6 +529,10 @@ class Filtering(Cog):
)
if invite_not_allowed:
+ reason = None
+ if guild_id in guild_invite_blacklist:
+ reason = self._get_filterlist_value("guild_invite", guild_id, allowed=False)["comment"]
+
guild_icon_hash = guild["icon"]
guild_icon = (
"https://cdn.discordapp.com/icons/"
@@ -511,7 +544,8 @@ class Filtering(Cog):
"id": guild['id'],
"icon": guild_icon,
"members": response["approximate_member_count"],
- "active": response["approximate_presence_count"]
+ "active": response["approximate_presence_count"],
+ "reason": reason
}
return invite_data if invite_data else False
diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index 88e904d03..92ddf0fbd 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -17,7 +17,7 @@ from bot.decorators import in_whitelist
from bot.pagination import LinePaginator
from bot.utils.channel import is_mod_channel, is_staff_channel
from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check
-from bot.utils.time import time_since
+from bot.utils.time import humanize_delta, time_since
log = logging.getLogger(__name__)
@@ -52,7 +52,7 @@ class Information(Cog):
)
return {role.name.title(): len(role.members) for role in roles}
- def get_extended_server_info(self) -> str:
+ def get_extended_server_info(self, ctx: Context) -> str:
"""Return additional server info only visible in moderation channels."""
talentpool_info = ""
if cog := self.bot.get_cog("Talentpool"):
@@ -64,9 +64,9 @@ class Information(Cog):
defcon_info = ""
if cog := self.bot.get_cog("Defcon"):
- defcon_status = "Enabled" if cog.enabled else "Disabled"
- defcon_days = cog.days.days if cog.enabled else "-"
- defcon_info = f"Defcon status: {defcon_status}\nDefcon days: {defcon_days}\n"
+ defcon_info = f"Defcon threshold: {humanize_delta(cog.threshold)}\n"
+
+ verification = f"Verification level: {ctx.guild.verification_level.name}\n"
python_general = self.bot.get_channel(constants.Channels.python_general)
@@ -74,6 +74,7 @@ class Information(Cog):
{talentpool_info}\
{bb_info}\
{defcon_info}\
+ {verification}\
{python_general.mention} cooldown: {python_general.slowmode_delay}s
""")
@@ -198,7 +199,7 @@ class Information(Cog):
# Additional info if ran in moderation channels
if is_mod_channel(ctx.channel):
- embed.add_field(name="Moderation:", value=self.get_extended_server_info())
+ embed.add_field(name="Moderation:", value=self.get_extended_server_info(ctx))
await ctx.send(embed=embed)
diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py
index 3e326e8bb..8fe249c8a 100644
--- a/bot/exts/info/pypi.py
+++ b/bot/exts/info/pypi.py
@@ -1,6 +1,7 @@
import itertools
import logging
import random
+import re
from discord import Embed
from discord.ext.commands import Cog, Context, command
@@ -12,8 +13,11 @@ from bot.constants import Colours, NEGATIVE_REPLIES
URL = "https://pypi.org/pypi/{package}/json"
FIELDS = ("author", "requires_python", "summary", "license")
PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png"
+
PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white))
+ILLEGAL_CHARACTERS = re.compile(r"[^a-zA-Z0-9-.]+")
+
log = logging.getLogger(__name__)
@@ -32,6 +36,11 @@ class PyPi(Cog):
)
embed.set_thumbnail(url=PYPI_ICON)
+ if (character := re.search(ILLEGAL_CHARACTERS, package)) is not None:
+ embed.description = f"Illegal character passed into command: '{escape_markdown(character.group(0))}'"
+ await ctx.send(embed=embed)
+ return
+
async with self.bot.http_session.get(URL.format(package=package)) as response:
if response.status == 404:
embed.description = "Package could not be found."
diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py
index 00b4d1a78..bb91a8563 100644
--- a/bot/exts/info/tags.py
+++ b/bot/exts/info/tags.py
@@ -189,7 +189,7 @@ class Tags(Cog):
If a tag is not specified, display a paginated embed of all tags.
Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display
- nothing and return False.
+ nothing and return True.
"""
def _command_on_cooldown(tag_name: str) -> bool:
"""
@@ -217,7 +217,7 @@ class Tags(Cog):
f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
f"Cooldown ends in {time_left:.1f} seconds."
)
- return False
+ return True
if tag_name is not None:
temp_founds = self._get_tag(tag_name)
@@ -285,7 +285,8 @@ class Tags(Cog):
"""
Get a specified tag, or a list of all tags if no tag is specified.
- Returns False if a tag is on cooldown, or if no matches are found.
+ Returns True if something can be sent, or if the tag is on cooldown.
+ Returns False if no matches are found.
"""
return await self.display_tag(ctx, tag_name)
diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py
index caa6fb917..bd16289b9 100644
--- a/bot/exts/moderation/defcon.py
+++ b/bot/exts/moderation/defcon.py
@@ -1,17 +1,25 @@
-from __future__ import annotations
-
+import asyncio
import logging
+import traceback
from collections import namedtuple
-from datetime import datetime, timedelta
+from datetime import datetime
from enum import Enum
+from typing import Optional, Union
-from discord import Colour, Embed, Member
+from aioredis import RedisError
+from async_rediscache import RedisCache
+from dateutil.relativedelta import relativedelta
+from discord import Colour, Embed, Member, User
+from discord.ext import tasks
from discord.ext.commands import Cog, Context, group, has_any_role
from bot.bot import Bot
from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles
+from bot.converters import DurationDelta, Expiry
from bot.exts.moderation.modlog import ModLog
from bot.utils.messages import format_user
+from bot.utils.scheduling import Scheduler
+from bot.utils.time import humanize_delta, parse_duration_string, relativedelta_to_timedelta
log = logging.getLogger(__name__)
@@ -28,71 +36,81 @@ will be resolved soon. In the meantime, please feel free to peruse the resources
BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism"
+SECONDS_IN_DAY = 86400
+
class Action(Enum):
"""Defcon Action."""
- ActionInfo = namedtuple('LogInfoDetails', ['icon', 'color', 'template'])
+ ActionInfo = namedtuple('LogInfoDetails', ['icon', 'emoji', 'color', 'template'])
- ENABLED = ActionInfo(Icons.defcon_enabled, Colours.soft_green, "**Days:** {days}\n\n")
- DISABLED = ActionInfo(Icons.defcon_disabled, Colours.soft_red, "")
- UPDATED = ActionInfo(Icons.defcon_updated, Colour.blurple(), "**Days:** {days}\n\n")
+ SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Emojis.defcon_unshutdown, Colours.soft_green, "")
+ SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Emojis.defcon_shutdown, Colours.soft_red, "")
+ DURATION_UPDATE = ActionInfo(
+ Icons.defcon_update, Emojis.defcon_update, Colour.blurple(), "**Threshold:** {threshold}\n\n"
+ )
class Defcon(Cog):
"""Time-sensitive server defense mechanisms."""
- days = None # type: timedelta
- enabled = False # type: bool
+ # RedisCache[str, str]
+ # The cache's keys are "threshold" and "expiry".
+ # The caches' values are strings formatted as valid input to the DurationDelta converter, or empty when off.
+ defcon_settings = RedisCache()
def __init__(self, bot: Bot):
self.bot = bot
self.channel = None
- self.days = timedelta(days=0)
+ self.threshold = relativedelta(days=0)
+ self.expiry = None
+
+ self.scheduler = Scheduler(self.__class__.__name__)
- self.bot.loop.create_task(self.sync_settings())
+ self.bot.loop.create_task(self._sync_settings())
@property
def mod_log(self) -> ModLog:
"""Get currently loaded ModLog cog instance."""
return self.bot.get_cog("ModLog")
- async def sync_settings(self) -> None:
+ @defcon_settings.atomic_transaction
+ async def _sync_settings(self) -> None:
"""On cog load, try to synchronize DEFCON settings to the API."""
+ log.trace("Waiting for the guild to become available before syncing.")
await self.bot.wait_until_guild_available()
self.channel = await self.bot.fetch_channel(Channels.defcon)
- try:
- response = await self.bot.api_client.get('bot/bot-settings/defcon')
- data = response['data']
+ log.trace("Syncing settings.")
- except Exception: # Yikes!
+ try:
+ settings = await self.defcon_settings.to_dict()
+ self.threshold = parse_duration_string(settings["threshold"]) if settings.get("threshold") else None
+ self.expiry = datetime.fromisoformat(settings["expiry"]) if settings.get("expiry") else None
+ except RedisError:
log.exception("Unable to get DEFCON settings!")
- await self.bot.get_channel(Channels.dev_log).send(
- f"<@&{Roles.admins}> **WARNING**: Unable to get DEFCON settings!"
+ await self.channel.send(
+ f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!"
+ f"\n\n```{traceback.format_exc()}```"
)
else:
- if data["enabled"]:
- self.enabled = True
- self.days = timedelta(days=data["days"])
- log.info(f"DEFCON enabled: {self.days.days} days")
+ if self.expiry:
+ self.scheduler.schedule_at(self.expiry, 0, self._remove_threshold())
- else:
- self.enabled = False
- self.days = timedelta(days=0)
- log.info("DEFCON disabled")
+ self._update_notifier()
+ log.info(f"DEFCON synchronized: {humanize_delta(self.threshold) if self.threshold else '-'}")
- await self.update_channel_topic()
+ self._update_channel_topic()
@Cog.listener()
async def on_member_join(self, member: Member) -> None:
- """If DEFCON is enabled, check newly joining users to see if they meet the account age threshold."""
- if self.enabled and self.days.days > 0:
+ """Check newly joining users to see if they meet the account age threshold."""
+ if self.threshold:
now = datetime.utcnow()
- if now - member.created_at < self.days:
- log.info(f"Rejecting user {member}: Account is too new and DEFCON is enabled")
+ if now - member.created_at < relativedelta_to_timedelta(self.threshold):
+ log.info(f"Rejecting user {member}: Account is too new")
message_sent = False
@@ -124,134 +142,163 @@ class Defcon(Cog):
"""Check the DEFCON status or run a subcommand."""
await ctx.send_help(ctx.command)
- async def _defcon_action(self, ctx: Context, days: int, action: Action) -> None:
- """Providing a structured way to do an defcon action."""
- try:
- response = await self.bot.api_client.get('bot/bot-settings/defcon')
- data = response['data']
-
- if "enable_date" in data and action is Action.DISABLED:
- enabled = datetime.fromisoformat(data["enable_date"])
-
- delta = datetime.now() - enabled
-
- self.bot.stats.timing("defcon.enabled", delta)
- except Exception:
- pass
-
- error = None
- try:
- await self.bot.api_client.put(
- 'bot/bot-settings/defcon',
- json={
- 'name': 'defcon',
- 'data': {
- # TODO: retrieve old days count
- 'days': days,
- 'enabled': action is not Action.DISABLED,
- 'enable_date': datetime.now().isoformat()
- }
- }
- )
- except Exception as err:
- log.exception("Unable to update DEFCON settings.")
- error = err
- finally:
- await ctx.send(self.build_defcon_msg(action, error))
- await self.send_defcon_log(action, ctx.author, error)
-
- self.bot.stats.gauge("defcon.threshold", days)
-
- @defcon_group.command(name='enable', aliases=('on', 'e'), root_aliases=("defon",))
- @has_any_role(*MODERATION_ROLES)
- async def enable_command(self, ctx: Context) -> None:
- """
- Enable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!
-
- Currently, this just adds an account age requirement. Use !defcon days <int> to set how old an account must be,
- in days.
- """
- self.enabled = True
- await self._defcon_action(ctx, days=0, action=Action.ENABLED)
- await self.update_channel_topic()
-
- @defcon_group.command(name='disable', aliases=('off', 'd'), root_aliases=("defoff",))
- @has_any_role(*MODERATION_ROLES)
- async def disable_command(self, ctx: Context) -> None:
- """Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!"""
- self.enabled = False
- await self._defcon_action(ctx, days=0, action=Action.DISABLED)
- await self.update_channel_topic()
-
- @defcon_group.command(name='status', aliases=('s',))
+ @defcon_group.command(aliases=('s',))
@has_any_role(*MODERATION_ROLES)
- async def status_command(self, ctx: Context) -> None:
+ async def status(self, ctx: Context) -> None:
"""Check the current status of DEFCON mode."""
embed = Embed(
colour=Colour.blurple(), title="DEFCON Status",
- description=f"**Enabled:** {self.enabled}\n"
- f"**Days:** {self.days.days}"
+ description=f"""
+ **Threshold:** {humanize_delta(self.threshold) if self.threshold else "-"}
+ **Expires in:** {humanize_delta(relativedelta(self.expiry, datetime.utcnow())) if self.expiry else "-"}
+ **Verification level:** {ctx.guild.verification_level.name}
+ """
)
await ctx.send(embed=embed)
- @defcon_group.command(name='days')
+ @defcon_group.command(aliases=('t', 'd'))
@has_any_role(*MODERATION_ROLES)
- async def days_command(self, ctx: Context, days: int) -> None:
- """Set how old an account must be to join the server, in days, with DEFCON mode enabled."""
- self.days = timedelta(days=days)
- self.enabled = True
- await self._defcon_action(ctx, days=days, action=Action.UPDATED)
- await self.update_channel_topic()
-
- async def update_channel_topic(self) -> None:
+ async def threshold(
+ self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None
+ ) -> None:
+ """
+ Set how old an account must be to join the server.
+
+ The threshold is the minimum required account age. Can accept either a duration string or a number of days.
+ Set it to 0 to have no threshold.
+ The expiry allows to automatically remove the threshold after a designated time. If no expiry is specified,
+ the cog will remind to remove the threshold hourly.
+ """
+ if isinstance(threshold, int):
+ threshold = relativedelta(days=threshold)
+ await self._update_threshold(ctx.author, threshold=threshold, expiry=expiry)
+
+ @defcon_group.command()
+ @has_any_role(Roles.admins)
+ async def shutdown(self, ctx: Context) -> None:
+ """Shut down the server by setting send permissions of everyone to False."""
+ role = ctx.guild.default_role
+ permissions = role.permissions
+
+ permissions.update(send_messages=False, add_reactions=False)
+ await role.edit(reason="DEFCON shutdown", permissions=permissions)
+ await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.")
+
+ @defcon_group.command()
+ @has_any_role(Roles.admins)
+ async def unshutdown(self, ctx: Context) -> None:
+ """Open up the server again by setting send permissions of everyone to None."""
+ role = ctx.guild.default_role
+ permissions = role.permissions
+
+ permissions.update(send_messages=True, add_reactions=True)
+ await role.edit(reason="DEFCON unshutdown", permissions=permissions)
+ await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.")
+
+ def _update_channel_topic(self) -> None:
"""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)"
+ new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold) if self.threshold else '-'})"
self.mod_log.ignore(Event.guild_channel_update, Channels.defcon)
- await self.channel.edit(topic=new_topic)
-
- def build_defcon_msg(self, action: Action, e: Exception = None) -> str:
- """Build in-channel response string for DEFCON action."""
- if action is Action.ENABLED:
- msg = f"{Emojis.defcon_enabled} DEFCON enabled.\n\n"
- elif action is Action.DISABLED:
- msg = f"{Emojis.defcon_disabled} DEFCON disabled.\n\n"
- elif action is Action.UPDATED:
- msg = (
- f"{Emojis.defcon_updated} DEFCON days updated; accounts must be {self.days.days} "
- f"day{'s' if self.days.days > 1 else ''} old to join the server.\n\n"
+ asyncio.create_task(self.channel.edit(topic=new_topic))
+
+ @defcon_settings.atomic_transaction
+ async def _update_threshold(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None:
+ """Update the new threshold in the cog, cache, defcon channel, and logs, and additionally schedule expiry."""
+ self.threshold = threshold
+ if threshold == relativedelta(days=0): # If the threshold is 0, we don't need to schedule anything
+ expiry = None
+ self.expiry = expiry
+
+ # Either way, we cancel the old task.
+ self.scheduler.cancel_all()
+ if self.expiry is not None:
+ self.scheduler.schedule_at(expiry, 0, self._remove_threshold())
+
+ self._update_notifier()
+
+ # Make sure to handle the critical part of the update before writing to Redis.
+ error = ""
+ try:
+ await self.defcon_settings.update(
+ {
+ 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "",
+ 'expiry': expiry.isoformat() if expiry else 0
+ }
)
+ except RedisError:
+ error = ", but failed to write to cache"
+
+ action = Action.DURATION_UPDATE
- if e:
- msg += (
- "**There was a problem updating the site** - This setting may be reverted when the bot restarts.\n\n"
- f"```py\n{e}\n```"
+ expiry_message = ""
+ if expiry:
+ expiry_message = f" for the next {humanize_delta(relativedelta(expiry, datetime.utcnow()), max_units=2)}"
+
+ if self.threshold:
+ channel_message = (
+ f"updated; accounts must be {humanize_delta(self.threshold)} "
+ f"old to join the server{expiry_message}"
)
+ else:
+ channel_message = "removed"
+
+ await self.channel.send(
+ f"{action.value.emoji} DEFCON threshold {channel_message}{error}."
+ )
+ await self._send_defcon_log(action, author)
+ self._update_channel_topic()
+
+ self._log_threshold_stat(threshold)
- return msg
+ async def _remove_threshold(self) -> None:
+ """Resets the threshold back to 0."""
+ await self._update_threshold(self.bot.user, relativedelta(days=0))
- async def send_defcon_log(self, action: Action, actor: Member, e: Exception = None) -> None:
+ @staticmethod
+ def _stringify_relativedelta(delta: relativedelta) -> str:
+ """Convert a relativedelta object to a duration string."""
+ units = [("years", "y"), ("months", "m"), ("days", "d"), ("hours", "h"), ("minutes", "m"), ("seconds", "s")]
+ return "".join(f"{getattr(delta, unit)}{symbol}" for unit, symbol in units if getattr(delta, unit)) or "0s"
+
+ def _log_threshold_stat(self, threshold: relativedelta) -> None:
+ """Adds the threshold to the bot stats in days."""
+ threshold_days = relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY
+ self.bot.stats.gauge("defcon.threshold", threshold_days)
+
+ async def _send_defcon_log(self, action: Action, actor: User) -> None:
"""Send log message for DEFCON action."""
info = action.value
log_msg: str = (
f"**Staffer:** {actor.mention} {actor} (`{actor.id}`)\n"
- f"{info.template.format(days=self.days.days)}"
+ f"{info.template.format(threshold=(humanize_delta(self.threshold) if self.threshold else '-'))}"
)
status_msg = f"DEFCON {action.name.lower()}"
- if e:
- log_msg += (
- "**There was a problem updating the site** - This setting may be reverted when the bot restarts.\n\n"
- f"```py\n{e}\n```"
- )
-
await self.mod_log.send_log_message(info.icon, info.color, status_msg, log_msg)
+ def _update_notifier(self) -> None:
+ """Start or stop the notifier according to the DEFCON status."""
+ if self.threshold and self.expiry is None and not self.defcon_notifier.is_running():
+ log.info("DEFCON notifier started.")
+ self.defcon_notifier.start()
+
+ elif (not self.threshold or self.expiry is not None) and self.defcon_notifier.is_running():
+ log.info("DEFCON notifier stopped.")
+ self.defcon_notifier.cancel()
+
+ @tasks.loop(hours=1)
+ async def defcon_notifier(self) -> None:
+ """Routinely notify moderators that DEFCON is active."""
+ await self.channel.send(f"Defcon is on and is set to {humanize_delta(self.threshold)}.")
+
+ def cog_unload(self) -> None:
+ """Cancel the notifer and threshold removal tasks when the cog unloads."""
+ log.trace("Cog unload: canceling defcon notifier task.")
+ self.defcon_notifier.cancel()
+ self.scheduler.cancel_all()
+
def setup(bot: Bot) -> None:
"""Load the Defcon cog."""
diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py
index c449752e1..d8baff76a 100644
--- a/bot/exts/moderation/slowmode.py
+++ b/bot/exts/moderation/slowmode.py
@@ -1,5 +1,4 @@
import logging
-from datetime import datetime
from typing import Optional
from dateutil.relativedelta import relativedelta
@@ -54,8 +53,7 @@ class Slowmode(Cog):
# Convert `dateutil.relativedelta.relativedelta` to `datetime.timedelta`
# Must do this to get the delta in a particular unit of time
- utcnow = datetime.utcnow()
- slowmode_delay = (utcnow + delay - utcnow).total_seconds()
+ slowmode_delay = time.relativedelta_to_timedelta(delay).total_seconds()
humanized_delay = time.humanize_delta(delay)
diff --git a/bot/log.py b/bot/log.py
index e92233a33..bc3bba0af 100644
--- a/bot/log.py
+++ b/bot/log.py
@@ -1,11 +1,12 @@
import logging
import os
import sys
-from logging import Logger, handlers
+from logging import Logger, StreamHandler, handlers
from pathlib import Path
import coloredlogs
import sentry_sdk
+from pythonjsonlogger import jsonlogger
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration
@@ -13,6 +14,15 @@ from bot import constants
TRACE_LEVEL = 5
+PROD_FIELDS = [
+ "asctime",
+ "name",
+ "levelname",
+ "message",
+ "funcName",
+ "filename"
+]
+
def setup() -> None:
"""Set up loggers."""
@@ -33,21 +43,28 @@ def setup() -> None:
root_log.setLevel(log_level)
root_log.addHandler(file_handler)
- if "COLOREDLOGS_LEVEL_STYLES" not in os.environ:
- coloredlogs.DEFAULT_LEVEL_STYLES = {
- **coloredlogs.DEFAULT_LEVEL_STYLES,
- "trace": {"color": 246},
- "critical": {"background": "red"},
- "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"]
- }
-
- if "COLOREDLOGS_LOG_FORMAT" not in os.environ:
- coloredlogs.DEFAULT_LOG_FORMAT = format_string
-
- if "COLOREDLOGS_LOG_LEVEL" not in os.environ:
- coloredlogs.DEFAULT_LOG_LEVEL = log_level
-
- coloredlogs.install(logger=root_log, stream=sys.stdout)
+ if constants.DEBUG_MODE:
+ if "COLOREDLOGS_LEVEL_STYLES" not in os.environ:
+ coloredlogs.DEFAULT_LEVEL_STYLES = {
+ **coloredlogs.DEFAULT_LEVEL_STYLES,
+ "trace": {"color": 246},
+ "critical": {"background": "red"},
+ "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"]
+ }
+
+ if "COLOREDLOGS_LOG_FORMAT" not in os.environ:
+ coloredlogs.DEFAULT_LOG_FORMAT = format_string
+
+ if "COLOREDLOGS_LOG_LEVEL" not in os.environ:
+ coloredlogs.DEFAULT_LOG_LEVEL = log_level
+
+ coloredlogs.install(logger=root_log, stream=sys.stdout)
+ else:
+ json_format = " ".join([f"%({field})s" for field in PROD_FIELDS])
+ stream_handler = StreamHandler()
+ formatter = jsonlogger.JsonFormatter(json_format)
+ stream_handler.setFormatter(formatter)
+ root_log.addHandler(stream_handler)
logging.getLogger("discord").setLevel(logging.WARNING)
logging.getLogger("websockets").setLevel(logging.WARNING)
diff --git a/bot/utils/time.py b/bot/utils/time.py
index 47e49904b..f862e40f7 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -1,5 +1,6 @@
import asyncio
import datetime
+import re
from typing import Optional
import dateutil.parser
@@ -8,6 +9,16 @@ from dateutil.relativedelta import relativedelta
RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
INFRACTION_FORMAT = "%Y-%m-%d %H:%M"
+_DURATION_REGEX = re.compile(
+ r"((?P<years>\d+?) ?(years|year|Y|y) ?)?"
+ r"((?P<months>\d+?) ?(months|month|m) ?)?"
+ r"((?P<weeks>\d+?) ?(weeks|week|W|w) ?)?"
+ r"((?P<days>\d+?) ?(days|day|D|d) ?)?"
+ r"((?P<hours>\d+?) ?(hours|hour|H|h) ?)?"
+ r"((?P<minutes>\d+?) ?(minutes|minute|M) ?)?"
+ r"((?P<seconds>\d+?) ?(seconds|second|S|s))?"
+)
+
def _stringify_time_unit(value: int, unit: str) -> str:
"""
@@ -74,6 +85,37 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
return humanized
+def parse_duration_string(duration: str) -> Optional[relativedelta]:
+ """
+ Converts a `duration` string to a relativedelta object.
+
+ The function supports the following symbols for each unit of time:
+ - years: `Y`, `y`, `year`, `years`
+ - months: `m`, `month`, `months`
+ - weeks: `w`, `W`, `week`, `weeks`
+ - days: `d`, `D`, `day`, `days`
+ - hours: `H`, `h`, `hour`, `hours`
+ - minutes: `M`, `minute`, `minutes`
+ - seconds: `S`, `s`, `second`, `seconds`
+ The units need to be provided in descending order of magnitude.
+ If the string does represent a durationdelta object, it will return None.
+ """
+ match = _DURATION_REGEX.fullmatch(duration)
+ if not match:
+ return None
+
+ duration_dict = {unit: int(amount) for unit, amount in match.groupdict(default=0).items()}
+ delta = relativedelta(**duration_dict)
+
+ return delta
+
+
+def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta:
+ """Converts a relativedelta object to a timedelta object."""
+ utcnow = datetime.datetime.utcnow()
+ return utcnow + delta - utcnow
+
+
def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max_units: int = 6) -> str:
"""
Takes a datetime and returns a human-readable string that describes how long ago that datetime was.
diff --git a/config-default.yml b/config-default.yml
index a9fb2262e..3dbc7bd6b 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -47,9 +47,9 @@ style:
badge_staff: "<:discord_staff:743882896498098226>"
badge_verified_bot_developer: "<:verified_bot_dev:743882897299210310>"
- defcon_disabled: "<:defcondisabled:470326273952972810>"
- defcon_enabled: "<:defconenabled:470326274213150730>"
- defcon_updated: "<:defconsettingsupdated:470326274082996224>"
+ defcon_shutdown: "<:defcondisabled:470326273952972810>"
+ defcon_unshutdown: "<:defconenabled:470326274213150730>"
+ defcon_update: "<:defconsettingsupdated:470326274082996224>"
failmail: "<:failmail:633660039931887616>"
@@ -83,9 +83,9 @@ style:
crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png"
defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png"
- defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png"
- defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png"
- defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png"
+ defcon_shutdown: "https://cdn.discordapp.com/emojis/470326273952972810.png"
+ defcon_unshutdown: "https://cdn.discordapp.com/emojis/470326274213150730.png"
+ defcon_update: "https://cdn.discordapp.com/emojis/472472638342561793.png"
filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png"
@@ -195,6 +195,7 @@ guild:
incidents_archive: 720668923636351037
mods: &MODS 305126844661760000
mod_alerts: 473092532147060736
+ mod_appeals: &MOD_APPEALS 808790025688711198
mod_meta: &MOD_META 775412552795947058
mod_spam: &MOD_SPAM 620607373828030464
mod_tools: &MOD_TOOLS 775413915391098921
@@ -230,6 +231,7 @@ guild:
moderation_channels:
- *ADMINS
- *ADMIN_SPAM
+ - *MOD_APPEALS
- *MOD_META
- *MOD_TOOLS
- *MODS
@@ -261,6 +263,7 @@ guild:
# Staff
admins: &ADMINS_ROLE 267628507062992896
core_developers: 587606783669829632
+ devops: 409416496733880320
helpers: &HELPERS_ROLE 267630620367257601
moderators: &MODS_ROLE 267629731250176001
owners: &OWNERS_ROLE 267627879762755584