diff options
Diffstat (limited to '')
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | Pipfile.lock | 389 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | bot/__init__.py | 1 | ||||
| -rw-r--r-- | bot/__main__.py | 1 | ||||
| -rw-r--r-- | bot/bot.py | 3 | ||||
| -rw-r--r-- | bot/cogs/alias.py | 21 | ||||
| -rw-r--r-- | bot/cogs/doc.py | 57 | ||||
| -rw-r--r-- | bot/cogs/information.py | 6 | ||||
| -rw-r--r-- | bot/cogs/metrics.py | 98 | ||||
| -rw-r--r-- | bot/cogs/moderation/infractions.py | 33 | ||||
| -rw-r--r-- | bot/cogs/moderation/management.py | 8 | ||||
| -rw-r--r-- | bot/cogs/moderation/modlog.py | 96 | ||||
| -rw-r--r-- | bot/cogs/moderation/scheduler.py | 26 | ||||
| -rw-r--r-- | bot/cogs/moderation/superstarify.py | 5 | ||||
| -rw-r--r-- | bot/cogs/moderation/utils.py | 89 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 14 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/bigbrother.py | 13 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/talentpool.py | 14 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/watchchannel.py | 20 | ||||
| -rw-r--r-- | bot/constants.py | 7 | ||||
| -rw-r--r-- | bot/converters.py | 74 | ||||
| -rw-r--r-- | config-default.yml | 20 | 
23 files changed, 648 insertions, 350 deletions
@@ -19,6 +19,7 @@ deepdiff = "~=4.0"  requests = "~=2.22"  more_itertools = "~=7.2"  urllib3 = ">=1.24.2,<1.25" +prometheus-async = {extras = ["aiohttp"],version = "~=19.2"}  [dev-packages]  coverage = "~=4.5" diff --git a/Pipfile.lock b/Pipfile.lock index 69caf4646..ab5dfb538 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "c27d699b4aeeed204dee41f924f682ae2a670add8549a8826e58776594370582" +            "sha256": "d9349e8c704b2b2403004039856d8d75aaebc76e4aa93390c4d177f583e73b71"          },          "pipfile-spec": 6,          "requires": { @@ -18,11 +18,11 @@      "default": {          "aio-pika": {              "hashes": [ -                "sha256:1da038b3d2c1b49e0e816d87424e702912bb77f9b5197f2bf279217915b4f7ed", -                "sha256:29fe851374b86c997a22174c04352b5941bc1c2e36bbf542918ac18a76cfc9d3" +                "sha256:a5837277e53755078db3a9e8c45bbca605c8ba9ecba7a02d74a7a1779f444723", +                "sha256:fa32e33b4b7d0804dcf439ae6ff24d2f0a83d1ba280ee9f555e647d71d394ff5"              ],              "index": "pypi", -            "version": "==6.3.0" +            "version": "==6.4.1"          },          "aiodns": {              "hashes": [ @@ -62,10 +62,10 @@          },          "aiormq": {              "hashes": [ -                "sha256:afc0d46837b121585e4faec0a7646706429b4e2f5110ae8d0b5cdc3708b4b0e5", -                "sha256:dc0fbbc7f8ad5af6a2cc18e00ccc5f925984cde3db6e8fe952c07b7ef157b5f2" +                "sha256:8c215a970133ab5ee7c478decac55b209af7731050f52d11439fe910fa0f9e9d", +                "sha256:9210f3389200aee7d8067f6435f4a9eff2d3a30b88beb5eaae406ccc11c0fc01"              ], -            "version": "==2.9.1" +            "version": "==3.2.0"          },          "alabaster": {              "hashes": [ @@ -90,25 +90,25 @@          },          "babel": {              "hashes": [ -                "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", -                "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" +                "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", +                "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"              ], -            "version": "==2.7.0" +            "version": "==2.8.0"          },          "beautifulsoup4": {              "hashes": [ -                "sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169", -                "sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931", -                "sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57" +                "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a", +                "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887", +                "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"              ], -            "version": "==4.8.1" +            "version": "==4.8.2"          },          "certifi": {              "hashes": [ -                "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", -                "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" +                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", +                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"              ], -            "version": "==2019.9.11" +            "version": "==2019.11.28"          },          "cffi": {              "hashes": [ @@ -195,10 +195,10 @@          },          "imagesize": {              "hashes": [ -                "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", -                "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" +                "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", +                "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"              ], -            "version": "==1.1.0" +            "version": "==1.2.0"          },          "jinja2": {              "hashes": [ @@ -223,35 +223,35 @@          },          "lxml": {              "hashes": [ -                "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", -                "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", -                "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", -                "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", -                "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", -                "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", -                "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", -                "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", -                "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", -                "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", -                "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", -                "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", -                "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", -                "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", -                "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", -                "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", -                "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", -                "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", -                "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", -                "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", -                "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", -                "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", -                "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", -                "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", -                "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", -                "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" -            ], -            "index": "pypi", -            "version": "==4.4.1" +                "sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2", +                "sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c", +                "sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487", +                "sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70", +                "sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d", +                "sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250", +                "sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d", +                "sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74", +                "sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d", +                "sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78", +                "sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145", +                "sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d", +                "sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da", +                "sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e", +                "sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd", +                "sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85", +                "sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7", +                "sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9", +                "sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85", +                "sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db", +                "sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336", +                "sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8", +                "sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18", +                "sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9", +                "sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06", +                "sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1" +            ], +            "index": "pypi", +            "version": "==4.4.2"          },          "markdownify": {              "hashes": [ @@ -303,37 +303,25 @@          },          "multidict": {              "hashes": [ -                "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", -                "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", -                "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", -                "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", -                "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", -                "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", -                "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", -                "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", -                "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", -                "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", -                "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", -                "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", -                "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", -                "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", -                "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", -                "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", -                "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", -                "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", -                "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", -                "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", -                "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", -                "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", -                "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", -                "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", -                "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", -                "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", -                "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", -                "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", -                "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" -            ], -            "version": "==4.5.2" +                "sha256:13f3ebdb5693944f52faa7b2065b751cb7e578b8dd0a5bb8e4ab05ad0188b85e", +                "sha256:26502cefa86d79b86752e96639352c7247846515c864d7c2eb85d036752b643c", +                "sha256:4fba5204d32d5c52439f88437d33ad14b5f228e25072a192453f658bddfe45a7", +                "sha256:527124ef435f39a37b279653ad0238ff606b58328ca7989a6df372fd75d7fe26", +                "sha256:5414f388ffd78c57e77bd253cf829373721f450613de53dc85a08e34d806e8eb", +                "sha256:5eee66f882ab35674944dfa0d28b57fa51e160b4dce0ce19e47f495fdae70703", +                "sha256:63810343ea07f5cd86ba66ab66706243a6f5af075eea50c01e39b4ad6bc3c57a", +                "sha256:6bd10adf9f0d6a98ccc792ab6f83d18674775986ba9bacd376b643fe35633357", +                "sha256:83c6ddf0add57c6b8a7de0bc7e2d656be3eefeff7c922af9a9aae7e49f225625", +                "sha256:93166e0f5379cf6cd29746989f8a594fa7204dcae2e9335ddba39c870a287e1c", +                "sha256:9a7b115ee0b9b92d10ebc246811d8f55d0c57e82dbb6a26b23c9a9a6ad40ce0c", +                "sha256:a38baa3046cce174a07a59952c9f876ae8875ef3559709639c17fdf21f7b30dd", +                "sha256:a6d219f49821f4b2c85c6d426346a5d84dab6daa6f85ca3da6c00ed05b54022d", +                "sha256:a8ed33e8f9b67e3b592c56567135bb42e7e0e97417a4b6a771e60898dfd5182b", +                "sha256:d7d428488c67b09b26928950a395e41cc72bb9c3d5abfe9f0521940ee4f796d4", +                "sha256:dcfed56aa085b89d644af17442cdc2debaa73388feba4b8026446d168ca8dad7", +                "sha256:f29b885e4903bd57a7789f09fe9d60b6475a6c1a4c0eca874d8558f00f9d4b51" +            ], +            "version": "==4.7.4"          },          "ordered-set": {              "hashes": [ @@ -343,10 +331,10 @@          },          "packaging": {              "hashes": [ -                "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", -                "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" +                "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", +                "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"              ], -            "version": "==19.2" +            "version": "==20.0"          },          "pamqp": {              "hashes": [ @@ -355,23 +343,56 @@              ],              "version": "==2.3.0"          }, +        "prometheus-async": { +            "extras": [ +                "aiohttp" +            ], +            "hashes": [ +                "sha256:227f516e5bf98a0dc602348381e182358f8b2ed24a8db05e8e34d9cf027bab83", +                "sha256:3cc68d1f39e9bbf16dbd0b51103d87671b3cbd1d75a72cda472cd9a35cc9d0d2" +            ], +            "index": "pypi", +            "version": "==19.2.0" +        }, +        "prometheus-client": { +            "hashes": [ +                "sha256:71cd24a2b3eb335cb800c7159f423df1bd4dcd5171b234be15e3f31ec9f622da" +            ], +            "version": "==0.7.1" +        },          "pycares": {              "hashes": [ -                "sha256:2ca080db265ea238dc45f997f94effb62b979a617569889e265c26a839ed6305", -                "sha256:6f79c6afb6ce603009db2042fddc2e348ad093ece9784cbe2daa809499871a23", -                "sha256:70918d06eb0603016d37092a5f2c0228509eb4e6c5a3faacb4184f6ab7be7650", -                "sha256:755187d28d24a9ea63aa2b4c0638be31d65fbf7f0ce16d41261b9f8cb55a1b99", -                "sha256:7baa4b1f2146eb8423ff8303ebde3a20fb444a60db761fba0430d104fe35ddbf", -                "sha256:90b27d4df86395f465a171386bc341098d6d47b65944df46518814ae298f6cc6", -                "sha256:9e090dd6b2afa65cb51c133883b2bf2240fd0f717b130b0048714b33fb0f47ce", -                "sha256:a11b7d63c3718775f6e805d6464cb10943780395ab042c7e5a0a7a9f612735dd", -                "sha256:b253f5dcaa0ac7076b79388a3ac80dd8f3bd979108f813baade40d3a9b8bf0bd", -                "sha256:c7f4f65e44ba35e35ad3febc844270665bba21cfb0fb7d749434e705b556e087", -                "sha256:cdb342e6a254f035bd976d95807a2184038fc088d957a5104dcaab8be602c093", -                "sha256:cf08e164f8bfb83b9fe633feb56f2754fae6baefcea663593794fa0518f8f98c", -                "sha256:df9bc694cf03673878ea8ce674082c5acd134991d64d6c306d4bd61c0c1df98f" -            ], -            "version": "==3.0.0" +                "sha256:050f00b39ed77ea8a4e555f09417d4b1a6b5baa24bb9531a3e15d003d2319b3f", +                "sha256:0a24d2e580a8eb567140d7b69f12cb7de90c836bd7b6488ec69394d308605ac3", +                "sha256:0c5bd1f6f885a219d5e972788d6eef7b8043b55c3375a845e5399638436e0bba", +                "sha256:11c628402cc8fc8ef461076d4e47f88afc1f8609989ebbff0dbffcd54c97239f", +                "sha256:18dfd4fd300f570d6c4536c1d987b7b7673b2a9d14346592c5d6ed716df0d104", +                "sha256:1917b82494907a4a342db420bc4dd5bac355a5fa3984c35ba9bf51422b020b48", +                "sha256:1b90fa00a89564df059fb18e796458864cc4e00cb55e364dbf921997266b7c55", +                "sha256:1d8d177c40567de78108a7835170f570ab04f09084bfd32df9919c0eaec47aa1", +                "sha256:236286f81664658b32c141c8e79d20afc3d54f6e2e49dfc8b702026be7265855", +                "sha256:2e4f74677542737fb5af4ea9a2e415ec5ab31aa67e7b8c3c969fdb15c069f679", +                "sha256:48a7750f04e69e1f304f4332b755728067e7c4b1abe2760bba1cacd9ff7a847a", +                "sha256:7d86e62b700b21401ffe7fd1bbfe91e08489416fecae99c6570ab023c6896022", +                "sha256:7e2d7effd08d2e5a3cb95d98a7286ebab71ab2fbce84fa93cc2dd56caf7240dd", +                "sha256:81edb016d9e43dde7473bc3999c29cdfee3a6b67308fed1ea21049f458e83ae0", +                "sha256:96c90e11b4a4c7c0b8ff5aaaae969c5035493136586043ff301979aae0623941", +                "sha256:9a0a1845f8cb2e62332bca0aaa9ad5494603ac43fb60d510a61d5b5b170d7216", +                "sha256:a05bbfdfd41f8410a905a818f329afe7510cbd9ee65c60f8860a72b6c64ce5dc", +                "sha256:a5089fd660f0b0d228b14cdaa110d0d311edfa5a63f800618dbf1321dcaef66b", +                "sha256:c457a709e6f2befea7e2996c991eda6d79705dd075f6521593ba6ebc1485b811", +                "sha256:c5cb72644b04e5e5abfb1e10a0e7eb75da6684ea0e60871652f348e412cf3b11", +                "sha256:cce46dd4717debfd2aab79d6d7f0cbdf6b1e982dc4d9bebad81658d59ede07c2", +                "sha256:cfdd1f90bcf373b00f4b2c55ea47868616fe2f779f792fc913fa82a3d64ffe43", +                "sha256:d88a279cbc5af613f73e86e19b3f63850f7a2e2736e249c51995dedcc830b1bb", +                "sha256:eba9a9227438da5e78fc8eee32f32eb35d9a50cf0a0bd937eb6275c7cc3015fe", +                "sha256:eee7b6a5f5b5af050cb7d66ab28179287b416f06d15a8974ac831437fec51336", +                "sha256:f41ac1c858687e53242828c9f59c2e7b0b95dbcd5bdd09c7e5d3c48b0f89a25a", +                "sha256:f8deaefefc3a589058df1b177275f79233e8b0eeee6734cf4336d80164ecd022", +                "sha256:fa78e919f3bd7d6d075db262aa41079b4c02da315c6043c6f43881e2ebcdd623", +                "sha256:fadb97d2e02dabdc15a0091591a972a938850d79ddde23d385d813c1731983f0" +            ], +            "version": "==3.1.1"          },          "pycparser": {              "hashes": [ @@ -381,17 +402,17 @@          },          "pygments": {              "hashes": [ -                "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", -                "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" +                "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", +                "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"              ], -            "version": "==2.4.2" +            "version": "==2.5.2"          },          "pyparsing": {              "hashes": [ -                "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", -                "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" +                "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", +                "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"              ], -            "version": "==2.4.5" +            "version": "==2.4.6"          },          "python-dateutil": {              "hashes": [ @@ -416,22 +437,20 @@          },          "pyyaml": {              "hashes": [ -                "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", -                "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", -                "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", -                "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", -                "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", -                "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", -                "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", -                "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", -                "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", -                "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", -                "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", -                "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", -                "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" +                "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", +                "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", +                "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", +                "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", +                "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", +                "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", +                "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", +                "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", +                "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", +                "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", +                "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"              ],              "index": "pypi", -            "version": "==5.1.2" +            "version": "==5.3"          },          "requests": {              "hashes": [ @@ -464,11 +483,11 @@          },          "sphinx": {              "hashes": [ -                "sha256:31088dfb95359384b1005619827eaee3056243798c62724fd3fa4b84ee4d71bd", -                "sha256:52286a0b9d7caa31efee301ec4300dbdab23c3b05da1c9024b4e84896fb73d79" +                "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", +                "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"              ],              "index": "pypi", -            "version": "==2.2.1" +            "version": "==2.3.1"          },          "sphinxcontrib-applehelp": {              "hashes": [ @@ -546,21 +565,33 @@              ],              "version": "==6.0"          }, -        "yarl": { +        "wrapt": {              "hashes": [ -                "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", -                "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", -                "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", -                "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", -                "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", -                "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", -                "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", -                "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", -                "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", -                "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", -                "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" +                "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"              ], -            "version": "==1.3.0" +            "version": "==1.11.2" +        }, +        "yarl": { +            "hashes": [ +                "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", +                "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", +                "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", +                "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", +                "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", +                "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", +                "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", +                "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", +                "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", +                "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", +                "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", +                "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", +                "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", +                "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", +                "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", +                "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", +                "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" +            ], +            "version": "==1.4.2"          }      },      "develop": { @@ -580,10 +611,10 @@          },          "certifi": {              "hashes": [ -                "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", -                "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" +                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", +                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"              ], -            "version": "==2019.9.11" +            "version": "==2019.11.28"          },          "cfgv": {              "hashes": [ @@ -646,10 +677,11 @@          },          "dodgy": {              "hashes": [ -                "sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c" +                "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a", +                "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"              ],              "index": "pypi", -            "version": "==0.1.9" +            "version": "==0.2.1"          },          "dparse": {              "hashes": [ @@ -675,11 +707,11 @@          },          "flake8-annotations": {              "hashes": [ -                "sha256:6ac7ca1e706307686b60af8043ff1db31dc2cfc1233c8210d67a3d9b8f364736", -                "sha256:b51131007000d67217608fa028a35ff80aa400b474e5972f1f99c2cf9d26bd2e" +                "sha256:05b85538014c850a86dce7374bb6621c64481c24e35e8e90af1315f4d7a3dbaa", +                "sha256:43e5233a76fda002b91a54a7cc4510f099c4bfd6279502ec70164016250eebd1"              ],              "index": "pypi", -            "version": "==1.1.0" +            "version": "==1.1.3"          },          "flake8-bugbear": {              "hashes": [ @@ -730,10 +762,10 @@          },          "identify": {              "hashes": [ -                "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", -                "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" +                "sha256:6f44e637caa40d1b4cb37f6ed3b262ede74901d28b1cc5b1fc07360871edd65d", +                "sha256:72e9c4ed3bc713c7045b762b0d2e2115c572b85abfc1f4604f5a4fd4c6642b71"              ], -            "version": "==1.4.7" +            "version": "==1.4.9"          },          "idna": {              "hashes": [ @@ -744,11 +776,11 @@          },          "importlib-metadata": {              "hashes": [ -                "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", -                "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" +                "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", +                "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"              ],              "markers": "python_version < '3.8'", -            "version": "==0.23" +            "version": "==1.4.0"          },          "mccabe": {              "hashes": [ @@ -767,24 +799,24 @@          },          "nodeenv": {              "hashes": [ -                "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" +                "sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"              ], -            "version": "==1.3.3" +            "version": "==1.3.4"          },          "packaging": {              "hashes": [ -                "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", -                "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" +                "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", +                "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"              ], -            "version": "==19.2" +            "version": "==20.0"          },          "pre-commit": {              "hashes": [ -                "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", -                "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" +                "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850", +                "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"              ],              "index": "pypi", -            "version": "==1.20.0" +            "version": "==1.21.0"          },          "pycodestyle": {              "hashes": [ @@ -795,10 +827,10 @@          },          "pydocstyle": {              "hashes": [ -                "sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058", -                "sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59" +                "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", +                "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"              ], -            "version": "==4.0.1" +            "version": "==5.0.2"          },          "pyflakes": {              "hashes": [ @@ -809,29 +841,27 @@          },          "pyparsing": {              "hashes": [ -                "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", -                "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" +                "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", +                "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"              ], -            "version": "==2.4.5" +            "version": "==2.4.6"          },          "pyyaml": {              "hashes": [ -                "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", -                "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", -                "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", -                "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", -                "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", -                "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", -                "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", -                "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", -                "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", -                "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", -                "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", -                "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", -                "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" +                "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", +                "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", +                "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", +                "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", +                "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", +                "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", +                "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", +                "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", +                "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", +                "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", +                "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"              ],              "index": "pypi", -            "version": "==5.1.2" +            "version": "==5.3"          },          "requests": {              "hashes": [ @@ -893,6 +923,7 @@                  "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",                  "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"              ], +            "markers": "python_version < '3.8'",              "version": "==1.4.0"          },          "unittest-xml-reporting": { @@ -913,10 +944,10 @@          },          "virtualenv": {              "hashes": [ -                "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", -                "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" +                "sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3", +                "sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"              ], -            "version": "==16.7.7" +            "version": "==16.7.9"          },          "zipp": {              "hashes": [ @@ -1,6 +1,6 @@  # Python Utility Bot -[](https://discord.gg/2B963hn) +[](https://discord.gg/2B963hn)  [](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=1&branchName=master)  [](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Bot?branchName=master)  [](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Bot?branchName=master) diff --git a/bot/__init__.py b/bot/__init__.py index 4a2df730d..789ace5c0 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -6,6 +6,7 @@ from pathlib import Path  from logmatic import JsonFormatter +  logging.TRACE = 5  logging.addLevelName(logging.TRACE, "TRACE") diff --git a/bot/__main__.py b/bot/__main__.py index 84bc7094b..61271a692 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -40,6 +40,7 @@ bot.load_extension("bot.cogs.duck_pond")  bot.load_extension("bot.cogs.free")  bot.load_extension("bot.cogs.information")  bot.load_extension("bot.cogs.jams") +bot.load_extension("bot.cogs.metrics")  bot.load_extension("bot.cogs.moderation")  bot.load_extension("bot.cogs.off_topic_names")  bot.load_extension("bot.cogs.reddit") diff --git a/bot/bot.py b/bot/bot.py index 8f808272f..930aaf70e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -4,6 +4,7 @@ from typing import Optional  import aiohttp  from discord.ext import commands +from prometheus_async.aio.web import start_http_server as start_prometheus_http_server  from bot import api @@ -50,4 +51,6 @@ class Bot(commands.Bot):          """Open an aiohttp session before logging in and connecting to Discord."""          self.http_session = aiohttp.ClientSession(connector=self.connector) +        await start_prometheus_http_server(addr="0.0.0.0", port=9330) +        log.debug("Started Prometheus server on port 9330.")          await super().start(*args, **kwargs) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index c1db38462..0b800575f 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -1,14 +1,15 @@  import inspect  import logging -from typing import Union -from discord import Colour, Embed, Member, User -from discord.ext.commands import Cog, Command, Context, clean_content, command, group +from discord import Colour, Embed +from discord.ext.commands import ( +    Cog, Command, Context, Greedy, +    clean_content, command, group, +)  from bot.bot import Bot  from bot.cogs.extensions import Extension -from bot.cogs.watchchannels.watchchannel import proxy_user -from bot.converters import TagNameConverter +from bot.converters import FetchedMember, TagNameConverter  from bot.pagination import LinePaginator  log = logging.getLogger(__name__) @@ -61,12 +62,12 @@ class Alias (Cog):          await self.invoke(ctx, "site tools")      @command(name="watch", hidden=True) -    async def bigbrother_watch_alias(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: +    async def bigbrother_watch_alias(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """Alias for invoking <prefix>bigbrother watch [user] [reason]."""          await self.invoke(ctx, "bigbrother watch", user, reason=reason)      @command(name="unwatch", hidden=True) -    async def bigbrother_unwatch_alias(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: +    async def bigbrother_unwatch_alias(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """Alias for invoking <prefix>bigbrother unwatch [user] [reason]."""          await self.invoke(ctx, "bigbrother unwatch", user, reason=reason) @@ -81,7 +82,7 @@ class Alias (Cog):          await self.invoke(ctx, "site faq")      @command(name="rules", aliases=("rule",), hidden=True) -    async def site_rules_alias(self, ctx: Context, *rules: int) -> None: +    async def site_rules_alias(self, ctx: Context, rules: Greedy[int], *_: str) -> None:          """Alias for invoking <prefix>site rules."""          await self.invoke(ctx, "site rules", *rules) @@ -132,12 +133,12 @@ class Alias (Cog):          await self.invoke(ctx, "docs get", symbol)      @command(name="nominate", hidden=True) -    async def nomination_add_alias(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: +    async def nomination_add_alias(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """Alias for invoking <prefix>talentpool add [user] [reason]."""          await self.invoke(ctx, "talentpool add", user, reason=reason)      @command(name="unnominate", hidden=True) -    async def nomination_end_alias(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: +    async def nomination_end_alias(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """Alias for invoking <prefix>nomination end [user] [reason]."""          await self.invoke(ctx, "nomination end", user, reason=reason) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 9506b195a..6e7c00b6a 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -5,6 +5,7 @@ import re  import textwrap  from collections import OrderedDict  from contextlib import suppress +from types import SimpleNamespace  from typing import Any, Callable, Optional, Tuple  import discord @@ -27,6 +28,16 @@ from bot.pagination import LinePaginator  log = logging.getLogger(__name__)  logging.getLogger('urllib3').setLevel(logging.WARNING) +# Since Intersphinx is intended to be used with Sphinx, +# we need to mock its configuration. +SPHINX_MOCK_APP = SimpleNamespace( +    config=SimpleNamespace( +        intersphinx_timeout=3, +        tls_verify=True, +        user_agent="python3:python-discord/bot:1.0.0" +    ) +) +  NO_OVERRIDE_GROUPS = (      "2to3fixer",      "token", @@ -102,18 +113,6 @@ def markdownify(html: str) -> DocMarkdownConverter:      return DocMarkdownConverter(bullets='•').convert(html) -class DummyObject(object): -    """A dummy object which supports assigning anything, which the builtin `object()` does not support normally.""" - - -class SphinxConfiguration: -    """Dummy configuration for use with intersphinx.""" - -    config = DummyObject() -    config.intersphinx_timeout = 3 -    config.tls_verify = True - -  class InventoryURL(commands.Converter):      """      Represents an Intersphinx inventory URL. @@ -128,7 +127,7 @@ class InventoryURL(commands.Converter):      async def convert(ctx: commands.Context, url: str) -> str:          """Convert url to Intersphinx inventory URL."""          try: -            intersphinx.fetch_inventory(SphinxConfiguration(), '', url) +            intersphinx.fetch_inventory(SPHINX_MOCK_APP, '', url)          except AttributeError:              raise commands.BadArgument(f"Failed to fetch Intersphinx inventory from URL `{url}`.")          except ConnectionError: @@ -162,7 +161,7 @@ class Doc(commands.Cog):          await self.refresh_inventory()      async def update_single( -        self, package_name: str, base_url: str, inventory_url: str, config: SphinxConfiguration +        self, package_name: str, base_url: str, inventory_url: str      ) -> None:          """          Rebuild the inventory for a single package. @@ -173,12 +172,10 @@ class Doc(commands.Cog):                  absolute paths that link to specific symbols              * `inventory_url` is the absolute URL to the intersphinx inventory, fetched by running                  `intersphinx.fetch_inventory` in an executor on the bot's event loop -            * `config` is a `SphinxConfiguration` instance to mock the regular sphinx -                project layout, required for use with intersphinx          """          self.base_urls[package_name] = base_url -        package = await self._fetch_inventory(inventory_url, config) +        package = await self._fetch_inventory(inventory_url)          if not package:              return None @@ -220,15 +217,11 @@ class Doc(commands.Cog):          self.renamed_symbols.clear()          async_cache.cache = OrderedDict() -        # Since Intersphinx is intended to be used with Sphinx, -        # we need to mock its configuration. -        config = SphinxConfiguration() -          # Run all coroutines concurrently - since each of them performs a HTTP          # request, this speeds up fetching the inventory data heavily.          coros = [              self.update_single( -                package["package"], package["base_url"], package["inventory_url"], config +                package["package"], package["base_url"], package["inventory_url"]              ) for package in await self.bot.api_client.get('bot/documentation-links')          ]          await asyncio.gather(*coros) @@ -306,10 +299,17 @@ class Doc(commands.Cog):          # of a double newline (interpreted as a paragraph) before index 1000.          if len(description) > 1000:              shortened = description[:1000] -            last_paragraph_end = shortened.rfind('\n\n', 100) -            if last_paragraph_end == -1: -                last_paragraph_end = shortened.rfind('. ') -            description = description[:last_paragraph_end] +            description_cutoff = shortened.rfind('\n\n', 100) +            if description_cutoff == -1: +                # Search the shortened version for cutoff points in decreasing desirability, +                # cutoff at 1000 if none are found. +                for string in (". ", ", ", ",", " "): +                    description_cutoff = shortened.rfind(string) +                    if description_cutoff != -1: +                        break +                else: +                    description_cutoff = 1000 +            description = description[:description_cutoff]              # If there is an incomplete code block, cut it out              if description.count("```") % 2: @@ -318,7 +318,6 @@ class Doc(commands.Cog):              description += f"... [read more]({permalink})"          description = WHITESPACE_AFTER_NEWLINES_RE.sub('', description) -          if signatures is None:              # If symbol is a module, don't show signature.              embed_description = description @@ -470,9 +469,9 @@ class Doc(commands.Cog):          )          await ctx.send(embed=embed) -    async def _fetch_inventory(self, inventory_url: str, config: SphinxConfiguration) -> Optional[dict]: +    async def _fetch_inventory(self, inventory_url: str) -> Optional[dict]:          """Get and return inventory from `inventory_url`. If fetching fails, return None.""" -        fetch_func = functools.partial(intersphinx.fetch_inventory, config, '', inventory_url) +        fetch_func = functools.partial(intersphinx.fetch_inventory, SPHINX_MOCK_APP, '', inventory_url)          for retry in range(1, FAILED_REQUEST_RETRY_AMOUNT+1):              try:                  package = await self.bot.loop.run_in_executor(None, fetch_func) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 1ede95ff4..125d7ce24 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -189,7 +189,11 @@ class Information(Cog):          # Custom status          custom_status = ''          for activity in user.activities: -            if activity.name == 'Custom Status': +            # Check activity.state for None value if user has a custom status set +            # This guards against a custom status with an emoji but no text, which will cause +            # escape_markdown to raise an exception +            # This can be reworked after a move to d.py 1.3.0+, which adds a CustomActivity class +            if activity.name == 'Custom Status' and activity.state:                  state = escape_markdown(activity.state)                  custom_status = f'Status: {state}\n' diff --git a/bot/cogs/metrics.py b/bot/cogs/metrics.py new file mode 100644 index 000000000..47c3cc55e --- /dev/null +++ b/bot/cogs/metrics.py @@ -0,0 +1,98 @@ +from collections import defaultdict + +from discord import Member, Message +from discord.ext.commands import Cog, Context +from prometheus_client import Counter, Gauge + +from bot.bot import Bot + + +class Metrics(Cog): +    """ +    Exports metrics for Prometheus. + +    See https://github.com/prometheus/client_python for metric documentation. +    """ + +    PREFIX = 'pydis_bot' + +    def __init__(self, bot: Bot) -> None: +        self.bot = bot + +        self.guild_members = Gauge( +            name=f'{self.PREFIX}_guild_members', +            documentation="Total members by guild by status.", +            labelnames=('guild_id', 'status') +        ) +        self.guild_messages = Counter( +            name=f'{self.PREFIX}_guild_messages', +            documentation="Guild messages by guild by channel.", +            labelnames=('channel_id', 'guild_id', 'channel_name') +        ) +        self.command_completions = Counter( +            name=f'{self.PREFIX}_command_completions', +            documentation="Completed commands by command, user, and guild.", +            labelnames=('guild_id', 'user_id', 'user_name', 'command') +        ) + +    @Cog.listener() +    async def on_ready(self) -> None: +        """Initialize the guild member counter.""" +        members_by_status = defaultdict(lambda: defaultdict(int)) + +        for guild in self.bot.guilds: +            if guild.large: +                await self.bot.request_offline_members(guild) +            for member in guild.members: +                members_by_status[guild.id][member.status] += 1 + +        for guild_id, members in members_by_status.items(): +            for status, count in members.items(): +                self.guild_members.labels(guild_id=guild_id, status=str(status)).set(count) + +    @Cog.listener() +    async def on_member_join(self, member: Member) -> None: +        """Increment the member gauge.""" +        self.guild_members.labels(guild_id=member.guild.id, status=str(member.status)).inc() + +    @Cog.listener() +    async def on_member_leave(self, member: Member) -> None: +        """Decrement the member gauge.""" +        self.guild_members.labels(guild_id=member.guild.id, status=str(member.status)).dec() + +    @Cog.listener() +    async def on_member_update(self, before: Member, after: Member) -> None: +        """Update member gauges for the new and old status if applicable.""" +        if before.status is not after.status: +            self.guild_members.labels(guild_id=after.guild.id, status=str(before.status)).dec() +            self.guild_members.labels(guild_id=after.guild.id, status=str(after.status)).inc() + +    @Cog.listener() +    async def on_message(self, message: Message) -> None: +        """Increment the guild message counter.""" +        self.guild_messages.labels( +            channel_id=message.channel.id, +            channel_name=message.channel.name, +            guild_id=message.guild.id, +        ).inc() + +    @Cog.listener() +    async def on_command_completion(self, ctx: Context) -> None: +        """Increment the command completion counter.""" +        if ctx.message.guild is not None: +            if ctx.command.full_parent_name: +                command = f'{ctx.command.full_parent_name} {ctx.command.name}' +            else: +                command = ctx.command.name + +            self.command_completions.labels( +                guild_id=ctx.message.guild.id, +                user_id=ctx.author.id, +                user_name=str(ctx.author), +                command=command, +            ).inc() + + +def setup(bot: Bot) -> None: +    """Load the Metrics cog.""" +    bot.add_cog(Metrics(bot)) diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 3536a3d38..f4e296df9 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -9,16 +9,15 @@ from discord.ext.commands import Context, command  from bot import constants  from bot.bot import Bot  from bot.constants import Event +from bot.converters import Expiry, FetchedMember  from bot.decorators import respect_role_hierarchy  from bot.utils.checks import with_role_check  from . import utils  from .scheduler import InfractionScheduler -from .utils import MemberObject +from .utils import UserSnowflake  log = logging.getLogger(__name__) -MemberConverter = t.Union[utils.UserTypes, utils.proxy_user] -  class Infractions(InfractionScheduler, commands.Cog):      """Apply and pardon infractions on users for moderation purposes.""" @@ -67,7 +66,7 @@ class Infractions(InfractionScheduler, commands.Cog):          await self.apply_kick(ctx, user, reason, active=False)      @command() -    async def ban(self, ctx: Context, user: MemberConverter, *, reason: str = None) -> None: +    async def ban(self, ctx: Context, user: FetchedMember, *, reason: str = None) -> None:          """Permanently ban a user for the given reason."""          await self.apply_ban(ctx, user, reason) @@ -75,7 +74,7 @@ class Infractions(InfractionScheduler, commands.Cog):      # region: Temporary infractions      @command(aliases=["mute"]) -    async def tempmute(self, ctx: Context, user: Member, duration: utils.Expiry, *, reason: str = None) -> None: +    async def tempmute(self, ctx: Context, user: Member, duration: Expiry, *, reason: str = None) -> None:          """          Temporarily mute a user for the given reason and duration. @@ -94,7 +93,7 @@ class Infractions(InfractionScheduler, commands.Cog):          await self.apply_mute(ctx, user, reason, expires_at=duration)      @command() -    async def tempban(self, ctx: Context, user: MemberConverter, duration: utils.Expiry, *, reason: str = None) -> None: +    async def tempban(self, ctx: Context, user: FetchedMember, duration: Expiry, *, reason: str = None) -> None:          """          Temporarily ban a user for the given reason and duration. @@ -116,7 +115,7 @@ class Infractions(InfractionScheduler, commands.Cog):      # region: Permanent shadow infractions      @command(hidden=True) -    async def note(self, ctx: Context, user: MemberConverter, *, reason: str = None) -> None: +    async def note(self, ctx: Context, user: FetchedMember, *, reason: str = None) -> None:          """Create a private note for a user with the given reason without notifying the user."""          infraction = await utils.post_infraction(ctx, user, "note", reason, hidden=True, active=False)          if infraction is None: @@ -130,7 +129,7 @@ class Infractions(InfractionScheduler, commands.Cog):          await self.apply_kick(ctx, user, reason, hidden=True, active=False)      @command(hidden=True, aliases=['shadowban', 'sban']) -    async def shadow_ban(self, ctx: Context, user: MemberConverter, *, reason: str = None) -> None: +    async def shadow_ban(self, ctx: Context, user: FetchedMember, *, reason: str = None) -> None:          """Permanently ban a user for the given reason without notifying the user."""          await self.apply_ban(ctx, user, reason, hidden=True) @@ -138,7 +137,7 @@ class Infractions(InfractionScheduler, commands.Cog):      # region: Temporary shadow infractions      @command(hidden=True, aliases=["shadowtempmute, stempmute", "shadowmute", "smute"]) -    async def shadow_tempmute(self, ctx: Context, user: Member, duration: utils.Expiry, *, reason: str = None) -> None: +    async def shadow_tempmute(self, ctx: Context, user: Member, duration: Expiry, *, reason: str = None) -> None:          """          Temporarily mute a user for the given reason and duration without notifying the user. @@ -160,8 +159,8 @@ class Infractions(InfractionScheduler, commands.Cog):      async def shadow_tempban(          self,          ctx: Context, -        user: MemberConverter, -        duration: utils.Expiry, +        user: FetchedMember, +        duration: Expiry,          *,          reason: str = None      ) -> None: @@ -186,12 +185,12 @@ class Infractions(InfractionScheduler, commands.Cog):      # region: Remove infractions (un- commands)      @command() -    async def unmute(self, ctx: Context, user: MemberConverter) -> None: +    async def unmute(self, ctx: Context, user: FetchedMember) -> None:          """Prematurely end the active mute infraction for the user."""          await self.pardon_infraction(ctx, "mute", user)      @command() -    async def unban(self, ctx: Context, user: MemberConverter) -> None: +    async def unban(self, ctx: Context, user: FetchedMember) -> None:          """Prematurely end the active ban infraction for the user."""          await self.pardon_infraction(ctx, "ban", user) @@ -203,7 +202,7 @@ class Infractions(InfractionScheduler, commands.Cog):          if await utils.has_active_infraction(ctx, user, "mute"):              return -        infraction = await utils.post_infraction(ctx, user, "mute", reason, **kwargs) +        infraction = await utils.post_infraction(ctx, user, "mute", reason, active=True, **kwargs)          if infraction is None:              return @@ -220,7 +219,7 @@ class Infractions(InfractionScheduler, commands.Cog):      @respect_role_hierarchy()      async def apply_kick(self, ctx: Context, user: Member, reason: str, **kwargs) -> None:          """Apply a kick infraction with kwargs passed to `post_infraction`.""" -        infraction = await utils.post_infraction(ctx, user, "kick", reason, **kwargs) +        infraction = await utils.post_infraction(ctx, user, "kick", reason, active=False, **kwargs)          if infraction is None:              return @@ -230,12 +229,12 @@ class Infractions(InfractionScheduler, commands.Cog):          await self.apply_infraction(ctx, infraction, user, action)      @respect_role_hierarchy() -    async def apply_ban(self, ctx: Context, user: MemberObject, reason: str, **kwargs) -> None: +    async def apply_ban(self, ctx: Context, user: UserSnowflake, reason: str, **kwargs) -> None:          """Apply a ban infraction with kwargs passed to `post_infraction`."""          if await utils.has_active_infraction(ctx, user, "ban"):              return -        infraction = await utils.post_infraction(ctx, user, "ban", reason, **kwargs) +        infraction = await utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs)          if infraction is None:              return diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 9605d47b2..0636422d3 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -10,7 +10,7 @@ from discord.ext.commands import Context  from bot import constants  from bot.bot import Bot -from bot.converters import InfractionSearchQuery, allowed_strings +from bot.converters import Expiry, InfractionSearchQuery, allowed_strings, proxy_user  from bot.pagination import LinePaginator  from bot.utils import time  from bot.utils.checks import in_channel_check, with_role_check @@ -20,8 +20,6 @@ from .modlog import ModLog  log = logging.getLogger(__name__) -UserConverter = t.Union[discord.User, utils.proxy_user] -  class ModManagement(commands.Cog):      """Management of infractions.""" @@ -53,7 +51,7 @@ class ModManagement(commands.Cog):          self,          ctx: Context,          infraction_id: t.Union[int, allowed_strings("l", "last", "recent")], -        duration: t.Union[utils.Expiry, allowed_strings("p", "permanent"), None], +        duration: t.Union[Expiry, allowed_strings("p", "permanent"), None],          *,          reason: str = None      ) -> None: @@ -182,7 +180,7 @@ class ModManagement(commands.Cog):              await ctx.invoke(self.search_reason, query)      @infraction_search_group.command(name="user", aliases=("member", "id")) -    async def search_user(self, ctx: Context, user: UserConverter) -> None: +    async def search_user(self, ctx: Context, user: t.Union[discord.User, proxy_user]) -> None:          """Search for infractions by member."""          infraction_list = await self.bot.api_client.get(              'bot/infractions', diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index 35ef6cbcc..c78eb24a7 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -15,7 +15,6 @@ from discord.ext.commands import Cog, Context  from bot.bot import Bot  from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs  from bot.utils.time import humanize_delta -from .utils import UserTypes  log = logging.getLogger(__name__) @@ -26,6 +25,12 @@ CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position")  MEMBER_CHANGES_SUPPRESSED = ("status", "activities", "_client_status", "nick")  ROLE_CHANGES_UNSUPPORTED = ("colour", "permissions") +VOICE_STATE_ATTRIBUTES = { +    "channel.name": "Channel", +    "self_stream": "Streaming", +    "self_video": "Broadcasting", +} +  class ModLog(Cog, name="ModLog"):      """Logging for server events and staff actions.""" @@ -206,7 +211,7 @@ class ModLog(Cog, name="ModLog"):                  new = value["new_value"]                  old = value["old_value"] -                changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") +                changes.append(f"**{key.title()}:** `{old}` **→** `{new}`")              done.append(key) @@ -284,7 +289,7 @@ class ModLog(Cog, name="ModLog"):                  new = value["new_value"]                  old = value["old_value"] -                changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") +                changes.append(f"**{key.title()}:** `{old}` **→** `{new}`")              done.append(key) @@ -334,7 +339,7 @@ class ModLog(Cog, name="ModLog"):              new = value["new_value"]              old = value["old_value"] -            changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") +            changes.append(f"**{key.title()}:** `{old}` **→** `{new}`")              done.append(key) @@ -355,7 +360,7 @@ class ModLog(Cog, name="ModLog"):          )      @Cog.listener() -    async def on_member_ban(self, guild: discord.Guild, member: UserTypes) -> None: +    async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None:          """Log ban event to user log."""          if guild.id != GuildConstant.id:              return @@ -487,23 +492,23 @@ class ModLog(Cog, name="ModLog"):                  old = value.get("old_value")                  if new and old: -                    changes.append(f"**{key.title()}:** `{old}` **->** `{new}`") +                    changes.append(f"**{key.title()}:** `{old}` **→** `{new}`")              done.append(key)          if before.name != after.name:              changes.append( -                f"**Username:** `{before.name}` **->** `{after.name}`" +                f"**Username:** `{before.name}` **→** `{after.name}`"              )          if before.discriminator != after.discriminator:              changes.append( -                f"**Discriminator:** `{before.discriminator}` **->** `{after.discriminator}`" +                f"**Discriminator:** `{before.discriminator}` **→** `{after.discriminator}`"              )          if before.display_name != after.display_name:              changes.append( -                f"**Display name:** `{before.display_name}` **->** `{after.display_name}`" +                f"**Display name:** `{before.display_name}` **→** `{after.display_name}`"              )          if not changes: @@ -749,3 +754,76 @@ class ModLog(Cog, name="ModLog"):              Icons.message_edit, Colour.blurple(), "Message edited (After)",              after_response, channel_id=Channels.message_log          ) + +    @Cog.listener() +    async def on_voice_state_update( +        self, +        member: discord.Member, +        before: discord.VoiceState, +        after: discord.VoiceState +    ) -> None: +        """Log member voice state changes to the voice log channel.""" +        if ( +            member.guild.id != GuildConstant.id +            or (before.channel and before.channel.id in GuildConstant.ignored) +        ): +            return + +        if member.id in self._ignored[Event.voice_state_update]: +            self._ignored[Event.voice_state_update].remove(member.id) +            return + +        # Exclude all channel attributes except the name. +        diff = DeepDiff( +            before, +            after, +            exclude_paths=("root.session_id", "root.afk"), +            exclude_regex_paths=r"root\.channel\.(?!name)", +        ) + +        # A type change seems to always take precedent over a value change. Furthermore, it will +        # include the value change along with the type change anyway. Therefore, it's OK to +        # "overwrite" values_changed; in practice there will never even be anything to overwrite. +        diff_values = {**diff.get("values_changed", {}), **diff.get("type_changes", {})} + +        icon = Icons.voice_state_blue +        colour = Colour.blurple() +        changes = [] + +        for attr, values in diff_values.items(): +            if not attr:  # Not sure why, but it happens. +                continue + +            old = values["old_value"] +            new = values["new_value"] + +            attr = attr[5:]  # Remove "root." prefix. +            attr = VOICE_STATE_ATTRIBUTES.get(attr, attr.replace("_", " ").capitalize()) + +            changes.append(f"**{attr}:** `{old}` **→** `{new}`") + +            # Set the embed icon and colour depending on which attribute changed. +            if any(name in attr for name in ("Channel", "deaf", "mute")): +                if new is None or new is True: +                    # Left a channel or was muted/deafened. +                    icon = Icons.voice_state_red +                    colour = Colours.soft_red +                elif old is None or old is True: +                    # Joined a channel or was unmuted/undeafened. +                    icon = Icons.voice_state_green +                    colour = Colours.soft_green + +        if not changes: +            return + +        message = "\n".join(f"{Emojis.bullet} {item}" for item in sorted(changes)) +        message = f"**{member}** (`{member.id}`)\n{message}" + +        await self.send_log_message( +            icon_url=icon, +            colour=colour, +            title="Voice state updated", +            text=message, +            thumbnail=member.avatar_url_as(static_format="png"), +            channel_id=Channels.voice_log +        ) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 01e4b1fe7..e14c302cb 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -17,7 +17,7 @@ from bot.utils import time  from bot.utils.scheduling import Scheduler  from . import utils  from .modlog import ModLog -from .utils import MemberObject +from .utils import UserSnowflake  log = logging.getLogger(__name__) @@ -77,7 +77,7 @@ class InfractionScheduler(Scheduler):          self,          ctx: Context,          infraction: utils.Infraction, -        user: MemberObject, +        user: UserSnowflake,          action_coro: t.Optional[t.Awaitable] = None      ) -> None:          """Apply an infraction to the user, log the infraction, and optionally notify the user.""" @@ -106,16 +106,20 @@ class InfractionScheduler(Scheduler):          # DM the user about the infraction if it's not a shadow/hidden infraction.          if not infraction["hidden"]: -            # Sometimes user is a discord.Object; make it a proper user. -            user = await self.bot.fetch_user(user.id) +            dm_result = f"{constants.Emojis.failmail} " +            dm_log_text = "\nDM: **Failed**" -            # Accordingly display whether the user was successfully notified via DM. -            if await utils.notify_infraction(user, infr_type, expiry, reason, icon): -                dm_result = ":incoming_envelope: " -                dm_log_text = "\nDM: Sent" +            # Sometimes user is a discord.Object; make it a proper user. +            try: +                if not isinstance(user, (discord.Member, discord.User)): +                    user = await self.bot.fetch_user(user.id) +            except discord.HTTPException as e: +                log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})")              else: -                dm_result = f"{constants.Emojis.failmail} " -                dm_log_text = "\nDM: **Failed**" +                # Accordingly display whether the user was successfully notified via DM. +                if await utils.notify_infraction(user, infr_type, expiry, reason, icon): +                    dm_result = ":incoming_envelope: " +                    dm_log_text = "\nDM: Sent"          if infraction["actor"] == self.bot.user.id:              log.trace( @@ -185,7 +189,7 @@ class InfractionScheduler(Scheduler):          log.info(f"Applied {infr_type} infraction #{id_} to {user}.") -    async def pardon_infraction(self, ctx: Context, infr_type: str, user: MemberObject) -> None: +    async def pardon_infraction(self, ctx: Context, infr_type: str, user: UserSnowflake) -> None:          """Prematurely end an infraction for a user and log the action in the mod log."""          log.trace(f"Pardoning {infr_type} infraction for {user}.") diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py index 7631d9bbe..050c847ac 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/cogs/moderation/superstarify.py @@ -10,6 +10,7 @@ from discord.ext.commands import Cog, Context, command  from bot import constants  from bot.bot import Bot +from bot.converters import Expiry  from bot.utils.checks import with_role_check  from bot.utils.time import format_infraction  from . import utils @@ -107,7 +108,7 @@ class Superstarify(InfractionScheduler, Cog):          self,          ctx: Context,          member: Member, -        duration: utils.Expiry, +        duration: Expiry,          reason: str = None      ) -> None:          """ @@ -133,7 +134,7 @@ class Superstarify(InfractionScheduler, Cog):          # Post the infraction to the API          reason = reason or f"old nick: {member.display_name}" -        infraction = await utils.post_infraction(ctx, member, "superstar", reason, duration) +        infraction = await utils.post_infraction(ctx, member, "superstar", reason, duration, active=True)          id_ = infraction["id"]          old_nick = member.display_name diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 325b9567a..5052b9048 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -4,12 +4,10 @@ import typing as t  from datetime import datetime  import discord -from discord.ext import commands  from discord.ext.commands import Context  from bot.api import ResponseCodeError  from bot.constants import Colours, Icons -from bot.converters import Duration, ISODateTime  log = logging.getLogger(__name__) @@ -25,40 +23,49 @@ INFRACTION_ICONS = {  RULES_URL = "https://pythondiscord.com/pages/rules"  APPEALABLE_INFRACTIONS = ("ban", "mute") -UserTypes = t.Union[discord.Member, discord.User] -MemberObject = t.Union[UserTypes, discord.Object] +# Type aliases +UserObject = t.Union[discord.Member, discord.User] +UserSnowflake = t.Union[UserObject, discord.Object]  Infraction = t.Dict[str, t.Union[str, int, bool]] -Expiry = t.Union[Duration, ISODateTime] -def proxy_user(user_id: str) -> discord.Object: +async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]:      """ -    Create a proxy user object from the given id. +    Create a new user in the database. -    Used when a Member or User object cannot be resolved. +    Used when an infraction needs to be applied on a user absent in the guild.      """ -    log.trace(f"Attempting to create a proxy user for the user id {user_id}.") +    log.trace(f"Attempting to add user {user.id} to the database.") -    try: -        user_id = int(user_id) -    except ValueError: -        raise commands.BadArgument +    if not isinstance(user, (discord.Member, discord.User)): +        log.warning("The user being added to the DB is not a Member or User object.") -    user = discord.Object(user_id) -    user.mention = user.id -    user.avatar_url_as = lambda static_format: None +    payload = { +        'avatar_hash': getattr(user, 'avatar', 0), +        'discriminator': int(getattr(user, 'discriminator', 0)), +        'id': user.id, +        'in_guild': False, +        'name': getattr(user, 'name', 'Name unknown'), +        'roles': [] +    } -    return user +    try: +        response = await ctx.bot.api_client.post('bot/users', json=payload) +        log.info(f"User {user.id} added to the DB.") +        return response +    except ResponseCodeError as e: +        log.error(f"Failed to add user {user.id} to the DB. {e}") +        await ctx.send(f":x: The attempt to add the user to the DB failed: status {e.status}")  async def post_infraction(      ctx: Context, -    user: MemberObject, +    user: UserSnowflake,      infr_type: str,      reason: str,      expires_at: datetime = None,      hidden: bool = False, -    active: bool = True, +    active: bool = True  ) -> t.Optional[dict]:      """Posts an infraction to the API."""      log.trace(f"Posting {infr_type} infraction for {user} to the API.") @@ -74,27 +81,23 @@ async def post_infraction(      if expires_at:          payload['expires_at'] = expires_at.isoformat() -    try: -        response = await ctx.bot.api_client.post('bot/infractions', json=payload) -    except ResponseCodeError as exp: -        if exp.status == 400 and 'user' in exp.response_json: -            log.info( -                f"{ctx.author} tried to add a {infr_type} infraction to `{user.id}`, " -                "but that user id was not found in the database." -            ) -            await ctx.send( -                f":x: Cannot add infraction, the specified user is not known to the database." -            ) -            return -        else: -            log.exception("An unexpected ResponseCodeError occurred while adding an infraction:") -            await ctx.send(":x: There was an error adding the infraction.") -            return - -    return response - - -async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str) -> bool: +    # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. +    for should_post_user in (True, False): +        try: +            response = await ctx.bot.api_client.post('bot/infractions', json=payload) +            return response +        except ResponseCodeError as e: +            if e.status == 400 and 'user' in e.response_json: +                # Only one attempt to add the user to the database, not two: +                if not should_post_user or await post_user(ctx, user) is None: +                    return +            else: +                log.exception(f"Unexpected error while adding an infraction for {user}:") +                await ctx.send(f":x: There was an error adding the infraction: status {e.status}.") +                return + + +async def has_active_infraction(ctx: Context, user: UserSnowflake, infr_type: str) -> bool:      """Checks if a user already has an active infraction of the given type."""      log.trace(f"Checking if {user} has active infractions of type {infr_type}.") @@ -119,7 +122,7 @@ async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str  async def notify_infraction( -    user: UserTypes, +    user: UserObject,      infr_type: str,      expires_at: t.Optional[str] = None,      reason: t.Optional[str] = None, @@ -150,7 +153,7 @@ async def notify_infraction(  async def notify_pardon( -    user: UserTypes, +    user: UserObject,      title: str,      content: str,      icon_url: str = Icons.user_verified @@ -168,7 +171,7 @@ async def notify_pardon(      return await send_private_embed(user, embed) -async def send_private_embed(user: UserTypes, embed: discord.Embed) -> bool: +async def send_private_embed(user: UserObject, embed: discord.Embed) -> bool:      """      A helper method for sending an embed to a user's DMs. diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 47a59db66..da278011a 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -62,14 +62,12 @@ class Utils(Cog):                  pep_embed.set_thumbnail(url="https://www.python.org/static/opengraph-icon-200x200.png")                  # Add the interesting information -                if "Status" in pep_header: -                    pep_embed.add_field(name="Status", value=pep_header["Status"]) -                if "Python-Version" in pep_header: -                    pep_embed.add_field(name="Python-Version", value=pep_header["Python-Version"]) -                if "Created" in pep_header: -                    pep_embed.add_field(name="Created", value=pep_header["Created"]) -                if "Type" in pep_header: -                    pep_embed.add_field(name="Type", value=pep_header["Type"]) +                fields_to_check = ("Status", "Python-Version", "Created", "Type") +                for field in fields_to_check: +                    # Check for a PEP metadata field that is present but has an empty value +                    # embed field values can't contain an empty string +                    if pep_header.get(field, ""): +                        pep_embed.add_field(name=field, value=pep_header[field])              elif response.status != 404:                  # any response except 200 and 404 is expected diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 306ed4c64..c601e0d4d 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -1,15 +1,14 @@  import logging  from collections import ChainMap -from typing import Union -from discord import User  from discord.ext.commands import Cog, Context, group  from bot.bot import Bot  from bot.cogs.moderation.utils import post_infraction  from bot.constants import Channels, MODERATION_ROLES, Webhooks +from bot.converters import FetchedMember  from bot.decorators import with_role -from .watchchannel import WatchChannel, proxy_user +from .watchchannel import WatchChannel  log = logging.getLogger(__name__) @@ -46,7 +45,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):      @bigbrother_group.command(name='watch', aliases=('w',))      @with_role(*MODERATION_ROLES) -    async def watch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: +    async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """          Relay messages sent by the given `user` to the `#big-brother` channel. @@ -62,10 +61,10 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):              return          if user.id in self.watched_users: -            await ctx.send(":x: The specified user is already being watched.") +            await ctx.send(f":x: {user} is already being watched.")              return -        response = await post_infraction(ctx, user, 'watch', reason, hidden=True) +        response = await post_infraction(ctx, user, 'watch', reason, hidden=True, active=True)          if response is not None:              self.watched_users[user.id] = response @@ -93,7 +92,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):      @bigbrother_group.command(name='unwatch', aliases=('uw',))      @with_role(*MODERATION_ROLES) -    async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: +    async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """Stop relaying messages by the given `user`."""          active_watches = await self.bot.api_client.get(              self.api_endpoint, diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index cc8feeeee..ad0c51fa6 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -1,18 +1,18 @@  import logging  import textwrap  from collections import ChainMap -from typing import Union -from discord import Color, Embed, Member, User +from discord import Color, Embed, Member  from discord.ext.commands import Cog, Context, group  from bot.api import ResponseCodeError  from bot.bot import Bot  from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks +from bot.converters import FetchedMember  from bot.decorators import with_role  from bot.pagination import LinePaginator  from bot.utils import time -from .watchchannel import WatchChannel, proxy_user +from .watchchannel import WatchChannel  log = logging.getLogger(__name__) @@ -49,7 +49,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):      @nomination_group.command(name='watch', aliases=('w', 'add', 'a'))      @with_role(*STAFF_ROLES) -    async def watch_command(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None: +    async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """          Relay messages sent by the given `user` to the `#talent-pool` channel. @@ -69,7 +69,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):              return          if user.id in self.watched_users: -            await ctx.send(":x: The specified user is already being watched in the talent pool") +            await ctx.send(f":x: {user} is already being watched in the talent pool")              return          # Manual request with `raise_for_status` as False because we want the actual response @@ -114,7 +114,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):      @nomination_group.command(name='history', aliases=('info', 'search'))      @with_role(*MODERATION_ROLES) -    async def history_command(self, ctx: Context, user: Union[User, proxy_user]) -> None: +    async def history_command(self, ctx: Context, user: FetchedMember) -> None:          """Shows the specified user's nomination history."""          result = await self.bot.api_client.get(              self.api_endpoint, @@ -143,7 +143,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):      @nomination_group.command(name='unwatch', aliases=('end', ))      @with_role(*MODERATION_ROLES) -    async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None: +    async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None:          """          Ends the active nomination of the specified user with the given reason. diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index bd0622554..eb787b083 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -9,8 +9,8 @@ from typing import Optional  import dateutil.parser  import discord -from discord import Color, Embed, HTTPException, Message, Object, errors -from discord.ext.commands import BadArgument, Cog, Context +from discord import Color, Embed, HTTPException, Message, errors +from discord.ext.commands import Cog, Context  from bot.api import ResponseCodeError  from bot.bot import Bot @@ -25,22 +25,6 @@ log = logging.getLogger(__name__)  URL_RE = re.compile(r"(https?://[^\s]+)") -def proxy_user(user_id: str) -> Object: -    """A proxy user object that mocks a real User instance for when the later is not available.""" -    try: -        user_id = int(user_id) -    except ValueError: -        raise BadArgument - -    user = Object(user_id) -    user.mention = user.id -    user.display_name = f"<@{user.id}>" -    user.avatar_url_as = lambda static_format: None -    user.bot = False - -    return user - -  @dataclass  class MessageHistory:      """Represents a watch channel's message history.""" diff --git a/bot/constants.py b/bot/constants.py index 2c0e3b10b..25c7856ba 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -333,6 +333,10 @@ class Icons(metaclass=YAMLGetter):      superstarify: str      unsuperstarify: str +    voice_state_blue: str +    voice_state_green: str +    voice_state_red: str +  class CleanMessages(metaclass=YAMLGetter):      section = "bot" @@ -387,6 +391,7 @@ class Channels(metaclass=YAMLGetter):      userlog: int      user_event_a: int      verification: int +    voice_log: int  class Webhooks(metaclass=YAMLGetter): @@ -553,6 +558,8 @@ class Event(Enum):      message_delete = "message_delete"      message_edit = "message_edit" +    voice_state_update = "voice_state_update" +  # Debug mode  DEBUG_MODE = True if 'local' in os.environ.get("SITE_URL", "local") else False diff --git a/bot/converters.py b/bot/converters.py index 8d2ab7eb8..cca57a02d 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -9,7 +9,7 @@ import dateutil.tz  import discord  from aiohttp import ClientConnectorError  from dateutil.relativedelta import relativedelta -from discord.ext.commands import BadArgument, Context, Converter +from discord.ext.commands import BadArgument, Context, Converter, UserConverter  log = logging.getLogger(__name__) @@ -278,3 +278,75 @@ class ISODateTime(Converter):              dt = dt.replace(tzinfo=None)          return dt + + +def proxy_user(user_id: str) -> discord.Object: +    """ +    Create a proxy user object from the given id. + +    Used when a Member or User object cannot be resolved. +    """ +    log.trace(f"Attempting to create a proxy user for the user id {user_id}.") + +    try: +        user_id = int(user_id) +    except ValueError: +        log.debug(f"Failed to create proxy user {user_id}: could not convert to int.") +        raise BadArgument(f"User ID `{user_id}` is invalid - could not convert to an integer.") + +    user = discord.Object(user_id) +    user.mention = user.id +    user.display_name = f"<@{user.id}>" +    user.avatar_url_as = lambda static_format: None +    user.bot = False + +    return user + + +class FetchedUser(UserConverter): +    """ +    Converts to a `discord.User` or, if it fails, a `discord.Object`. + +    Unlike the default `UserConverter`, which only does lookups via the global user cache, this +    converter attempts to fetch the user via an API call to Discord when the using the cache is +    unsuccessful. + +    If the fetch also fails and the error doesn't imply the user doesn't exist, then a +    `discord.Object` is returned via the `user_proxy` converter. + +    The lookup strategy is as follows (in order): + +    1. Lookup by ID. +    2. Lookup by mention. +    3. Lookup by name#discrim +    4. Lookup by name +    5. Lookup via API +    6. Create a proxy user with discord.Object +    """ + +    async def convert(self, ctx: Context, arg: str) -> t.Union[discord.User, discord.Object]: +        """Convert the `arg` to a `discord.User` or `discord.Object`.""" +        try: +            return await super().convert(ctx, arg) +        except BadArgument: +            pass + +        try: +            user_id = int(arg) +            log.trace(f"Fetching user {user_id}...") +            return await ctx.bot.fetch_user(user_id) +        except ValueError: +            log.debug(f"Failed to fetch user {arg}: could not convert to int.") +            raise BadArgument(f"The provided argument can't be turned into integer: `{arg}`") +        except discord.HTTPException as e: +            # If the Discord error isn't `Unknown user`, return a proxy instead +            if e.code != 10013: +                log.warning(f"Failed to fetch user, returning a proxy instead: status {e.status}") +                return proxy_user(arg) + +            log.debug(f"Failed to fetch user {arg}: user does not exist.") +            raise BadArgument(f"User `{arg}` does not exist") + + +Expiry = t.Union[Duration, ISODateTime] +FetchedMember = t.Union[discord.Member, FetchedUser] diff --git a/config-default.yml b/config-default.yml index c64430336..f842cf606 100644 --- a/config-default.yml +++ b/config-default.yml @@ -100,6 +100,10 @@ style:          superstarify: "https://cdn.discordapp.com/emojis/636288153044516874.png"          unsuperstarify: "https://cdn.discordapp.com/emojis/636288201258172446.png" +        voice_state_blue: "https://cdn.discordapp.com/emojis/656899769662439456.png" +        voice_state_green: "https://cdn.discordapp.com/emojis/656899770094452754.png" +        voice_state_red: "https://cdn.discordapp.com/emojis/656899769905709076.png" +  guild:      id: 267624335836053506 @@ -109,6 +113,7 @@ guild:      channels:          admins:            &ADMINS        365960823622991872          admin_spam:        &ADMIN_SPAM    563594791770914816 +        admins_voice:      &ADMINS_VOICE  500734494840717332          announcements:                    354619224620138496          big_brother_logs:  &BBLOGS        468507907357409333          bot:                              267659945086812160 @@ -139,13 +144,15 @@ guild:          python:                           267624335836053506          reddit:                           458224812528238616          staff_lounge:      &STAFF_LOUNGE  464905259261755392 +        staff_voice:       &STAFF_VOICE   412375055910043655          talent_pool:       &TALENT_POOL   534321732593647616          userlog:                          528976905546760203          user_event_a:      &USER_EVENT_A  592000283102674944          verification:                     352442727016693763 +        voice_log:                        640292421988646961      staff_channels: [*ADMINS, *ADMIN_SPAM, *MOD_SPAM, *MODS, *HELPERS, *ORGANISATION, *DEFCON] -    ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG] +    ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG, *ADMINS_VOICE, *STAFF_VOICE]      roles:          admin:             &ADMIN_ROLE      267628507062992896 @@ -154,7 +161,7 @@ guild:          contributor:                        295488872404484098          core_developer:                     587606783669829632          helpers:                            267630620367257601 -        jammer:                             423054537079783434 +        jammer:                             591786436651646989          moderator:         &MOD_ROLE        267629731250176001          muted:             &MUTED_ROLE      277914926603829249          owner:             &OWNER_ROLE      267627879762755584 @@ -201,6 +208,12 @@ filter:          - 544525886180032552  # kennethreitz.org          - 590806733924859943  # Discord Hack Week          - 423249981340778496  # Kivy +        - 197038439483310086  # Discord Testers +        - 286633898581164032  # Ren'Py +        - 349505959032389632  # PyGame +        - 438622377094414346  # Pyglet +        - 524691714909274162  # Panda3D +        - 336642139381301249  # discord.py      domain_blacklist:          - pornhub.com @@ -373,6 +386,9 @@ anti_malware:          - '.ai'   # Illustrator          - '.aep'  # After Effects          - '.xcf'  # GIMP +        - '.mp3' +        - '.wav' +        - '.ogg'  reddit:  |