diff options
Diffstat (limited to '')
| -rw-r--r-- | .github/workflows/build.yml | 2 | ||||
| -rw-r--r-- | .github/workflows/sentry_release.yml | 24 | ||||
| -rw-r--r-- | Dockerfile | 11 | ||||
| -rw-r--r-- | Pipfile | 6 | ||||
| -rw-r--r-- | Pipfile.lock | 466 | ||||
| -rw-r--r-- | bot/api.py | 73 | ||||
| -rw-r--r-- | bot/bot.py | 82 | ||||
| -rw-r--r-- | bot/constants.py | 6 | ||||
| -rw-r--r-- | bot/exts/info/information.py | 22 | ||||
| -rw-r--r-- | bot/exts/info/pep.py | 164 | ||||
| -rw-r--r-- | bot/exts/moderation/silence.py | 14 | ||||
| -rw-r--r-- | bot/exts/moderation/verification.py | 62 | ||||
| -rw-r--r-- | bot/exts/moderation/voice_gate.py | 11 | ||||
| -rw-r--r-- | bot/exts/utils/bot.py | 4 | ||||
| -rw-r--r-- | bot/exts/utils/clean.py | 2 | ||||
| -rw-r--r-- | bot/exts/utils/utils.py | 137 | ||||
| -rw-r--r-- | bot/log.py | 5 | ||||
| -rw-r--r-- | config-default.yml | 3 | ||||
| -rw-r--r-- | tests/bot/exts/info/test_information.py | 2 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_silence.py | 15 | ||||
| -rw-r--r-- | tests/bot/test_api.py | 8 | ||||
| -rw-r--r-- | tests/helpers.py | 2 | 
22 files changed, 567 insertions, 554 deletions
| diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6152f1543..6c97e8784 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,3 +55,5 @@ jobs:            tags: |              ghcr.io/python-discord/bot:latest              ghcr.io/python-discord/bot:${{ steps.sha_tag.outputs.tag }} +          build-args: | +            git_sha=${{ github.sha }} diff --git a/.github/workflows/sentry_release.yml b/.github/workflows/sentry_release.yml new file mode 100644 index 000000000..b8d92e90a --- /dev/null +++ b/.github/workflows/sentry_release.yml @@ -0,0 +1,24 @@ +name: Create Sentry release + +on: +  push: +    branches: +      - master + +jobs: +  create_sentry_release: +    runs-on: ubuntu-latest +    steps: +      - name: Checkout code +        uses: actions/checkout@master + +      - name: Create a Sentry.io release +        uses: tclindner/[email protected] +        env: +          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} +          SENTRY_ORG: python-discord +          SENTRY_PROJECT: bot +        with: +          tagName: ${{ github.sha }} +          environment: production +          releaseNamePrefix: bot@ diff --git a/Dockerfile b/Dockerfile index 06a538b2a..5d0380b44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,19 @@  FROM python:3.8-slim +# Define Git SHA build argument +ARG git_sha="development" +  # Set pip to have cleaner logs and no saved cache  ENV PIP_NO_CACHE_DIR=false \      PIPENV_HIDE_EMOJIS=1 \      PIPENV_IGNORE_VIRTUALENVS=1 \ -    PIPENV_NOSPIN=1 +    PIPENV_NOSPIN=1 \ +    GIT_SHA=$git_sha + +RUN apt-get -y update \ +    && apt-get install -y \ +        git \ +    && rm -rf /var/lib/apt/lists/*  # Install pipenv  RUN pip install -U pipenv @@ -14,16 +14,16 @@ beautifulsoup4 = "~=4.9"  colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"}  coloredlogs = "~=14.0"  deepdiff = "~=4.0" -"discord.py" = "~=1.5.0" +"discord.py" = {git = "https://github.com/Rapptz/discord.py.git", ref = "93f102ca907af6722ee03638766afd53dfe93a7f"}  feedparser = "~=5.2"  fuzzywuzzy = "~=0.17"  lxml = "~=4.4" -markdownify = "~=0.4" +markdownify = "==0.5.3"  more_itertools = "~=8.2"  python-dateutil = "~=2.8"  pyyaml = "~=5.1"  requests = "~=2.22" -sentry-sdk = "~=0.14" +sentry-sdk = "~=0.19"  sphinx = "~=2.2"  statsd = "~=3.3"  emoji = "~=0.6" diff --git a/Pipfile.lock b/Pipfile.lock index 541db1627..085d3d829 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "3ccb368599709d2970f839fc3721cfeebcd5a2700fed7231b2ce38a080828325" +            "sha256": "1ba637e521c654a23bcc82950e155f5366219eae00bbf809170a371122961a4f"          },          "pipfile-spec": 6,          "requires": { @@ -34,22 +34,46 @@          },          "aiohttp": {              "hashes": [ -                "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", -                "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", -                "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", -                "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", -                "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", -                "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", -                "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", -                "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", -                "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", -                "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", -                "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", -                "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", -                "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" +                "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", +                "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", +                "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", +                "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", +                "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", +                "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", +                "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", +                "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", +                "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", +                "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", +                "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", +                "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", +                "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", +                "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", +                "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", +                "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", +                "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", +                "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", +                "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", +                "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", +                "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", +                "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", +                "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", +                "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", +                "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", +                "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", +                "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", +                "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", +                "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", +                "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", +                "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", +                "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", +                "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", +                "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", +                "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", +                "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", +                "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"              ],              "index": "pypi", -            "version": "==3.6.3" +            "version": "==3.7.3"          },          "aioping": {              "hashes": [ @@ -129,51 +153,51 @@          },          "certifi": {              "hashes": [ -                "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", -                "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" +                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", +                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"              ], -            "version": "==2020.11.8" +            "version": "==2020.12.5"          },          "cffi": {              "hashes": [ -                "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", -                "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", -                "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", -                "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", -                "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", -                "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", -                "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", -                "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", -                "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", -                "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", -                "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", -                "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", -                "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", -                "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", -                "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", -                "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", -                "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", -                "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", -                "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", -                "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", -                "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", -                "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", -                "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", -                "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", -                "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", -                "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", -                "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", -                "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", -                "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", -                "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", -                "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", -                "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", -                "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", -                "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", -                "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", -                "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" -            ], -            "version": "==1.14.3" +                "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", +                "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", +                "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", +                "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", +                "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", +                "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", +                "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", +                "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", +                "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", +                "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", +                "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", +                "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", +                "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", +                "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", +                "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", +                "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", +                "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", +                "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", +                "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", +                "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", +                "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", +                "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", +                "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", +                "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", +                "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", +                "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", +                "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", +                "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", +                "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", +                "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", +                "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", +                "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", +                "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", +                "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", +                "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", +                "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" +            ], +            "version": "==1.14.4"          },          "chardet": {              "hashes": [ @@ -192,11 +216,11 @@          },          "coloredlogs": {              "hashes": [ -                "sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a", -                "sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505" +                "sha256:7ef1a7219870c7f02c218a2f2877ce68f2f8e087bb3a55bd6fbaa2a4362b4d52", +                "sha256:e244a892f9d97ffd2c60f15bf1d2582ef7f9ac0f848d132249004184785702b3"              ],              "index": "pypi", -            "version": "==14.0" +            "version": "==14.3"          },          "deepdiff": {              "hashes": [ @@ -206,13 +230,13 @@              "index": "pypi",              "version": "==4.3.2"          }, +        "discord-py": { +            "git": "https://github.com/Rapptz/discord.py.git", +            "ref": "93f102ca907af6722ee03638766afd53dfe93a7f" +        },          "discord.py": { -            "hashes": [ -                "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563", -                "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b" -            ], -            "index": "pypi", -            "version": "==1.5.1" +            "git": "https://github.com/Rapptz/discord.py.git", +            "ref": "93f102ca907af6722ee03638766afd53dfe93a7f"          },          "docutils": {              "hashes": [ @@ -231,10 +255,10 @@          },          "fakeredis": {              "hashes": [ -                "sha256:8070b7fce16f828beaef2c757a4354af91698685d5232404f1aeeb233529c7a5", -                "sha256:f8c8ea764d7b6fd801e7f5486e3edd32ca991d506186f1923a01fc072e33c271" +                "sha256:01cb47d2286825a171fb49c0e445b1fa9307087e07cbb3d027ea10dbff108b6a", +                "sha256:2c6041cf0225889bc403f3949838b2c53470a95a9e2d4272422937786f5f8f73"              ], -            "version": "==1.4.4" +            "version": "==1.4.5"          },          "feedparser": {              "hashes": [ @@ -307,11 +331,11 @@          },          "humanfriendly": {              "hashes": [ -                "sha256:bf52ec91244819c780341a3438d5d7b09f431d3f113a475147ac9b7b167a3d12", -                "sha256:e78960b31198511f45fd455534ae7645a6207d33e512d2e842c766d15d9c8080" +                "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": "==8.2" +            "version": "==9.1"          },          "idna": {              "hashes": [ @@ -339,46 +363,46 @@          },          "lxml": {              "hashes": [ -                "sha256:098fb713b31050463751dcc694878e1d39f316b86366fb9fe3fbbe5396ac9fab", -                "sha256:0e89f5d422988c65e6936e4ec0fe54d6f73f3128c80eb7ecc3b87f595523607b", -                "sha256:189ad47203e846a7a4951c17694d845b6ade7917c47c64b29b86526eefc3adf5", -                "sha256:1d87936cb5801c557f3e981c9c193861264c01209cb3ad0964a16310ca1b3301", -                "sha256:211b3bcf5da70c2d4b84d09232534ad1d78320762e2c59dedc73bf01cb1fc45b", -                "sha256:2358809cc64394617f2719147a58ae26dac9e21bae772b45cfb80baa26bfca5d", -                "sha256:23c83112b4dada0b75789d73f949dbb4e8f29a0a3511647024a398ebd023347b", -                "sha256:24e811118aab6abe3ce23ff0d7d38932329c513f9cef849d3ee88b0f848f2aa9", -                "sha256:2d5896ddf5389560257bbe89317ca7bcb4e54a02b53a3e572e1ce4226512b51b", -                "sha256:2d6571c48328be4304aee031d2d5046cbc8aed5740c654575613c5a4f5a11311", -                "sha256:2e311a10f3e85250910a615fe194839a04a0f6bc4e8e5bb5cac221344e3a7891", -                "sha256:302160eb6e9764168e01d8c9ec6becddeb87776e81d3fcb0d97954dd51d48e0a", -                "sha256:3a7a380bfecc551cfd67d6e8ad9faa91289173bdf12e9cfafbd2bdec0d7b1ec1", -                "sha256:3d9b2b72eb0dbbdb0e276403873ecfae870599c83ba22cadff2db58541e72856", -                "sha256:475325e037fdf068e0c2140b818518cf6bc4aa72435c407a798b2db9f8e90810", -                "sha256:4b7572145054330c8e324a72d808c8c8fbe12be33368db28c39a255ad5f7fb51", -                "sha256:4fff34721b628cce9eb4538cf9a73d02e0f3da4f35a515773cce6f5fe413b360", -                "sha256:56eff8c6fb7bc4bcca395fdff494c52712b7a57486e4fbde34c31bb9da4c6cc4", -                "sha256:573b2f5496c7e9f4985de70b9bbb4719ffd293d5565513e04ac20e42e6e5583f", -                "sha256:7ecaef52fd9b9535ae5f01a1dd2651f6608e4ec9dc136fc4dfe7ebe3c3ddb230", -                "sha256:803a80d72d1f693aa448566be46ffd70882d1ad8fc689a2e22afe63035eb998a", -                "sha256:8862d1c2c020cb7a03b421a9a7b4fe046a208db30994fc8ff68c627a7915987f", -                "sha256:9b06690224258db5cd39a84e993882a6874676f5de582da57f3df3a82ead9174", -                "sha256:a71400b90b3599eb7bf241f947932e18a066907bf84617d80817998cee81e4bf", -                "sha256:bb252f802f91f59767dcc559744e91efa9df532240a502befd874b54571417bd", -                "sha256:be1ebf9cc25ab5399501c9046a7dcdaa9e911802ed0e12b7d620cd4bbf0518b3", -                "sha256:be7c65e34d1b50ab7093b90427cbc488260e4b3a38ef2435d65b62e9fa3d798a", -                "sha256:c0dac835c1a22621ffa5e5f999d57359c790c52bbd1c687fe514ae6924f65ef5", -                "sha256:c152b2e93b639d1f36ec5a8ca24cde4a8eefb2b6b83668fcd8e83a67badcb367", -                "sha256:d182eada8ea0de61a45a526aa0ae4bcd222f9673424e65315c35820291ff299c", -                "sha256:d18331ea905a41ae71596502bd4c9a2998902328bbabd29e3d0f5f8569fabad1", -                "sha256:d20d32cbb31d731def4b1502294ca2ee99f9249b63bc80e03e67e8f8e126dea8", -                "sha256:d4ad7fd3269281cb471ad6c7bafca372e69789540d16e3755dd717e9e5c9d82f", -                "sha256:d6f8c23f65a4bfe4300b85f1f40f6c32569822d08901db3b6454ab785d9117cc", -                "sha256:d84d741c6e35c9f3e7406cb7c4c2e08474c2a6441d59322a00dcae65aac6315d", -                "sha256:e65c221b2115a91035b55a593b6eb94aa1206fa3ab374f47c6dc10d364583ff9", -                "sha256:f98b6f256be6cec8dd308a8563976ddaff0bdc18b730720f6f4bee927ffe926f" +                "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d", +                "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37", +                "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01", +                "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2", +                "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644", +                "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75", +                "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80", +                "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2", +                "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780", +                "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98", +                "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308", +                "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf", +                "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388", +                "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d", +                "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3", +                "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8", +                "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af", +                "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2", +                "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e", +                "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939", +                "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03", +                "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d", +                "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a", +                "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5", +                "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a", +                "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711", +                "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf", +                "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089", +                "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505", +                "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b", +                "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f", +                "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc", +                "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e", +                "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931", +                "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc", +                "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe", +                "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"              ],              "index": "pypi", -            "version": "==4.6.1" +            "version": "==4.6.2"          },          "markdownify": {              "hashes": [ @@ -437,26 +461,46 @@          },          "multidict": {              "hashes": [ -                "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", -                "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", -                "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", -                "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", -                "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", -                "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", -                "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", -                "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", -                "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", -                "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", -                "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", -                "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", -                "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", -                "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", -                "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", -                "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", -                "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" +                "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", +                "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", +                "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", +                "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", +                "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", +                "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", +                "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", +                "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", +                "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", +                "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", +                "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", +                "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", +                "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", +                "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", +                "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", +                "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", +                "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", +                "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", +                "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", +                "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", +                "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", +                "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", +                "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", +                "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", +                "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", +                "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", +                "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", +                "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", +                "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", +                "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", +                "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", +                "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", +                "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", +                "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", +                "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", +                "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", +                "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"              ], -            "markers": "python_version >= '3.5'", -            "version": "==4.7.6" +            "markers": "python_version >= '3.6'", +            "version": "==5.1.0"          },          "ordered-set": {              "hashes": [ @@ -467,11 +511,11 @@          },          "packaging": {              "hashes": [ -                "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", -                "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" +                "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", +                "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.4" +            "version": "==20.8"          },          "pamqp": {              "hashes": [ @@ -524,11 +568,11 @@          },          "pygments": {              "hashes": [ -                "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", -                "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" +                "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", +                "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"              ],              "markers": "python_version >= '3.5'", -            "version": "==2.7.2" +            "version": "==2.7.3"          },          "pyparsing": {              "hashes": [ @@ -555,18 +599,18 @@          },          "pyyaml": {              "hashes": [ -                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", -                "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", -                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", +                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",                  "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", +                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",                  "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",                  "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", -                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", -                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", +                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",                  "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", -                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", -                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",                  "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", +                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", +                "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", +                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", +                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",                  "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"              ],              "index": "pypi", @@ -582,19 +626,19 @@          },          "requests": {              "hashes": [ -                "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", -                "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" +                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", +                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"              ],              "index": "pypi", -            "version": "==2.25.0" +            "version": "==2.25.1"          },          "sentry-sdk": {              "hashes": [ -                "sha256:1052f0ed084e532f66cb3e4ba617960d820152aee8b93fc6c05bd53861768c1c", -                "sha256:4c42910a55a6b1fe694d5e4790d5188d105d77b5a6346c1c64cbea8c06c0e8b7" +                "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0", +                "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f"              ],              "index": "pypi", -            "version": "==0.19.4" +            "version": "==0.19.5"          },          "six": {              "hashes": [ @@ -620,11 +664,11 @@          },          "soupsieve": {              "hashes": [ -                "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55", -                "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232" +                "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851", +                "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e"              ],              "markers": "python_version >= '3.0'", -            "version": "==2.0.1" +            "version": "==2.1"          },          "sphinx": {              "hashes": [ @@ -690,6 +734,14 @@              "index": "pypi",              "version": "==3.3.0"          }, +        "typing-extensions": { +            "hashes": [ +                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", +                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", +                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" +            ], +            "version": "==3.7.4.3" +        },          "urllib3": {              "hashes": [                  "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", @@ -700,26 +752,46 @@          },          "yarl": {              "hashes": [ -                "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", -                "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", -                "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", -                "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", -                "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", -                "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", -                "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", -                "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", -                "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", -                "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", -                "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", -                "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", -                "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", -                "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", -                "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", -                "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", -                "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" +                "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", +                "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", +                "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", +                "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", +                "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", +                "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", +                "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", +                "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", +                "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", +                "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", +                "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", +                "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", +                "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", +                "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", +                "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", +                "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", +                "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", +                "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", +                "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", +                "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", +                "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", +                "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", +                "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", +                "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", +                "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", +                "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", +                "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", +                "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", +                "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", +                "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", +                "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", +                "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", +                "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", +                "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", +                "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", +                "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", +                "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"              ], -            "markers": "python_version >= '3.5'", -            "version": "==1.5.1" +            "markers": "python_version >= '3.6'", +            "version": "==1.6.3"          }      },      "develop": { @@ -740,10 +812,10 @@          },          "certifi": {              "hashes": [ -                "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", -                "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" +                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", +                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"              ], -            "version": "==2020.11.8" +            "version": "==2020.12.5"          },          "cfgv": {              "hashes": [ @@ -846,11 +918,11 @@          },          "flake8-bugbear": {              "hashes": [ -                "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63", -                "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162" +                "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538", +                "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"              ],              "index": "pypi", -            "version": "==20.1.4" +            "version": "==20.11.1"          },          "flake8-docstrings": {              "hashes": [ @@ -885,11 +957,11 @@          },          "flake8-tidy-imports": {              "hashes": [ -                "sha256:62059ca07d8a4926b561d392cbab7f09ee042350214a25cf12823384a45d27dd", -                "sha256:c30b40337a2e6802ba3bb611c26611154a27e94c53fc45639e3e282169574fd3" +                "sha256:52e5f2f987d3d5597538d5941153409ebcab571635835b78f522c7bf03ca23bc", +                "sha256:76e36fbbfdc8e3c5017f9a216c2855a298be85bc0631e66777f4e6a07a859dc4"              ],              "index": "pypi", -            "version": "==4.1.0" +            "version": "==4.2.1"          },          "flake8-todo": {              "hashes": [ @@ -900,11 +972,11 @@          },          "identify": {              "hashes": [ -                "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12", -                "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513" +                "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", +                "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==1.5.9" +            "version": "==1.5.10"          },          "idna": {              "hashes": [ @@ -938,11 +1010,11 @@          },          "pre-commit": {              "hashes": [ -                "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315", -                "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6" +                "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", +                "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4"              ],              "index": "pypi", -            "version": "==2.8.2" +            "version": "==2.9.3"          },          "pycodestyle": {              "hashes": [ @@ -970,18 +1042,18 @@          },          "pyyaml": {              "hashes": [ -                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", -                "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", -                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", +                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",                  "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", +                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",                  "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",                  "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", -                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", -                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", +                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",                  "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", -                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", -                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",                  "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", +                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", +                "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", +                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", +                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",                  "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"              ],              "index": "pypi", @@ -989,11 +1061,11 @@          },          "requests": {              "hashes": [ -                "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", -                "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" +                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", +                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"              ],              "index": "pypi", -            "version": "==2.25.0" +            "version": "==2.25.1"          },          "six": {              "hashes": [ @@ -1028,11 +1100,11 @@          },          "virtualenv": {              "hashes": [ -                "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", -                "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" +                "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", +                "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.1.0" +            "version": "==20.2.2"          }      }  } diff --git a/bot/api.py b/bot/api.py index 4b8520582..d93f9f2ba 100644 --- a/bot/api.py +++ b/bot/api.py @@ -37,64 +37,27 @@ class APIClient:      session: Optional[aiohttp.ClientSession] = None      loop: asyncio.AbstractEventLoop = None -    def __init__(self, loop: asyncio.AbstractEventLoop, **kwargs): +    def __init__(self, **session_kwargs):          auth_headers = {              'Authorization': f"Token {Keys.site_api}"          } -        if 'headers' in kwargs: -            kwargs['headers'].update(auth_headers) +        if 'headers' in session_kwargs: +            session_kwargs['headers'].update(auth_headers)          else: -            kwargs['headers'] = auth_headers +            session_kwargs['headers'] = auth_headers -        self.session = None -        self.loop = loop - -        self._ready = asyncio.Event(loop=loop) -        self._creation_task = None -        self._default_session_kwargs = kwargs - -        self.recreate() +        # aiohttp will complain if APIClient gets instantiated outside a coroutine. Thankfully, we +        # don't and shouldn't need to do that, so we can avoid scheduling a task to create it. +        self.session = aiohttp.ClientSession(**session_kwargs)      @staticmethod      def _url_for(endpoint: str) -> str:          return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" -    async def _create_session(self, **session_kwargs) -> None: -        """ -        Create the aiohttp session with `session_kwargs` and set the ready event. - -        `session_kwargs` is merged with `_default_session_kwargs` and overwrites its values. -        If an open session already exists, it will first be closed. -        """ -        await self.close() -        self.session = aiohttp.ClientSession(**{**self._default_session_kwargs, **session_kwargs}) -        self._ready.set() -      async def close(self) -> None: -        """Close the aiohttp session and unset the ready event.""" -        if self.session: -            await self.session.close() - -        self._ready.clear() - -    def recreate(self, force: bool = False, **session_kwargs) -> None: -        """ -        Schedule the aiohttp session to be created with `session_kwargs` if it's been closed. - -        If `force` is True, the session will be recreated even if an open one exists. If a task to -        create the session is pending, it will be cancelled. - -        `session_kwargs` is merged with the kwargs given when the `APIClient` was created and -        overwrites those default kwargs. -        """ -        if force or self.session is None or self.session.closed: -            if force and self._creation_task: -                self._creation_task.cancel() - -            # Don't schedule a task if one is already in progress. -            if force or self._creation_task is None or self._creation_task.done(): -                self._creation_task = self.loop.create_task(self._create_session(**session_kwargs)) +        """Close the aiohttp session.""" +        await self.session.close()      async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool) -> None:          """Raise ResponseCodeError for non-OK response if an exception should be raised.""" @@ -108,8 +71,6 @@ class APIClient:      async def request(self, method: str, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> dict:          """Send an HTTP request to the site API and return the JSON response.""" -        await self._ready.wait() -          async with self.session.request(method.upper(), self._url_for(endpoint), **kwargs) as resp:              await self.maybe_raise_for_status(resp, raise_for_status)              return await resp.json() @@ -132,25 +93,9 @@ class APIClient:      async def delete(self, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> Optional[dict]:          """Site API DELETE.""" -        await self._ready.wait() -          async with self.session.delete(self._url_for(endpoint), **kwargs) as resp:              if resp.status == 204:                  return None              await self.maybe_raise_for_status(resp, raise_for_status)              return await resp.json() - - -def loop_is_running() -> bool: -    """ -    Determine if there is a running asyncio event loop. - -    This helps enable "call this when event loop is running" logic (see: Twisted's `callWhenRunning`), -    which is currently not provided by asyncio. -    """ -    try: -        asyncio.get_running_loop() -    except RuntimeError: -        return False -    return True diff --git a/bot/bot.py b/bot/bot.py index e00d355b6..d5f108575 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -33,7 +33,7 @@ class Bot(commands.Bot):          self.http_session: Optional[aiohttp.ClientSession] = None          self.redis_session = redis_session -        self.api_client = api.APIClient(loop=self.loop) +        self.api_client: Optional[api.APIClient] = None          self.filter_list_cache = defaultdict(dict)          self._connector = None @@ -81,46 +81,6 @@ class Bot(commands.Bot):          for item in full_cache:              self.insert_item_into_filter_list_cache(item) -    def _recreate(self) -> None: -        """Re-create the connector, aiohttp session, the APIClient and the Redis session.""" -        # Use asyncio for DNS resolution instead of threads so threads aren't spammed. -        # Doesn't seem to have any state with regards to being closed, so no need to worry? -        self._resolver = aiohttp.AsyncResolver() - -        # Its __del__ does send a warning but it doesn't always show up for some reason. -        if self._connector and not self._connector._closed: -            log.warning( -                "The previous connector was not closed; it will remain open and be overwritten" -            ) - -        if self.redis_session.closed: -            # If the RedisSession was somehow closed, we try to reconnect it -            # here. Normally, this shouldn't happen. -            self.loop.create_task(self.redis_session.connect()) - -        # Use AF_INET as its socket family to prevent HTTPS related problems both locally -        # and in production. -        self._connector = aiohttp.TCPConnector( -            resolver=self._resolver, -            family=socket.AF_INET, -        ) - -        # Client.login() will call HTTPClient.static_login() which will create a session using -        # this connector attribute. -        self.http.connector = self._connector - -        # Its __del__ does send a warning but it doesn't always show up for some reason. -        if self.http_session and not self.http_session.closed: -            log.warning( -                "The previous session was not closed; it will remain open and be overwritten" -            ) - -        self.http_session = aiohttp.ClientSession(connector=self._connector) -        self.api_client.recreate(force=True, connector=self._connector) - -        # Build the FilterList cache -        self.loop.create_task(self.cache_filter_list_data()) -      @classmethod      def create(cls) -> "Bot":          """Create and return an instance of a Bot.""" @@ -184,15 +144,8 @@ class Bot(commands.Bot):          return command      def clear(self) -> None: -        """ -        Clears the internal state of the bot and recreates the connector and sessions. - -        Will cause a DeprecationWarning if called outside a coroutine. -        """ -        # Because discord.py recreates the HTTPClient session, may as well follow suit and recreate -        # our own stuff here too. -        self._recreate() -        super().clear() +        """Not implemented! Re-instantiate the bot instead of attempting to re-use a closed one.""" +        raise NotImplementedError("Re-using a Bot object after closing it is not supported.")      async def close(self) -> None:          """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" @@ -212,7 +165,8 @@ class Bot(commands.Bot):          # Now actually do full close of bot          await super().close() -        await self.api_client.close() +        if self.api_client: +            await self.api_client.close()          if self.http_session:              await self.http_session.close() @@ -247,7 +201,31 @@ class Bot(commands.Bot):      async def login(self, *args, **kwargs) -> None:          """Re-create the connector and set up sessions before logging into Discord.""" -        self._recreate() +        # Use asyncio for DNS resolution instead of threads so threads aren't spammed. +        self._resolver = aiohttp.AsyncResolver() + +        # Use AF_INET as its socket family to prevent HTTPS related problems both locally +        # and in production. +        self._connector = aiohttp.TCPConnector( +            resolver=self._resolver, +            family=socket.AF_INET, +        ) + +        # Client.login() will call HTTPClient.static_login() which will create a session using +        # this connector attribute. +        self.http.connector = self._connector + +        self.http_session = aiohttp.ClientSession(connector=self._connector) +        self.api_client = api.APIClient(connector=self._connector) + +        if self.redis_session.closed: +            # If the RedisSession was somehow closed, we try to reconnect it +            # here. Normally, this shouldn't happen. +            await self.redis_session.connect() + +        # Build the FilterList cache +        await self.cache_filter_list_data() +          await self.stats.create_socket()          await super().login(*args, **kwargs) diff --git a/bot/constants.py b/bot/constants.py index 08ae0d52f..6bfda160b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -493,6 +493,7 @@ class Keys(metaclass=YAMLGetter):      section = "keys"      site_api: Optional[str] +    github: Optional[str]  class URLs(metaclass=YAMLGetter): @@ -606,7 +607,7 @@ class Verification(metaclass=YAMLGetter):  class VoiceGate(metaclass=YAMLGetter):      section = "voice_gate" -    minimum_days_verified: int +    minimum_days_member: int      minimum_messages: int      bot_message_delete_delay: int      minimum_activity_blocks: int @@ -656,6 +657,9 @@ MODERATION_CHANNELS = Guild.moderation_channels  # Category combinations  MODERATION_CATEGORIES = Guild.moderation_categories +# Git SHA for Sentry +GIT_SHA = os.environ.get("GIT_SHA", "development") +  # Bot replies  NEGATIVE_REPLIES = [      "Noooooo!!", diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 187950689..2057876e4 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -6,7 +6,6 @@ from collections import Counter, defaultdict  from string import Template  from typing import Any, Mapping, Optional, Tuple, Union -from dateutil import parser  from discord import ChannelType, Colour, Embed, Guild, Message, Role, Status, utils  from discord.abc import GuildChannel  from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role @@ -225,14 +224,14 @@ class Information(Cog):              if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}", None)):                  badges.append(emoji) -        verified_at, activity = await self.user_verification_and_messages(user) +        activity = await self.user_messages(user)          if on_server:              joined = time_since(user.joined_at, max_units=3)              roles = ", ".join(role.mention for role in user.roles[1:]) -            membership = {"Joined": joined, "Verified": verified_at or "False", "Roles": roles or None} +            membership = {"Joined": joined, "Pending": user.pending, "Roles": roles or None}              if not is_mod_channel(ctx.channel): -                membership.pop("Verified") +                membership.pop("Pending")              membership = textwrap.dedent("\n".join([f"{key}: {value}" for key, value in membership.items()]))          else: @@ -360,30 +359,21 @@ class Information(Cog):          return "Nominations", "\n".join(output) -    async def user_verification_and_messages(self, user: FetchedMember) -> Tuple[Union[bool, str], Tuple[str, str]]: +    async def user_messages(self, user: FetchedMember) -> Tuple[Union[bool, str], Tuple[str, str]]:          """ -        Gets the time of verification and amount of messages for `member`. +        Gets the amount of messages for `member`.          Fetches information from the metricity database that's hosted by the site.          If the database returns a code besides a 404, then many parts of the bot are broken including this one.          """          activity_output = [] -        verified_at = False          try:              user_activity = await self.bot.api_client.get(f"bot/users/{user.id}/metricity_data")          except ResponseCodeError as e:              if e.status == 404:                  activity_output = "No activity" -          else: -            try: -                if (verified_at := user_activity["verified_at"]) is not None: -                    verified_at = time_since(parser.isoparse(verified_at), max_units=3) -            except ValueError: -                log.warning(f"Could not parse ISO string correctly for user {user.id} verification date.") -                verified_at = None -              activity_output.append(user_activity["total_messages"] or "No messages")              activity_output.append(user_activity["activity_blocks"] or "No activity") @@ -391,7 +381,7 @@ class Information(Cog):                  f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output)              ) -        return verified_at, ("Activity", activity_output) +        return ("Activity", activity_output)      def format_fields(self, mapping: Mapping[str, Any], field_width: Optional[int] = None) -> str:          """Format a mapping to be readable to a human.""" diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py new file mode 100644 index 000000000..8ac96bbdb --- /dev/null +++ b/bot/exts/info/pep.py @@ -0,0 +1,164 @@ +import logging +from datetime import datetime, timedelta +from email.parser import HeaderParser +from io import StringIO +from typing import Dict, Optional, Tuple + +from discord import Colour, Embed +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import Keys +from bot.utils.cache import AsyncCache + +log = logging.getLogger(__name__) + +ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" +BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" +PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" + +pep_cache = AsyncCache() + +GITHUB_API_HEADERS = {} +if Keys.github: +    GITHUB_API_HEADERS["Authorization"] = f"token {Keys.github}" + + +class PythonEnhancementProposals(Cog): +    """Cog for displaying information about PEPs.""" + +    def __init__(self, bot: Bot): +        self.bot = bot +        self.peps: Dict[int, str] = {} +        # To avoid situations where we don't have last datetime, set this to now. +        self.last_refreshed_peps: datetime = datetime.now() +        self.bot.loop.create_task(self.refresh_peps_urls()) + +    async def refresh_peps_urls(self) -> None: +        """Refresh PEP URLs listing in every 3 hours.""" +        # Wait until HTTP client is available +        await self.bot.wait_until_ready() +        log.trace("Started refreshing PEP URLs.") +        self.last_refreshed_peps = datetime.now() + +        async with self.bot.http_session.get( +            PEPS_LISTING_API_URL, +            headers=GITHUB_API_HEADERS +        ) as resp: +            if resp.status != 200: +                log.warning(f"Fetching PEP URLs from GitHub API failed with code {resp.status}") +                return + +            listing = await resp.json() + +        log.trace("Got PEP URLs listing from GitHub API") + +        for file in listing: +            name = file["name"] +            if name.startswith("pep-") and name.endswith((".rst", ".txt")): +                pep_number = name.replace("pep-", "").split(".")[0] +                self.peps[int(pep_number)] = file["download_url"] + +        log.info("Successfully refreshed PEP URLs listing.") + +    @staticmethod +    def get_pep_zero_embed() -> Embed: +        """Get information embed about PEP 0.""" +        pep_embed = Embed( +            title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", +            url="https://www.python.org/dev/peps/" +        ) +        pep_embed.set_thumbnail(url=ICON_URL) +        pep_embed.add_field(name="Status", value="Active") +        pep_embed.add_field(name="Created", value="13-Jul-2000") +        pep_embed.add_field(name="Type", value="Informational") + +        return pep_embed + +    async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: +        """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" +        if ( +            pep_nr not in self.peps +            and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() +            and len(str(pep_nr)) < 5 +        ): +            await self.refresh_peps_urls() + +        if pep_nr not in self.peps: +            log.trace(f"PEP {pep_nr} was not found") +            return Embed( +                title="PEP not found", +                description=f"PEP {pep_nr} does not exist.", +                colour=Colour.red() +            ) + +        return None + +    def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: +        """Generate PEP embed based on PEP headers data.""" +        # Assemble the embed +        pep_embed = Embed( +            title=f"**PEP {pep_nr} - {pep_header['Title']}**", +            description=f"[Link]({BASE_PEP_URL}{pep_nr:04})", +        ) + +        pep_embed.set_thumbnail(url=ICON_URL) + +        # Add the interesting information +        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]) + +        return pep_embed + +    @pep_cache(arg_offset=1) +    async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: +        """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" +        response = await self.bot.http_session.get(self.peps[pep_nr]) + +        if response.status == 200: +            log.trace(f"PEP {pep_nr} found") +            pep_content = await response.text() + +            # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 +            pep_header = HeaderParser().parse(StringIO(pep_content)) +            return self.generate_pep_embed(pep_header, pep_nr), True +        else: +            log.trace( +                f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." +            ) +            return Embed( +                title="Unexpected error", +                description="Unexpected HTTP error during PEP search. Please let us know.", +                colour=Colour.red() +            ), False + +    @command(name='pep', aliases=('get_pep', 'p')) +    async def pep_command(self, ctx: Context, pep_number: int) -> None: +        """Fetches information about a PEP and sends it to the channel.""" +        # Trigger typing in chat to show users that bot is responding +        await ctx.trigger_typing() + +        # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. +        if pep_number == 0: +            pep_embed = self.get_pep_zero_embed() +            success = True +        else: +            success = False +            if not (pep_embed := await self.validate_pep_number(pep_number)): +                pep_embed, success = await self.get_pep_embed(pep_number) + +        await ctx.send(embed=pep_embed) +        if success: +            log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") +            self.bot.stats.incr(f"pep_fetches.{pep_number}") +        else: +            log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") + + +def setup(bot: Bot) -> None: +    """Load the PEP cog.""" +    bot.add_cog(PythonEnhancementProposals(bot)) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index e6712b3b6..a942d5294 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -93,7 +93,7 @@ class Silence(commands.Cog):          await self.bot.wait_until_guild_available()          guild = self.bot.get_guild(Guild.id) -        self._verified_role = guild.get_role(Roles.verified) +        self._everyone_role = guild.default_role          self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts)          self.notifier = SilenceNotifier(self.bot.get_channel(Channels.mod_log))          await self._reschedule() @@ -142,7 +142,7 @@ class Silence(commands.Cog):      async def _unsilence_wrapper(self, channel: TextChannel) -> None:          """Unsilence `channel` and send a success/failure message."""          if not await self._unsilence(channel): -            overwrite = channel.overwrites_for(self._verified_role) +            overwrite = channel.overwrites_for(self._everyone_role)              if overwrite.send_messages is False or overwrite.add_reactions is False:                  await channel.send(MSG_UNSILENCE_MANUAL)              else: @@ -152,14 +152,14 @@ class Silence(commands.Cog):      async def _set_silence_overwrites(self, channel: TextChannel) -> bool:          """Set silence permission overwrites for `channel` and return True if successful.""" -        overwrite = channel.overwrites_for(self._verified_role) +        overwrite = channel.overwrites_for(self._everyone_role)          prev_overwrites = dict(send_messages=overwrite.send_messages, add_reactions=overwrite.add_reactions)          if channel.id in self.scheduler or all(val is False for val in prev_overwrites.values()):              return False          overwrite.update(send_messages=False, add_reactions=False) -        await channel.set_permissions(self._verified_role, overwrite=overwrite) +        await channel.set_permissions(self._everyone_role, overwrite=overwrite)          await self.previous_overwrites.set(channel.id, json.dumps(prev_overwrites))          return True @@ -188,14 +188,14 @@ class Silence(commands.Cog):              log.info(f"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.")              return False -        overwrite = channel.overwrites_for(self._verified_role) +        overwrite = channel.overwrites_for(self._everyone_role)          if prev_overwrites is None:              log.info(f"Missing previous overwrites for #{channel} ({channel.id}); defaulting to None.")              overwrite.update(send_messages=None, add_reactions=None)          else:              overwrite.update(**json.loads(prev_overwrites)) -        await channel.set_permissions(self._verified_role, overwrite=overwrite) +        await channel.set_permissions(self._everyone_role, overwrite=overwrite)          log.info(f"Unsilenced channel #{channel} ({channel.id}).")          self.scheduler.cancel(channel.id) @@ -207,7 +207,7 @@ class Silence(commands.Cog):              await self._mod_alerts_channel.send(                  f"<@&{Roles.admins}> Restored overwrites with default values after unsilencing "                  f"{channel.mention}. Please check that the `Send Messages` and `Add Reactions` " -                f"overwrites for {self._verified_role.mention} are at their desired values." +                f"overwrites for {self._everyone_role.mention} are at their desired values."              )          return True diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 7aa559617..ce91dcb15 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -55,7 +55,7 @@ If you'd like to unsubscribe from the announcement notifications, simply send `!  """  ALTERNATE_VERIFIED_MESSAGE = f""" -Thanks for accepting our rules! +You are now verified!  You can find a copy of our rules for reference at <https://pythondiscord.com/pages/rules>. @@ -174,14 +174,13 @@ class Verification(Cog):      # ]      task_cache = RedisCache() -    # Create a cache for storing recipients of the alternate welcome DM. -    member_gating_cache = RedisCache() -      def __init__(self, bot: Bot) -> None:          """Start internal tasks."""          self.bot = bot          self.bot.loop.create_task(self._maybe_start_tasks()) +        self.pending_members = set() +      def cog_unload(self) -> None:          """          Cancel internal tasks. @@ -568,19 +567,8 @@ class Verification(Cog):          # If the user has the pending flag set, they will be using the alternate          # gate and will not need a welcome DM with verification instructions.          # We will send them an alternate DM once they verify with the welcome -        # video. +        # video when they pass the gate.          if raw_member.get("pending"): -            await self.member_gating_cache.set(member.id, True) - -            # TODO: Temporary, remove soon after asking joe. -            await self.mod_log.send_log_message( -                icon_url=self.bot.user.avatar_url, -                colour=discord.Colour.blurple(), -                title="New native gated user", -                channel_id=constants.Channels.user_log, -                text=f"<@{member.id}> ({member.id})", -            ) -              return          log.trace(f"Sending on join message to new member: {member.id}") @@ -592,19 +580,15 @@ class Verification(Cog):      @Cog.listener()      async def on_member_update(self, before: discord.Member, after: discord.Member) -> None:          """Check if we need to send a verification DM to a gated user.""" -        before_roles = [role.id for role in before.roles] -        after_roles = [role.id for role in after.roles] - -        if constants.Roles.verified not in before_roles and constants.Roles.verified in after_roles: -            if await self.member_gating_cache.pop(after.id): -                try: -                    # If the member has not received a DM from our !accept command -                    # and has gone through the alternate gating system we should send -                    # our alternate welcome DM which includes info such as our welcome -                    # video. -                    await safe_dm(after.send(ALTERNATE_VERIFIED_MESSAGE)) -                except discord.HTTPException: -                    log.exception("DM dispatch failed on unexpected error code") +        if before.pending is True and after.pending is False: +            try: +                # If the member has not received a DM from our !accept command +                # and has gone through the alternate gating system we should send +                # our alternate welcome DM which includes info such as our welcome +                # video. +                await safe_dm(after.send(ALTERNATE_VERIFIED_MESSAGE)) +            except discord.HTTPException: +                log.exception("DM dispatch failed on unexpected error code")      @Cog.listener()      async def on_message(self, message: discord.Message) -> None: @@ -850,19 +834,21 @@ class Verification(Cog):      @command(name='verify')      @has_any_role(*constants.MODERATION_ROLES) -    async def apply_developer_role(self, ctx: Context, user: discord.Member) -> None: -        """Command for moderators to apply the Developer role to any user.""" +    async def perform_manual_verification(self, ctx: Context, user: discord.Member) -> None: +        """Command for moderators to verify any user."""          log.trace(f'verify command called by {ctx.author} for {user.id}.') -        developer_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.verified) -        if developer_role in user.roles: -            log.trace(f'{user.id} is already a developer, aborting.') -            await ctx.send(f'{constants.Emojis.cross_mark} {user} is already a developer.') +        if not user.pending: +            log.trace(f'{user.id} is already verified, aborting.') +            await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already verified.')              return -        await user.add_roles(developer_role) -        log.trace(f'Developer role successfully applied to {user.id}') -        await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user}.') +        # Adding a role automatically verifies the user, so we add and remove the Announcements role. +        temporary_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.announcements) +        await user.add_roles(temporary_role) +        await user.remove_roles(temporary_role) +        log.trace(f'{user.id} manually verified.') +        await ctx.send(f'{constants.Emojis.check_mark} {user.mention} is now verified.')      # endregion diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index 4d48d2c1b..0cbce6a51 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -5,7 +5,6 @@ from datetime import datetime, timedelta  import discord  from async_rediscache import RedisCache -from dateutil import parser  from discord import Colour, Member, VoiceState  from discord.ext.commands import Cog, Context, command @@ -29,7 +28,7 @@ FAILED_MESSAGE = (  )  MESSAGE_FIELD_MAP = { -    "verified_at": f"have been verified for less than {GateConf.minimum_days_verified} days", +    "joined_at": f"have been on the server for less than {GateConf.minimum_days_member} days",      "voice_banned": "have an active voice ban infraction",      "total_messages": f"have sent less than {GateConf.minimum_messages} messages",      "activity_blocks": f"have been active for fewer than {GateConf.minimum_activity_blocks} ten-minute blocks", @@ -149,14 +148,8 @@ class VoiceGate(Cog):              await ctx.author.send(embed=embed)              return -        # Pre-parse this for better code style -        if data["verified_at"] is not None: -            data["verified_at"] = parser.isoparse(data["verified_at"]) -        else: -            data["verified_at"] = datetime.utcnow() - timedelta(days=3) -          checks = { -            "verified_at": data["verified_at"] > datetime.utcnow() - timedelta(days=GateConf.minimum_days_verified), +            "joined_at": ctx.author.joined_at > datetime.utcnow() - timedelta(days=GateConf.minimum_days_member),              "total_messages": data["total_messages"] < GateConf.minimum_messages,              "voice_banned": data["voice_banned"],              "activity_blocks": data["activity_blocks"] < GateConf.minimum_activity_blocks diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py index 69d623581..a4c828f95 100644 --- a/bot/exts/utils/bot.py +++ b/bot/exts/utils/bot.py @@ -5,7 +5,7 @@ from discord import Embed, TextChannel  from discord.ext.commands import Cog, Context, command, group, has_any_role  from bot.bot import Bot -from bot.constants import Guild, MODERATION_ROLES, Roles, URLs +from bot.constants import Guild, MODERATION_ROLES, URLs  log = logging.getLogger(__name__) @@ -17,13 +17,11 @@ class BotCog(Cog, name="Bot"):          self.bot = bot      @group(invoke_without_command=True, name="bot", hidden=True) -    @has_any_role(Roles.verified)      async def botinfo_group(self, ctx: Context) -> None:          """Bot informational commands."""          await ctx.send_help(ctx.command)      @botinfo_group.command(name='about', aliases=('info',), hidden=True) -    @has_any_role(Roles.verified)      async def about_command(self, ctx: Context) -> None:          """Get information about the bot."""          embed = Embed( diff --git a/bot/exts/utils/clean.py b/bot/exts/utils/clean.py index bf25cb4c2..8acaf9131 100644 --- a/bot/exts/utils/clean.py +++ b/bot/exts/utils/clean.py @@ -191,7 +191,7 @@ class Clean(Cog):              channel_id=Channels.mod_log,          ) -    @group(invoke_without_command=True, name="clean", aliases=["purge"]) +    @group(invoke_without_command=True, name="clean", aliases=["clear", "purge"])      @has_any_role(*MODERATION_ROLES)      async def clean_group(self, ctx: Context) -> None:          """Commands for cleaning messages in channels.""" diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 8e7e6ba36..eb92dfca7 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -2,10 +2,7 @@ import difflib  import logging  import re  import unicodedata -from datetime import datetime, timedelta -from email.parser import HeaderParser -from io import StringIO -from typing import Dict, Optional, Tuple, Union +from typing import Tuple, Union  from discord import Colour, Embed, utils  from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role @@ -17,7 +14,6 @@ from bot.converters import Snowflake  from bot.decorators import in_whitelist  from bot.pagination import LinePaginator  from bot.utils import messages -from bot.utils.cache import AsyncCache  from bot.utils.time import time_since  log = logging.getLogger(__name__) @@ -44,23 +40,12 @@ If the implementation is easy to explain, it may be a good idea.  Namespaces are one honking great idea -- let's do more of those!  """ -ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" - -pep_cache = AsyncCache() -  class Utils(Cog):      """A selection of utilities which don't have a clear category.""" -    BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" -    BASE_GITHUB_PEP_URL = "https://raw.githubusercontent.com/python/peps/master/pep-" -    PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" -      def __init__(self, bot: Bot):          self.bot = bot -        self.peps: Dict[int, str] = {} -        self.last_refreshed_peps: Optional[datetime] = None -        self.bot.loop.create_task(self.refresh_peps_urls())      @command()      @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) @@ -207,126 +192,6 @@ class Utils(Cog):          for reaction in options:              await message.add_reaction(reaction) -    # region: PEP - -    async def refresh_peps_urls(self) -> None: -        """Refresh PEP URLs listing in every 3 hours.""" -        # Wait until HTTP client is available -        await self.bot.wait_until_ready() -        log.trace("Started refreshing PEP URLs.") - -        async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: -            listing = await resp.json() - -        log.trace("Got PEP URLs listing from GitHub API") - -        for file in listing: -            name = file["name"] -            if name.startswith("pep-") and name.endswith((".rst", ".txt")): -                pep_number = name.replace("pep-", "").split(".")[0] -                self.peps[int(pep_number)] = file["download_url"] - -        self.last_refreshed_peps = datetime.now() -        log.info("Successfully refreshed PEP URLs listing.") - -    @command(name='pep', aliases=('get_pep', 'p')) -    async def pep_command(self, ctx: Context, pep_number: int) -> None: -        """Fetches information about a PEP and sends it to the channel.""" -        # Trigger typing in chat to show users that bot is responding -        await ctx.trigger_typing() - -        # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. -        if pep_number == 0: -            pep_embed = self.get_pep_zero_embed() -            success = True -        else: -            success = False -            if not (pep_embed := await self.validate_pep_number(pep_number)): -                pep_embed, success = await self.get_pep_embed(pep_number) - -        await ctx.send(embed=pep_embed) -        if success: -            log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") -            self.bot.stats.incr(f"pep_fetches.{pep_number}") -        else: -            log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") - -    @staticmethod -    def get_pep_zero_embed() -> Embed: -        """Get information embed about PEP 0.""" -        pep_embed = Embed( -            title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", -            url="https://www.python.org/dev/peps/" -        ) -        pep_embed.set_thumbnail(url=ICON_URL) -        pep_embed.add_field(name="Status", value="Active") -        pep_embed.add_field(name="Created", value="13-Jul-2000") -        pep_embed.add_field(name="Type", value="Informational") - -        return pep_embed - -    async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: -        """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" -        if ( -            pep_nr not in self.peps -            and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() -            and len(str(pep_nr)) < 5 -        ): -            await self.refresh_peps_urls() - -        if pep_nr not in self.peps: -            log.trace(f"PEP {pep_nr} was not found") -            return Embed( -                title="PEP not found", -                description=f"PEP {pep_nr} does not exist.", -                colour=Colour.red() -            ) - -        return None - -    def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: -        """Generate PEP embed based on PEP headers data.""" -        # Assemble the embed -        pep_embed = Embed( -            title=f"**PEP {pep_nr} - {pep_header['Title']}**", -            description=f"[Link]({self.BASE_PEP_URL}{pep_nr:04})", -        ) - -        pep_embed.set_thumbnail(url=ICON_URL) - -        # Add the interesting information -        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]) - -        return pep_embed - -    @pep_cache(arg_offset=1) -    async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: -        """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" -        response = await self.bot.http_session.get(self.peps[pep_nr]) - -        if response.status == 200: -            log.trace(f"PEP {pep_nr} found") -            pep_content = await response.text() - -            # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 -            pep_header = HeaderParser().parse(StringIO(pep_content)) -            return self.generate_pep_embed(pep_header, pep_nr), True -        else: -            log.trace( -                f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." -            ) -            return Embed( -                title="Unexpected error", -                description="Unexpected HTTP error during PEP search. Please let us know.", -                colour=Colour.red() -            ), False -    # endregion -  def setup(bot: Bot) -> None:      """Load the Utils cog.""" diff --git a/bot/log.py b/bot/log.py index 13141de40..0935666d1 100644 --- a/bot/log.py +++ b/bot/log.py @@ -6,7 +6,6 @@ from pathlib import Path  import coloredlogs  import sentry_sdk -from sentry_sdk.integrations.aiohttp import AioHttpIntegration  from sentry_sdk.integrations.logging import LoggingIntegration  from sentry_sdk.integrations.redis import RedisIntegration @@ -67,9 +66,9 @@ def setup_sentry() -> None:          dsn=constants.Bot.sentry_dsn,          integrations=[              sentry_logging, -            AioHttpIntegration(),              RedisIntegration(), -        ] +        ], +        release=f"bot@{constants.GIT_SHA}"      ) diff --git a/config-default.yml b/config-default.yml index 006743342..ca89bb639 100644 --- a/config-default.yml +++ b/config-default.yml @@ -323,6 +323,7 @@ filter:  keys:      site_api:    !ENV "BOT_API_KEY" +    github:      !ENV "GITHUB_API_KEY"  urls: @@ -526,7 +527,7 @@ verification:  voice_gate: -    minimum_days_verified: 3  # How many days the user must have been verified for +    minimum_days_member: 3  # How many days the user must have been a member for      minimum_messages: 50  # How many messages a user must have to be eligible for voice      bot_message_delete_delay: 10  # Seconds before deleting bot's response in Voice Gate      minimum_activity_blocks: 3  # Number of 10 minute blocks during which a user must have been active diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 254b0a867..043cce8de 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -355,7 +355,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          self.assertEqual(              textwrap.dedent(f"""                  Joined: {"1 year ago"} -                Verified: {"False"} +                Pending: {"False"}                  Roles: &Moderators              """).strip(),              embed.fields[1].value diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 104293d8e..fa5fc9e81 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -117,15 +117,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase):          self.bot.get_guild.assert_called_once_with(Guild.id)      @autospec(silence, "SilenceNotifier", pass_mocks=False) -    async def test_async_init_got_role(self): -        """Got `Roles.verified` role from guild.""" -        guild = self.bot.get_guild() -        guild.get_role.side_effect = lambda id_: Mock(id=id_) - -        await self.cog._async_init() -        self.assertEqual(self.cog._verified_role.id, Roles.verified) - -    @autospec(silence, "SilenceNotifier", pass_mocks=False)      async def test_async_init_got_channels(self):          """Got channels from bot."""          self.bot.get_channel.side_effect = lambda id_: MockTextChannel(id=id_) @@ -302,7 +293,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          self.assertFalse(self.overwrite.send_messages)          self.assertFalse(self.overwrite.add_reactions)          self.channel.set_permissions.assert_awaited_once_with( -            self.cog._verified_role, +            self.cog._everyone_role,              overwrite=self.overwrite          ) @@ -435,7 +426,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase):          """Channel's `send_message` and `add_reactions` overwrites were restored."""          await self.cog._unsilence(self.channel)          self.channel.set_permissions.assert_awaited_once_with( -            self.cog._verified_role, +            self.cog._everyone_role,              overwrite=self.overwrite,          ) @@ -449,7 +440,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase):          await self.cog._unsilence(self.channel)          self.channel.set_permissions.assert_awaited_once_with( -            self.cog._verified_role, +            self.cog._everyone_role,              overwrite=self.overwrite,          ) diff --git a/tests/bot/test_api.py b/tests/bot/test_api.py index 99e942813..76bcb481d 100644 --- a/tests/bot/test_api.py +++ b/tests/bot/test_api.py @@ -13,14 +13,6 @@ class APIClientTests(unittest.IsolatedAsyncioTestCase):          cls.error_api_response = MagicMock()          cls.error_api_response.status = 999 -    def test_loop_is_not_running_by_default(self): -        """The event loop should not be running by default.""" -        self.assertFalse(api.loop_is_running()) - -    async def test_loop_is_running_in_async_context(self): -        """The event loop should be running in an async context.""" -        self.assertTrue(api.loop_is_running()) -      def test_response_code_error_default_initialization(self):          """Test the default initialization of `ResponseCodeError` without `text` or `json`"""          error = api.ResponseCodeError(response=self.error_api_response) diff --git a/tests/helpers.py b/tests/helpers.py index 870f66197..496363ae3 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -230,7 +230,7 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin      spec_set = member_instance      def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None: -        default_kwargs = {'name': 'member', 'id': next(self.discord_id), 'bot': False} +        default_kwargs = {'name': 'member', 'id': next(self.discord_id), 'bot': False, "pending": False}          super().__init__(**collections.ChainMap(kwargs, default_kwargs))          self.roles = [MockRole(name="@everyone", position=1, id=0)] | 
