diff options
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | Pipfile.lock | 260 | ||||
| -rw-r--r-- | bot/constants.py | 13 | ||||
| -rw-r--r-- | bot/converters.py | 17 | ||||
| -rw-r--r-- | bot/exts/backend/error_handler.py | 2 | ||||
| -rw-r--r-- | bot/exts/filters/filtering.py | 66 | ||||
| -rw-r--r-- | bot/exts/info/information.py | 13 | ||||
| -rw-r--r-- | bot/exts/info/pypi.py | 9 | ||||
| -rw-r--r-- | bot/exts/info/tags.py | 7 | ||||
| -rw-r--r-- | bot/exts/moderation/defcon.py | 315 | ||||
| -rw-r--r-- | bot/exts/moderation/slowmode.py | 4 | ||||
| -rw-r--r-- | bot/log.py | 49 | ||||
| -rw-r--r-- | bot/utils/time.py | 42 | ||||
| -rw-r--r-- | config-default.yml | 15 | 
14 files changed, 510 insertions, 303 deletions
| @@ -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 | 
