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 |