diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 76153c873f..8a61d4006f 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,283 +1,326 @@ { "errors": [], - "generated_at": "2021-11-20T17:06:28Z", + "generated_at": "2022-09-06T16:19:31Z", "metrics": { "./bot.py": { - "CONFIDENCE.HIGH": 1.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 1406, - "nosec": 0 + "CONFIDENCE.HIGH": 1, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 1507, + "nosec": 0, + "skipped_tests": 0 }, "./cogs/modmail.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 1678, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 1837, + "nosec": 0, + "skipped_tests": 0 }, "./cogs/plugins.py": { - "CONFIDENCE.HIGH": 1.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, + "CONFIDENCE.HIGH": 1, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, "loc": 597, - "nosec": 0 + "nosec": 0, + "skipped_tests": 0 }, "./cogs/utility.py": { - "CONFIDENCE.HIGH": 2.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 1768, - "nosec": 0 + "CONFIDENCE.HIGH": 2, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 1, + "SEVERITY.UNDEFINED": 0, + "loc": 1794, + "nosec": 0, + "skipped_tests": 0 }, "./core/_color_data.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, "loc": 1166, - "nosec": 0 + "nosec": 0, + "skipped_tests": 0 }, "./core/changelog.py": { - "CONFIDENCE.HIGH": 1.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, + "CONFIDENCE.HIGH": 1, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, "loc": 159, - "nosec": 0 + "nosec": 0, + "skipped_tests": 0 }, "./core/checks.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, "loc": 105, - "nosec": 0 + "nosec": 0, + "skipped_tests": 0 }, "./core/clients.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 598, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 1, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 1, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 644, + "nosec": 0, + "skipped_tests": 0 }, "./core/config.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 375, - "nosec": 0 - }, - "./core/decorators.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 9, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 388, + "nosec": 0, + "skipped_tests": 0 }, "./core/models.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 204, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 210, + "nosec": 0, + "skipped_tests": 0 }, "./core/paginator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 209, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 312, + "nosec": 0, + "skipped_tests": 0 }, "./core/thread.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 1097, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 1184, + "nosec": 0, + "skipped_tests": 0 }, "./core/time.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 156, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 265, + "nosec": 0, + "skipped_tests": 0 }, "./core/utils.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 351, - "nosec": 0 + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 396, + "nosec": 0, + "skipped_tests": 0 + }, + "./plugins/Cordila/cord/jishaku-migration/jishaku.py": { + "CONFIDENCE.HIGH": 0, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 0, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 0, + "SEVERITY.MEDIUM": 0, + "SEVERITY.UNDEFINED": 0, + "loc": 2, + "nosec": 0, + "skipped_tests": 0 }, "_totals": { - "CONFIDENCE.HIGH": 5.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 5.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 9878, - "nosec": 0 + "CONFIDENCE.HIGH": 5, + "CONFIDENCE.LOW": 0, + "CONFIDENCE.MEDIUM": 1, + "CONFIDENCE.UNDEFINED": 0, + "SEVERITY.HIGH": 0, + "SEVERITY.LOW": 5, + "SEVERITY.MEDIUM": 1, + "SEVERITY.UNDEFINED": 0, + "loc": 10566, + "nosec": 0, + "skipped_tests": 0 } }, "results": [ { - "code": "13 from datetime import datetime\n14 from subprocess import PIPE\n15 from types import SimpleNamespace\n", + "code": "14 from datetime import datetime, timezone\n15 from subprocess import PIPE\n16 from types import SimpleNamespace\n", + "col_offset": 0, "filename": "./bot.py", "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, "issue_severity": "LOW", - "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 14, + "issue_text": "Consider possible security implications associated with the subprocess module.", + "line_number": 15, "line_range": [ - 14 + 15 ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { "code": "13 from site import USER_SITE\n14 from subprocess import PIPE\n15 \n16 import discord\n", + "col_offset": 0, "filename": "./cogs/plugins.py", "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, "issue_severity": "LOW", - "issue_text": "Consider possible security implications associated with PIPE module.", + "issue_text": "Consider possible security implications associated with the subprocess module.", "line_number": 14, "line_range": [ 14, 15 ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "12 from json import JSONDecodeError, loads\n13 from subprocess import PIPE\n14 from textwrap import indent\n", + "code": "11 from json import JSONDecodeError, loads\n12 from subprocess import PIPE\n13 from textwrap import indent\n", + "col_offset": 0, "filename": "./cogs/utility.py", "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, "issue_severity": "LOW", - "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 13, + "issue_text": "Consider possible security implications associated with the subprocess module.", + "line_number": 12, "line_range": [ - 13 + 12 ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "2061 try:\n2062 exec(to_compile, env) # pylint: disable=exec-used\n2063 except Exception as exc:\n", + "code": "2093 try:\n2094 exec(to_compile, env) # pylint: disable=exec-used\n2095 except Exception as exc:\n", + "col_offset": 12, "filename": "./cogs/utility.py", "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", - "line_number": 2062, + "line_number": 2094, "line_range": [ - 2062 + 2094 ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b102_exec_used.html", "test_id": "B102", "test_name": "exec_used" }, { "code": "2 import re\n3 from subprocess import PIPE\n4 from typing import List\n", + "col_offset": 0, "filename": "./core/changelog.py", "issue_confidence": "HIGH", + "issue_cwe": { + "id": 78, + "link": "https://cwe.mitre.org/data/definitions/78.html" + }, "issue_severity": "LOW", - "issue_text": "Consider possible security implications associated with PIPE module.", + "issue_text": "Consider possible security implications associated with the subprocess module.", "line_number": 3, "line_range": [ 3 ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: Optional[dict] = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", + "code": "70 \n71 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n72 self.bot = bot\n73 self.session = bot.session\n74 self.headers: Optional[dict] = None\n75 self.access_token = access_token\n76 self.username = username\n77 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n78 self.url: str = kwargs.pop(\"url\", \"\")\n79 if self.access_token:\n80 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n81 \n82 @property\n83 def BRANCH(self) -> str:\n", + "col_offset": 4, "filename": "./core/clients.py", "issue_confidence": "MEDIUM", + "issue_cwe": { + "id": 259, + "link": "https://cwe.mitre.org/data/definitions/259.html" + }, "issue_severity": "LOW", "issue_text": "Possible hardcoded password: ''", - "line_number": 68, + "line_number": 71, "line_range": [ - 68, - 69, - 70, 71, 72, 73, @@ -286,9 +329,12 @@ 76, 77, 78, - 79 + 79, + 80, + 81, + 82 ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b107_hardcoded_password_default.html", "test_id": "B107", "test_name": "hardcoded_password_default" } diff --git a/.dockerignore b/.dockerignore index 74003e7e30..a3de147db4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -138,11 +138,15 @@ temp/ test.py # Other stuff +.dockerignore .env.example +.git/ .gitignore .github/ app.json CHANGELOG.md +Dockerfile +docker-compose.yml Procfile pyproject.toml README.md diff --git a/.env.example b/.env.example index 44c91c59c7..972eca4517 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 -MODMAIL_GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID CONNECTION_URI=mongodb+srv://mongodburi +DISABLE_AUTOUPDATES=true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..176a458f94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 568e5f2175..6c6694b809 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,8 +30,19 @@ Pull requests are the best way to propose changes to the codebase. We actively w ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. -## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +## Report bugs using [Github Issues](https://github.com/modmail-dev/modmail/issues) +We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/modmail-dev/modmail/issues/new); it's that easy! + +## Find pre-existing issues to tackle +Check out our [unstaged issue tracker](https://github.com/modmail-dev/modmail/issues?q=is%3Aissue+is%3Aopen+-label%3Astaged) and start helping out! + +Ways to help out: +- Help out new members +- Highlight invalid bugs/unsupported use cases +- Code review of pull requests +- Add on new use cases or reproduction steps +- Point out duplicate issues and guide them to the right direction +- Create a pull request to resolve the issue! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: @@ -43,7 +54,6 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new Issue] - What *actually* happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - ## Use a Consistent Coding Style We use [black](https://github.com/python/black) for a unified code style. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fbcb038e69..8125c1756a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -patreon: kyber +buy_me_a_coffee: modmaildev diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 99779ab4f3..35fc2bedb1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,7 +20,7 @@ body: - Heroku - Systemd - PM2 - - Patreon + - Buy Me A Coffee / Patreon - Other validations: required: true diff --git a/.github/pull.yml b/.github/pull.yml index 2fec8bbda0..8a0898a6b7 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -1,8 +1,8 @@ version: "1" rules: - base: master - upstream: kyb3r:master + upstream: modmail-dev:master mergeMethod: hardreset - base: development - upstream: kyb3r:development + upstream: modmail-dev:development mergeMethod: hardreset \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..6af8630b38 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,42 @@ + +name: Create and publish a Docker image + +on: + push: + branches: ['master'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 145a83d489..b283dff078 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -4,27 +4,27 @@ on: [push, pull_request] jobs: code-style: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11'] -# runs-on: ${{ matrix.os }} -# strategy: -# fail-fast: false -# matrix: -# os: [ubuntu-latest, windows-latest, macOS-latest] -# python-version: [3.6, 3.7] + name: Python ${{ matrix.python-version }} on ubuntu-latest - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} + architecture: x64 - name: Install dependencies run: | python -m pip install --upgrade pip pipenv pipenv install --dev --system - - name: Bandit syntax check - run: bandit -r . -b .bandit_baseline.json + # to refresh: bandit -f json -o .bandit_baseline.json -r . + # - name: Bandit syntax check + # run: bandit -r . -b .bandit_baseline.json - name: Pylint run: pylint ./bot.py cogs/*.py core/*.py --exit-zero -r y continue-on-error: true diff --git a/.gitignore b/.gitignore index 635e3160e8..e165a06432 100644 --- a/.gitignore +++ b/.gitignore @@ -1,93 +1,29 @@ +#### Modmail +# Plugins +plugins/* +!plugins/registry.json +!plugins/@local + +# Config files +config.json +.env + +# Data +temp/ + +### Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -98,43 +34,62 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject +#### PyCharm +.idea/ -# mkdocs documentation -/site +#### VS Code +.vscode/ -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +### Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db -# Pyre type checker -.pyre/ +# Dump file +*.stackdump -# PyCharm -.idea/ +# Folder config file +[Dd]esktop.ini -# MacOS -.DS_Store +# Recycle Bin used on file shares +$RECYCLE.BIN/ -# VS Code -.vscode/ +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp -# Node -package-lock.json -node_modules/ +# Windows shortcuts +*.lnk -# Modmail -plugins/* -!plugins/registry.json -!plugins/@local -config.json -temp/ -test.py -stack.yml -.github/workflows/build_docker.yml \ No newline at end of file +### macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9b461169..536680ad2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,235 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. + +# v4.2.1 + +### Added +* `unsnooze_history_limit`: Limits the number of messages replayed when unsnoozing (genesis message and notes are always shown). +* `snooze_behavior`: Choose between `delete` (legacy) or `move` behavior for snoozing. +* `snoozed_category_id`: Target category for `move` snoozing; required when `snooze_behavior` is `move`. +* Thread-creation menu: Adds an interactive select step before a thread channel is created. + * Commands: + * `threadmenu toggle`: Enable/disable the menu. + * `threadmenu show`: List current top-level options. + * `threadmenu option add`: Interactive wizard to create an option. + * `threadmenu option edit/remove/show`: Manage or inspect an existing option. + * `threadmenu submenu create/delete/list/show`: Manage submenus. + * `threadmenu submenu option add/edit/remove`: Manage options inside a submenu. + * Configuration / Behavior: + * Per-option `category` targeting when creating a thread; falls back to `main_category_id` if invalid/missing. + * Optional selection logging (`thread_creation_menu_selection_log`) posts the chosen option in the new thread. + * Anonymous prompt support (`thread_creation_menu_anonymous_menu`). + +### Changed +- Renamed `max_snooze_time` to `snooze_default_duration`. The old config will be invalidated. +- When `snooze_behavior` is set to `move`, the snoozed category now has a hard limit of 49 channels. New snoozes are blocked once it’s full until space is freed. +- When switching `snooze_behavior` to `move` via `?config set`, the bot reminds admins to set `snoozed_category_id` if it’s missing. +- Thread-creation menu options & submenu options now support an optional per-option `category` target. The interactive wizards (`threadmenu option add` / `threadmenu submenu option add`) and edit commands allow specifying or updating a category. If the stored category is missing or invalid at selection time, channel creation automatically falls back to `main_category_id`. + + +# v4.2.0 + +Upgraded discord.py to version 2.6.3, added support for CV2. +Forwarded messages now properly show in threads, rather than showing as an empty embed. + +### Fixed +- Make Modmail keep working when typing is disabled due to an outage caused by Discord. +- Resolved an issue where forwarded messages appeared as empty embeds. +- Fixed internal message handling and restoration processes. +- Eliminated duplicate logs and notes. +- Addressed inconsistent use of `logkey` after ticket restoration. +- Fixed issues with identifying the user who sent internal messages. +- Solved an ancient bug where closing with words like `evening` wouldn't work. +- Fixed the command from being included in the reply in rare conditions. + +### Added +Commands: +* `snooze`: Initiates a snooze action. +* `snoozed`: Displays snoozed items. +* `unsnooze`: Reverses the snooze action. +* `clearsnoozed`: Clears all snoozed items. + +Configuration Options: +* `max_snooze_time`: Sets the maximum duration for snooze. +* `snooze_title`: Customizes the title for snooze notifications. +* `snooze_text`: Customizes the text for snooze notifications. +* `unsnooze_text`: Customizes the text for unsnooze notifications. +* `unsnooze_notify_channel`: Specifies the channel for unsnooze notifications. +* `thread_min_characters`: Minimum number of characters required. +* `thread_min_characters_title`: Title shown when the message is too short. +* `thread_min_characters_response`: Response shown to the user if their message is too short. +* `thread_min_characters_footer`: Footer displaying the minimum required characters. + +# v4.1.2 + +### Fixed +- Members not caching correctly for large servers. ([PR #3365](https://github.com/modmail-dev/Modmail/pull/3365)) + +# v4.1.1 + +### Fixed +- `?msglink` now supports threads with multiple recipients. ([PR #3341](https://github.com/modmail-dev/Modmail/pull/3341)) +- Fixed persistent notes not working due to discord.py internal change. ([PR #3324](https://github.com/modmail-dev/Modmail/pull/3324)) + +### Added +- Support for custom activities with `?activity custom ` ([PR #3352](https://github.com/modmail-dev/Modmail/pull/3352)) + +# v4.1.0 + +Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. + +### Fixed +- GIF stickers no longer cause the bot to crash. +- `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) +- Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) +- Reply not being forwarded from DM. ([PR #3239](https://github.com/modmail-dev/modmail/pull/3239)) +- Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) +- Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) +- Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) +- Corrected some incorrect config help descriptions. ([PR #3277](https://github.com/modmail-dev/Modmail/pull/3277)) +- Rate limit issue when fetch the messages due to reaction linking. ([PR #3306](https://github.com/modmail-dev/Modmail/pull/3306)) +- Update command fails when the plugin is invalid. ([PR #3295](https://github.com/modmail-dev/Modmail/pull/3295)) + +### Added +- `?log key ` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) +- `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) +- `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) +- `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` environment variable to set the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) +- `LOG_EXPIRATION` environment variable to set the expiration time of logs. ([PR #3257](https://github.com/modmail-dev/Modmail/pull/3257)) +- New registry plugins: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact) and [`rename`](https://github.com/Nicklaus-s/modmail-plugins/tree/main/rename). +- Improved join/leave message for multiple servers. + +### Changed +- Repo moved to https://github.com/modmail-dev/modmail. +- Channel name no longer shows `-0` if the user has migrated to the new username system. +- `?note` and `?reply` now allows you to send a sticker without any message. +- Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) +- Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) +- The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) +- `?disable all` no longer overrides `?disable new`. ([PR #3278](https://github.com/modmail-dev/Modmail/pull/3278)) +- Dropped root privileges for Modmail running under Docker. ([PR #3284](https://github.com/modmail-dev/Modmail/pull/3284)) + +### Internal +- Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) +- `ConfigManager.get` no longer accepts two positional arguments: the `convert` argument is now keyword-only. +- Various dependencies have been updated to their latest versions. + +# v4.0.2 + +### Breaking + +- Presence intent is now by-default OFF. You can turn it on by setting `ENABLE_PRESENCE_INTENT=true` in the environment variables. + +### Fixed + +- Not having a guild icon no longer raises an exception. ([PR #3235](https://github.com/modmail-dev/modmail/pull/3235)) + - When no icon is set, use the default user icon. +- Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/modmail-dev/modmail/pull/3201)) +- AttributeError raised when failing to forward a reaction. ([GH #3218](https://github.com/modmail-dev/modmail/issues/3218)) + +### Changed + +- Plain messages no longer forces `()` around the respondent text. ([PR #3234](https://github.com/modmail-dev/modmail/pull/3234)) +- Added workflow to automatically build Docker image ([PR #3232](https://github.com/modmail-dev/modmail/pull/3228)) +- Updated installation guide to reflect new preferred hosting methods + +# v4.0.1 + +This is a hotfix release. + +### Improved + +- Error Messages + +### Fixed + +- Thread cooldown + +# v4.0.0 + +### Breaking + +- Modmail now requires [`Message Content` privileged intent](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-for-Verified-Bots). +- Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/modmail-dev/modmail/issues/2990)). +- Python 3.8 or higher is required. +- Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) +- Plugin registry is purged and all developers have to re-apply due to breaking changes. + +### Added + +- `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/modmail-dev/modmail/pull/3093)) +- `require_close_reason` config to require a reason to close a thread. ([GH #3107](https://github.com/modmail-dev/modmail/issues/3107)) +- `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/modmail-dev/modmail/issues/3083)) +- `?fpareply` and `?fpreply` to reply to messages with variables plainly. +- `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/modmail-dev/modmail/issues/3112)) +- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/modmail-dev/modmail/issues/3143)) +- `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/modmail-dev/modmail/issues/3122)) +- Select menus for certain paginators. +- `Title` field in `?logs`. ([GH #3142](https://github.com/modmail-dev/modmail/issues/3142)) +- Snippets can be used in aliases. ([GH #3108](https://github.com/modmail-dev/modmail/issues/3108), [PR #3124](https://github.com/modmail-dev/modmail/pull/3124)) +- `?snippet make/create` as aliases to `?snippet add`. ([GH #3172](https://github.com/modmail-dev/modmail/issues/3173), [PR #3174](https://github.com/modmail-dev/modmail/pull/3174)) + +### Improved + +- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/modmail-dev/modmail/issues/3048)) +- Use discord relative timedeltas. ([GH #3046](https://github.com/modmail-dev/modmail/issues/3046)) +- Use discord native buttons for all paginator sessions. +- `?help` and `?blocked` paginator sessions now have better multi-page UI. +- Autoupdate now automatically updates pipenv dependencies if possible. + +### Fixed + +- Several minor typos. ([PR #3095](https://github.com/modmail-dev/modmail/pull/3095), [PR #3116](https://github.com/modmail-dev/modmail/pull/3116)) +- Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/modmail-dev/modmail/pull/3109)) +- `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/modmail-dev/modmail/issues/3091), [PR #3092](https://github.com/modmail-dev/modmail/pull/3092)) +- Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/modmail-dev/modmail/issues/3104), [PR #3105](https://github.com/modmail-dev/modmail/pull/3105)) +- Stickers now work in Modmail. +- Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/modmail-dev/modmail/issues/3088)) +- Attachments now work on plain replies. ([GH #3102](https://github.com/modmail-dev/modmail/issues/3102)) +- Support LOTTIE stickers. ([GH #3119](https://github.com/modmail-dev/modmail/issues/3119)) +- Editing notes now work. ([GH #3094](https://github.com/modmail-dev/modmail/issues/3094)) +- Commands now work in threads. +- Audit log searching now properly works. +- Old data causing `?blocked` to fail. ([GH #3131](https://github.com/modmail-dev/modmail/issues/3131)) +- Delete channel auto close functionality now works. +- Improved error handling for autoupdate. ([PR #3161](https://github.com/modmail-dev/modmail/pull/3161)) +- Skip loading of already-loaded cog. ([PR #3172](https://github.com/modmail-dev/modmail/pull/3172)) +- Respect plugin's `cog_command_error`. ([GH #3170](https://github.com/modmail-dev/modmail/issues/3170), [PR #3178](https://github.com/modmail-dev/modmail/pull/3178)) +- Use silent as a typing literal for contacting. ([GH #3179](https://github.com/modmail-dev/modmail/issues/3179)) + +### Internal + +- Improve regex parsing of channel topics. ([GH #3114](https://github.com/modmail-dev/modmail/issues/3114), [PR #3111](https://github.com/modmail-dev/modmail/pull/3111)) +- Add warning if deploying on a developmental version. +- Extensions are now loaded `on_connect`. +- MongoDB v5.0 clients are now supported. ([GH #3126](https://github.com/modmail-dev/modmail/issues/3126)) +- Bump python-dotenv to v0.20.0, support for python 3.10 +- Bump emoji to v1.7.0 +- Bump aiohttp to v3.8.1 +- Bump lottie to v0.6.11 +- Remove deprecated `core/decorators.py` from v3.3.0 + +# v3.10.5 + +### Internal + +- Locked plugin registry version impending v4 release. + +# v3.10.4 + +### Improved + +- Thread genesis message now shows other recipients. + +### Fixed + +- `?snippet add` now properly blocks command names. + +### Internal + +- Set `LOG_DISCORD` environment variable to the logger level and log discord events. # v3.10.3 This is a hotfix for contact command. @@ -13,7 +241,6 @@ This is a hotfix for contact command. - Fixed a bug where contacting with no category argument defaults to the top category. - # v3.10.2 This is a hotfix for react to contact. @@ -40,37 +267,37 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Added -- Ability to have group conversations with up to 5 users. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) -- Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) -- Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) -- New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/kyb3r/modmail/issues/3072)) +- Ability to have group conversations with up to 5 users. ([GH #143](https://github.com/modmail-dev/modmail/issues/143)) +- Snippets are invoked case insensitively. ([GH #3077](https://github.com/modmail-dev/modmail/issues/3077), [PR #3080](https://github.com/modmail-dev/modmail/pull/3080)) +- Default tags now use top hoisted role. ([GH #3014](https://github.com/modmail-dev/modmail/issues/3014)) +- New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/modmail-dev/modmail/issues/3072)) - `use_timestamp_channel_name` config to create thread channels by timestamp. ### Improved -- `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/kyb3r/modmail/issues/3082)) -- Aliases are now supported in autotrigger. ([GH #3081](https://github.com/kyb3r/modmail/pull/3081)) +- `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/modmail-dev/modmail/issues/3082)) +- Aliases are now supported in autotrigger. ([GH #3081](https://github.com/modmail-dev/modmail/pull/3081)) ### Fixed -- Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) -- Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) -- UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) -- Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) -- `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) -- `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) +- Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/modmail-dev/modmail/issues/3022), [PR #3028](https://github.com/modmail-dev/modmail/pull/3028)) +- Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/modmail-dev/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/modmail-dev/modmail/pull/3027)) +- UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/modmail-dev/modmail/pull/3043)) +- Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/modmail-dev/modmail/pull/3015)) +- `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/modmail-dev/modmail/issues/3076)) +- `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/modmail-dev/modmail/issues/3075)) - Invalid arguments are now properly catched and a proper error message is sent. -- Update database after resetting/purging all plugins. ([GH #3011](https://github.com/kyb3r/modmail/pull/3011)) -- `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/kyb3r/modmail/issues/3030)) -- Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/kyb3r/modmail/issues/3041), [@JerrieAries](https://github.com/kyb3r/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) -- Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) +- Update database after resetting/purging all plugins. ([GH #3011](https://github.com/modmail-dev/modmail/pull/3011)) +- `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/modmail-dev/modmail/issues/3030)) +- Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/modmail-dev/modmail/issues/3041), [@JerrieAries](https://github.com/modmail-dev/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) +- Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/modmail-dev/modmail/issues/3013)) - Modmail Bot is now recognized to have `OWNER` permission level. This affects what can be run in autotriggers. ### Internal -- Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) -- Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) -- `format_channel_name` is now extendable to plugins. Modify `Bot.format_channel_name(bot, author, exclude_channel=None, force_null=False):`. ([GH #2982](https://github.com/kyb3r/modmail/issues/2982)) +- Fix return types, type hints and unresolved references ([PR #3009](https://github.com/modmail-dev/modmail/pull/3009)) +- Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/modmail-dev/modmail/issues/3037)) +- `format_channel_name` is now extendable to plugins. Modify `Bot.format_channel_name(bot, author, exclude_channel=None, force_null=False):`. ([GH #2982](https://github.com/modmail-dev/modmail/issues/2982)) # v3.9.5 @@ -84,17 +311,17 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Fixed -- Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/kyb3r/modmail/issues/3002), [PR #3003](https://github.com/kyb3r/modmail/pull/3003)) +- Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/modmail-dev/modmail/issues/3002), [PR #3003](https://github.com/modmail-dev/modmail/pull/3003)) - There is now a proper message when trying to contact a bot. ### Improved -- `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) -- `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) +- `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/modmail-dev/modmail/pull/2993/files)) +- `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/modmail-dev/modmail/pull/2993/files)) ### Internal -- Change heroku python version to 3.9.4 ([PR #3001](https://github.com/kyb3r/modmail/pull/3001)) +- Change heroku python version to 3.9.4 ([PR #3001](https://github.com/modmail-dev/modmail/pull/3001)) # v3.9.3 @@ -113,7 +340,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Improved -- Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/kyb3r/modmail/issues/2977), [PR #2988](https://github.com/kyb3r/modmail/pull/2988)) +- Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/modmail-dev/modmail/issues/2977), [PR #2988](https://github.com/modmail-dev/modmail/pull/2988)) ### Fixed @@ -134,8 +361,8 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Fixed -- `confirm_thread_creation` now properly works when a user opens a thread using react to contact. ([GH #2930](https://github.com/kyb3r/modmail/issues/2930), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) -- `?disable all/new` now disables react to contact threads. ([GH #2969](https://github.com/kyb3r/modmail/issues/2969), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) +- `confirm_thread_creation` now properly works when a user opens a thread using react to contact. ([GH #2930](https://github.com/modmail-dev/modmail/issues/2930), [PR #2971](https://github.com/modmail-dev/modmail/pull/2971)) +- `?disable all/new` now disables react to contact threads. ([GH #2969](https://github.com/modmail-dev/modmail/issues/2969), [PR #2971](https://github.com/modmail-dev/modmail/pull/2971)) - Ghost errors are no longer raised when threads are created using non-organic methods. ### Internal @@ -154,13 +381,13 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Added -- `?msglink `, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/kyb3r/modmail/issues/2963), [PR #2964](https://github.com/kyb3r/modmail/pull/2964)) -- `?mention disable/reset`, disables or resets mention on thread creation. ([PR #2951](https://github.com/kyb3r/modmail/pull/2951)) +- `?msglink `, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/modmail-dev/modmail/issues/2963), [PR #2964](https://github.com/modmail-dev/modmail/pull/2964)) +- `?mention disable/reset`, disables or resets mention on thread creation. ([PR #2951](https://github.com/modmail-dev/modmail/pull/2951)) ### Fixed - Non-master/development branch deployments no longer cause errors to be raised. -- Autotriggers now can search for roles/channels in guild context. ([GH #2961](https://github.com/kyb3r/modmail/issues/2961)) +- Autotriggers now can search for roles/channels in guild context. ([GH #2961](https://github.com/modmail-dev/modmail/issues/2961)) # v3.8.4 @@ -174,28 +401,28 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/kyb3r/modmail/issues/2934)) +- Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/modmail-dev/modmail/issues/2934)) - Fix update notifications. -- Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/kyb3r/modmail/issues/2935), [PR #2936](https://github.com/kyb3r/modmail/pull/2936)) +- Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/modmail-dev/modmail/issues/2935), [PR #2936](https://github.com/modmail-dev/modmail/pull/2936)) - IDs in `` commands work now. # v3.8.1 ### Fixed -- Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933)) -- `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/kyb3r/modmail/issues/2931), [PR #2933](https://github.com/kyb3r/modmail/pull/2933)) -- Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/kyb3r/modmail/issues/2932)) +- Additional image uploads now render properly. ([PR #2933](https://github.com/modmail-dev/modmail/pull/2933)) +- `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/modmail-dev/modmail/issues/2931), [PR #2933](https://github.com/modmail-dev/modmail/pull/2933)) +- Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/modmail-dev/modmail/issues/2932)) # v3.8.0 ### Added -- `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) +- `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/modmail-dev/modmail/issues/2896)) - `?fareply`, anonymously reply with variables. -- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) +- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/modmail-dev/modmail/issues/2905)) - `disable_updates` config variable to control if the update command should be disabled or not. -- `silent_alert_on_mention` to alert mods silently. ([GH #2907](https://github.com/kyb3r/modmail/issues/2907)) +- `silent_alert_on_mention` to alert mods silently. ([GH #2907](https://github.com/modmail-dev/modmail/issues/2907)) - Support for only the "Server Members" intent. ### Improved @@ -206,17 +433,17 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) +- Mentioned `competing` as an activity type. ([PR #2902](https://github.com/modmail-dev/modmail/pull/2902)) - Level permissions were not checked if command permissions were set. - Regex autotriggers were not working if term was in the middle of strings. - `?blocked` now no longers show blocks that have expired. - Blocked roles will no longer trigger an error during unblock. -- Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) -- Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) -- Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) -- Deleting messages no longer shows a false error. ([GH #2910](https://github.com/kyb3r/modmail/issues/2910), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2910#issuecomment-753557313)) +- Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/modmail-dev/modmail/issues/2916)) +- Finding linked messages in replies work now. ([GH #2920](https://github.com/modmail-dev/modmail/issues/2920), [Jerrie-Aries](https://github.com/modmail-dev/modmail/issues/2920#issuecomment-751530495)) +- Sending files in threads (non-images) now work. ([GH #2926](https://github.com/modmail-dev/modmail/issues/2926)) +- Deleting messages no longer shows a false error. ([GH #2910](https://github.com/modmail-dev/modmail/issues/2910), [Jerrie-Aries](https://github.com/modmail-dev/modmail/issues/2910#issuecomment-753557313)) - Display an error on [Lottie](https://airbnb.io/lottie/#/) stickers, instead of failing the send. -- `?perms get` now shows role/user names. ([PR #2927](https://github.com/kyb3r/modmail/pull/2927)) +- `?perms get` now shows role/user names. ([PR #2927](https://github.com/modmail-dev/modmail/pull/2927)) ### Internal @@ -255,13 +482,13 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added - Added `update_channel_id` to specify which channel autoupdate notifications were being sent to. -- Added `show_timestamp` to specify if timestamps should be displayed in message embeds. ([GH #2885](https://github.com/kyb3r/modmail/issues/2885)) +- Added `show_timestamp` to specify if timestamps should be displayed in message embeds. ([GH #2885](https://github.com/modmail-dev/modmail/issues/2885)) # v3.7.9 ### Fixed -- `perms add/remove` with permission levels should now work again. ([GH #2892](https://github.com/kyb3r/modmail/issues/2892), [PR #2893](https://github.com/kyb3r/modmail/pull/2893)) +- `perms add/remove` with permission levels should now work again. ([GH #2892](https://github.com/modmail-dev/modmail/issues/2892), [PR #2893](https://github.com/modmail-dev/modmail/pull/2893)) ### Improved @@ -271,7 +498,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added -- Added `thread_contact_silently` to allow opening threads silently by default. ([PR #2887](https://github.com/kyb3r/modmail/pull/2887)) +- Added `thread_contact_silently` to allow opening threads silently by default. ([PR #2887](https://github.com/modmail-dev/modmail/pull/2887)) ### Fixed - Permission levels were not respected. @@ -297,7 +524,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed - Autoupdate persists despite errors. -- Mention when normal thread created was not working. ([GH #2883](https://github.com/kyb3r/modmail/issues/2883)) +- Mention when normal thread created was not working. ([GH #2883](https://github.com/modmail-dev/modmail/issues/2883)) # v3.7.5 @@ -309,13 +536,13 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- React to contact threads were treated like normal contact threads. ([GH #2881](https://github.com/kyb3r/modmail/issues/2881)) +- React to contact threads were treated like normal contact threads. ([GH #2881](https://github.com/modmail-dev/modmail/issues/2881)) # v3.7.2 ### Added -- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to. ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) +- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to. ([GH #2880](https://github.com/modmail-dev/modmail/issues/2880)) ### Fixed @@ -332,31 +559,31 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added -- Plain replies functionality. Added commands `preply`, `pareply` and config `plain_reply_without_command`. ([GH #2872](https://github.com/kyb3r/modmail/issues/2872)) +- Plain replies functionality. Added commands `preply`, `pareply` and config `plain_reply_without_command`. ([GH #2872](https://github.com/modmail-dev/modmail/issues/2872)) - Added `react_to_contact_message`, `react_to_contact_emoji` to allow users to create threads by reacting to a message. -- Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/kyb3r/modmail/issues/215)) -- Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) -- Added `close_on_leave`, `close_on_leave_reason` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) -- Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) -- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) -- Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) +- Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/modmail-dev/modmail/issues/215)) +- Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/modmail-dev/modmail/issues/2763)) +- Added `close_on_leave`, `close_on_leave_reason` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/modmail-dev/modmail/issues/2757)) +- Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/modmail-dev/modmail/issues/2833)) +- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/modmail-dev/modmail/issues/2773)) +- Support Gyazo image links in message embeds. ([GH #282](https://github.com/modmail-dev/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. -- Added new functionality: If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) -- Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) -- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) -- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) +- Added new functionality: If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/modmail-dev/modmail/issues/2847)) +- Added a way to block roles. ([GH #2753](https://github.com/modmail-dev/modmail/issues/2753)) +- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/modmail-dev/modmail/issues/2865)) +- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/modmail-dev/modmail/issues/2762)) - Support stickers and reject non-messages. (i.e. pin_add) -- Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) +- Added support for thread titles, `?title`. ([GH #2838](https://github.com/modmail-dev/modmail/issues/2838)) - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. -- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) -- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/kyb3r/modmail/issues/2842), [PR #2878](https://github.com/kyb3r/modmail/pull/2878)) +- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/modmail-dev/modmail/issues/130), [GH #649](https://github.com/modmail-dev/modmail/issues/649)) +- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/modmail-dev/modmail/issues/2842), [PR #2878](https://github.com/modmail-dev/modmail/pull/2878)) - Autoupdates and `?update` which was removed in v3.0.0 ### Fixed - `?contact` now sends members a DM. -- `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) -- Command truncated after && in alias. ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) +- `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/modmail-dev/modmail/issues/2856)) +- Command truncated after && in alias. ([GH #2870](https://github.com/modmail-dev/modmail/issues/2870)) - `on_plugins_ready` event for plugins works now. ### Improved @@ -365,7 +592,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh - `?move` now does not require exact category names, accepts case-insensitive and startswith names. ### Internal -- Use enums in config. ([GH #2821](https://github.com/kyb3r/modmail/issues/2821)) +- Use enums in config. ([GH #2821](https://github.com/modmail-dev/modmail/issues/2821)) - `on_thread_close` event for plugins. - `on_thread_reply` event for plugins. @@ -388,14 +615,14 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added - Added `thread_move_title` to specify title of thread moved embed. -- Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) -- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) -- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) +- Mark NSFW logs in log message. ([GH #2792](https://github.com/modmail-dev/modmail/issues/2792)) +- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/modmail-dev/modmail/issues/2828)) +- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/modmail-dev/modmail/issues/2796)) ### Changed - `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! -- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) +- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/modmail-dev/modmail/pull/2845)) ### Fixed - Unicode errors raised during windows selfhosting @@ -404,7 +631,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh - Bump discord.py version to 1.5.1 - Explicitly state intents used for connection -- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/kyb3r/modmail/issues/2816)) +- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/modmail-dev/modmail/issues/2816)) # v3.5.0 @@ -414,7 +641,7 @@ Fixed discord.py issue. ### Added - A confirmation when you manually delete a thread message embed. -- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/kyb3r/modmail/issues/2803)) +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/modmail-dev/modmail/issues/2803)) - Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. - Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) @@ -723,7 +950,7 @@ Security update! - Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). -Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) +Read more about updating your bot [here](https://github.com/modmail-dev/modmail/wiki/updating) ### Changed - Channel names now can contain Unicode characters. @@ -768,7 +995,7 @@ Added a 🛑 reaction to the paginators to delete the embed. ### Fixed -`?blocked` is now paginated using reactions. This fixes [#249](https://github.com/kyb3r/modmail/issues/249) +`?blocked` is now paginated using reactions. This fixes [#249](https://github.com/modmail-dev/modmail/issues/249) # v2.21.0 @@ -807,7 +1034,7 @@ This update contains mostly internal changes. ### What's new? -New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/modmail-dev/logviewer). # v2.19.1 @@ -1092,7 +1319,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! - - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). + - [Creating Plugins Documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). # v2.12.5 @@ -1103,7 +1330,7 @@ Added image link in title in case discord fails to embed an image. # v2.12.4 ### What's new? -- Named colors are now supported! Over 900 different common color names are recognized. A list of color names can be found in [core/_color_data.py](https://github.com/kyb3r/modmail/blob/master/core/_color_data.py). +- Named colors are now supported! Over 900 different common color names are recognized. A list of color names can be found in [core/_color_data.py](https://github.com/modmail-dev/modmail/blob/master/core/_color_data.py). - Named colors can be set the same way as hex. But this can only be done through `config set`, which means database modifications will not work. - For example: `config set main_color yellowish green`. - New config var `main_color` allows you to customize the main Modmail color (as requested by many). Defaults to Discord `blurple`. @@ -1297,7 +1524,7 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Background - Bots hosted by Heroku restart at least once every 27 hours. - During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. -- [PR #135](https://github.com/kyb3r/modmail/pull/135) +- [PR #135](https://github.com/modmail-dev/modmail/pull/135) ### Changed - Created a new internal config var: `closures`. @@ -1335,7 +1562,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### What's new? - Added the `?activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). +- [PR #131](https://github.com/modmail-dev/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). ### Removed - Removed the deprecated `status` command. @@ -1468,9 +1695,9 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o # v2.0.0 -This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. +This release introduces the use of our centralized [API service](https://github.com/modmail-dev/webserver) to enable dynamic configuration, auto-updates, and thread logs. To use this release, you must acquire an API token from https://modmail.tk. -Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). +Read the updated installation guide [here](https://github.com/modmail-dev/modmail/wiki/installation). ### Changed - Stability improvements through synchronization primitives. diff --git a/Dockerfile b/Dockerfile index 34aba25ce6..246d3cf7a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,38 @@ -FROM python:3.9-slim as py +FROM python:3.11-slim-bookworm as base -FROM py as build +RUN apt-get update && \ + apt-get install --no-install-recommends -y \ + # Install CairoSVG dependencies. + libcairo2 && \ + # Cleanup APT. + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + # Create a non-root user. + useradd --shell /usr/sbin/nologin --create-home -d /opt/modmail modmail -RUN apt update && apt install -y g++ -COPY requirements.txt / -RUN pip install --prefix=/inst -U -r /requirements.txt +FROM base as builder -FROM py +COPY requirements.txt . -ENV USING_DOCKER yes -COPY --from=build /inst /usr/local +RUN pip install --root-user-action=ignore --no-cache-dir --upgrade pip wheel && \ + python -m venv /opt/modmail/.venv && \ + . /opt/modmail/.venv/bin/activate && \ + pip install --no-cache-dir --upgrade -r requirements.txt + +FROM base + +# Copy the entire venv. +COPY --from=builder --chown=modmail:modmail /opt/modmail/.venv /opt/modmail/.venv + +# Copy repository files. +WORKDIR /opt/modmail +USER modmail:modmail +COPY --chown=modmail:modmail . . + +# This sets some Python runtime variables and disables the internal auto-update. +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH=/opt/modmail/.venv/bin:$PATH \ + USING_DOCKER=yes -WORKDIR /modmailbot CMD ["python", "bot.py"] -COPY . /modmailbot diff --git a/Pipfile b/Pipfile index 4ed01f5b70..daa0e60698 100644 --- a/Pipfile +++ b/Pipfile @@ -4,26 +4,31 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -bandit = "~=1.7.0" -black = "==21.6b0" -pylint = "~=2.9.3" +bandit = ">=1.7.5" +black = "==23.11.0" +pylint = "==3.0.2" +tomli = "==2.2.1" # Needed for black on Python < 3.11 [packages] -aiohttp = "==3.7.4.post0" -colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = "==1.7.3" -emoji = "~=1.2.0" -isodate = "~=0.6.0" -motor = "~=2.4.0" -natural = "~=0.2.0" -parsedatetime = "~=2.6" -pymongo = {extras = ["srv"], version = "*"} # Required by motor -python-dateutil = "~=2.8.1" -python-dotenv = "~=0.18.0" -uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} +aiohttp = "==3.13.2" +async-timeout = {version = "==5.0.1", markers = "python_version < '3.11'"} # Required by aiohttp +typing-extensions = ">=4.12.2" # Required by aiohttp +colorama = "==0.4.6" +"discord.py" = {version = "==2.6.3", extras = ["speed"]} +emoji = "==2.8.0" +isodate = "==0.6.1" +motor = "==3.7.1" +natural = "==0.2.0" # Why is this needed? +packaging = "==23.2" +parsedatetime = "==2.6" +dnspython = ">=2.8,<3" # Required by pymongo +pymongo = ">=4.9,<5" # Required by motor +python-dateutil = "==2.8.2" +python-dotenv = "==1.0.0" +uvloop = {version = ">=0.19.0", markers = "sys_platform != 'win32'"} +lottie = {version = "==0.7.2", extras = ["pdf"]} +setuptools = "*" # Needed for lottie +requests = "==2.31.0" [scripts] bot = "python bot.py" - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index a9f820119e..39cd6c33e2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "0e726213f83b90d7c4e90a04cea6636dbdc5be2ad82049c96820535e5cc3d1ad" + "sha256": "b9e47a4bb95c39f0d11eeffe03c9229ef1751eec0e412c1a9b4c1f6dc47ed754" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -16,171 +14,942 @@ ] }, "default": { + "aiodns": { + "hashes": [ + "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", + "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5" + ], + "markers": "python_version >= '3.9'", + "version": "==3.5.0" + }, + "aiohappyeyeballs": { + "hashes": [ + "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", + "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8" + ], + "markers": "python_version >= '3.9'", + "version": "==2.6.1" + }, "aiohttp": { "hashes": [ - "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", - "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", - "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", - "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", - "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", - "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", - "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", - "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", - "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", - "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", - "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", - "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", - "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", - "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", - "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", - "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", - "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", - "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", - "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", - "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", - "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", - "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", - "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", - "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", - "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", - "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", - "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", - "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", - "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", - "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", - "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", - "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", - "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", - "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", - "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", - "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", - "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" + "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329", + "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", + "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7", + "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", + "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", + "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", + "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", + "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5", + "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", + "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", + "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca", + "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", + "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", + "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", + "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", + "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", + "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6", + "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", + "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", + "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", + "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b", + "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa", + "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", + "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", + "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", + "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", + "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", + "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07", + "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", + "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", + "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", + "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", + "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", + "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d", + "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", + "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", + "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", + "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", + "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", + "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", + "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", + "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", + "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", + "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", + "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", + "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", + "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", + "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", + "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", + "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d", + "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989", + "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", + "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", + "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", + "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", + "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", + "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac", + "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", + "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", + "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", + "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", + "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", + "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", + "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", + "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", + "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", + "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", + "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5", + "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", + "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", + "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", + "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084", + "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", + "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", + "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", + "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", + "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", + "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", + "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", + "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", + "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", + "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", + "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", + "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", + "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", + "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", + "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", + "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", + "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", + "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", + "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700", + "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", + "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901", + "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2", + "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", + "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", + "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", + "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", + "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", + "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", + "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", + "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", + "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", + "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", + "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", + "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", + "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", + "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", + "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", + "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", + "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", + "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", + "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", + "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", + "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", + "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", + "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", + "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", + "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", + "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a" ], "index": "pypi", - "version": "==3.7.4.post0" + "markers": "python_version >= '3.9'", + "version": "==3.13.2" + }, + "aiosignal": { + "hashes": [ + "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" + ], + "markers": "python_version >= '3.9'", + "version": "==1.4.0" }, "async-timeout": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", + "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.0.1" + "markers": "python_version < '3.11'", + "version": "==5.0.1" }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", + "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" + "markers": "python_version >= '3.9'", + "version": "==25.4.0" }, - "chardet": { + "audioop-lts": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", + "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", + "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", + "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", + "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", + "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", + "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", + "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", + "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", + "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", + "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", + "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", + "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", + "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", + "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", + "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", + "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", + "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", + "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", + "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", + "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", + "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", + "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", + "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", + "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", + "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", + "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", + "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", + "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", + "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", + "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", + "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", + "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", + "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", + "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", + "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", + "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", + "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", + "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", + "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", + "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", + "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", + "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", + "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", + "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", + "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", + "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", + "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", + "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.13'", + "version": "==0.2.2" + }, + "brotli": { + "hashes": [ + "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", + "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", + "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4", + "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", + "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", + "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470", + "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", + "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", + "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", + "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502", + "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", + "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7", + "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", + "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", + "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17", + "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc", + "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", + "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971", + "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", + "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d", + "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", + "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", + "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", + "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", + "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", + "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a", + "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", + "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", + "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0", + "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46", + "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", + "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8", + "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", + "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3", + "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a", + "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6", + "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64", + "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", + "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", + "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", + "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5", + "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a", + "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", + "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", + "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", + "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982", + "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f", + "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b", + "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", + "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518", + "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", + "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", + "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16", + "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a", + "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", + "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1", + "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190", + "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", + "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e", + "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", + "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea", + "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8", + "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", + "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", + "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526", + "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", + "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92", + "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12", + "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", + "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8", + "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", + "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", + "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", + "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", + "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", + "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", + "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb", + "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533", + "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8", + "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2", + "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69", + "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96", + "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49", + "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", + "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", + "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f", + "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", + "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7", + "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", + "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", + "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8", + "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990", + "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e", + "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", + "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675", + "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", + "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c", + "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13", + "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", + "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d" + ], + "version": "==1.2.0" + }, + "cairocffi": { + "hashes": [ + "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", + "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f" + ], + "markers": "python_version >= '3.8'", + "version": "==1.7.1" + }, + "cairosvg": { + "hashes": [ + "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", + "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5" + ], + "markers": "python_version >= '3.9'", + "version": "==2.8.2" + }, + "certifi": { + "hashes": [ + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" + }, + "cffi": { + "hashes": [ + "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", + "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", + "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", + "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", + "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", + "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", + "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", + "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", + "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", + "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", + "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", + "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", + "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", + "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", + "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", + "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", + "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", + "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", + "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", + "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", + "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", + "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", + "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", + "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", + "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", + "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", + "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", + "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", + "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", + "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", + "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", + "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", + "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", + "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", + "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", + "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", + "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", + "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", + "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", + "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", + "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", + "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", + "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", + "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", + "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", + "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", + "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", + "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", + "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", + "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", + "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", + "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", + "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", + "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", + "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", + "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", + "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", + "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", + "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", + "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", + "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", + "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", + "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", + "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", + "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", + "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", + "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", + "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", + "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", + "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", + "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", + "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", + "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", + "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", + "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", + "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", + "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", + "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", + "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", + "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", + "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", + "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", + "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", + "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.0.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], "index": "pypi", - "version": "==0.4.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "cssselect2": { + "hashes": [ + "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", + "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a" + ], + "markers": "python_version >= '3.9'", + "version": "==0.8.0" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.7.1" }, "discord.py": { + "extras": [ + "speed" + ], "hashes": [ - "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408", - "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c" + "sha256:69835269d73d9889a2f0efff4c91264a18998db0fdc4295a3c886fe9196dea4e", + "sha256:92bb3ef9dbe08525803be1e357bc0191f59ae16956690fc96c34f40bcd02c649" ], - "index": "pypi", - "version": "==1.7.3" + "markers": "python_version >= '3.8'", + "version": "==2.6.3" }, "dnspython": { "hashes": [ - "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", - "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" + "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", + "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==2.8.0" }, "emoji": { "hashes": [ - "sha256:496f432058567985838c13d67dde84ca081614a8286c0b9cdc7d63dfa89d51a3", - "sha256:6b19b65da8d6f30551eead1705539cc0eadcd9e33a6ecbc421a29b87f96287eb" + "sha256:8d8b5dec3c507444b58890e598fc895fcec022b3f5acb49497c6ccc5208b8b00", + "sha256:a8468fd836b7ecb6d1eac054c9a591701ce0ccd6c6f7779ad71b66f76664df90" ], "index": "pypi", - "version": "==1.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.0" + }, + "frozenlist": { + "hashes": [ + "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", + "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", + "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", + "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", + "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", + "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", + "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", + "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", + "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", + "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", + "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", + "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", + "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", + "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", + "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", + "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", + "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", + "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", + "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", + "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", + "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", + "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", + "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", + "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", + "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", + "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", + "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", + "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", + "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", + "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", + "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", + "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", + "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", + "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", + "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", + "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", + "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", + "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", + "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", + "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", + "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", + "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", + "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", + "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", + "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", + "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", + "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", + "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", + "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", + "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", + "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", + "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", + "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", + "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", + "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", + "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", + "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", + "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", + "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", + "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", + "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", + "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", + "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", + "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", + "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", + "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", + "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", + "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", + "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", + "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", + "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", + "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", + "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", + "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", + "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", + "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", + "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", + "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", + "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", + "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", + "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", + "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", + "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", + "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", + "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", + "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", + "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", + "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", + "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", + "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", + "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", + "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", + "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", + "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", + "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", + "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", + "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", + "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", + "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", + "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", + "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", + "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", + "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", + "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", + "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", + "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", + "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", + "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", + "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", + "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", + "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", + "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", + "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", + "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", + "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", + "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", + "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", + "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", + "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", + "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", + "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", + "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", + "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", + "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", + "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", + "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", + "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", + "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", + "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", + "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" + ], + "markers": "python_version >= '3.9'", + "version": "==1.8.0" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], - "markers": "python_version >= '3.5'", - "version": "==3.2" + "markers": "python_version >= '3.8'", + "version": "==3.11" }, "isodate": { "hashes": [ - "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", - "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", + "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.6.1" + }, + "lottie": { + "extras": [ + "pdf" + ], + "hashes": [ + "sha256:85304ac152cd042c9fe98a527b5d136a31c6fe2aacd859ce1c58189bb946d127" + ], + "markers": "python_version >= '3'", + "version": "==0.7.2" }, "motor": { "hashes": [ - "sha256:1196db507142ef8f00d953efa2f37b39335ef2d72af6ce4fbccfd870b65c5e9f", - "sha256:839c11a43897dbec8e5ba0e87a9c9b877239803126877b2efa5cef89aa6b687a" + "sha256:27b4d46625c87928f331a6ca9d7c51c2f518ba0e270939d395bc1ddc89d64526", + "sha256:8a63b9049e38eeeb56b4fdd57c3312a6d1f25d01db717fe7d82222393c410298" ], "index": "pypi", - "version": "==2.4.0" + "markers": "python_version >= '3.9'", + "version": "==3.7.1" }, "multidict": { "hashes": [ - "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" + "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", + "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", + "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", + "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", + "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", + "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", + "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", + "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", + "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", + "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", + "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", + "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", + "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", + "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", + "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", + "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", + "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", + "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", + "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", + "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", + "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", + "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", + "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", + "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", + "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", + "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", + "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", + "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", + "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", + "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", + "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", + "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", + "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", + "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", + "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", + "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", + "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", + "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", + "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", + "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", + "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", + "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", + "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", + "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", + "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", + "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", + "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", + "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", + "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", + "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", + "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", + "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", + "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", + "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", + "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", + "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", + "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", + "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", + "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", + "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", + "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", + "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", + "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", + "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", + "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", + "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", + "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", + "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", + "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", + "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", + "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", + "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", + "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", + "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", + "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", + "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", + "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", + "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", + "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", + "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", + "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", + "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", + "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", + "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", + "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", + "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", + "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", + "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", + "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", + "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", + "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", + "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", + "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", + "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", + "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", + "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", + "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", + "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", + "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", + "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", + "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", + "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", + "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", + "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", + "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", + "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", + "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", + "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", + "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", + "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", + "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", + "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", + "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", + "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", + "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", + "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", + "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", + "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", + "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", + "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", + "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", + "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", + "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", + "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", + "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", + "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", + "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", + "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", + "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", + "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", + "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", + "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", + "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", + "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", + "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", + "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", + "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", + "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", + "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", + "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", + "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", + "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", + "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", + "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", + "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", + "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7" ], - "markers": "python_version >= '3.6'", - "version": "==5.1.0" + "markers": "python_version >= '3.9'", + "version": "==6.7.0" }, "natural": { "hashes": [ @@ -189,6 +958,108 @@ "index": "pypi", "version": "==0.2.0" }, + "orjson": { + "hashes": [ + "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", + "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", + "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", + "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", + "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", + "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10", + "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350", + "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", + "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", + "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13", + "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", + "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", + "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", + "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", + "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", + "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", + "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", + "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", + "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", + "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", + "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", + "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a", + "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b", + "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", + "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", + "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", + "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", + "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", + "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", + "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", + "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", + "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", + "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc", + "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", + "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", + "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", + "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", + "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", + "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", + "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9", + "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", + "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", + "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", + "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", + "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19", + "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", + "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", + "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", + "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", + "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", + "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", + "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", + "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", + "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", + "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", + "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", + "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", + "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", + "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", + "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", + "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", + "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f", + "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", + "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", + "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", + "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d", + "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", + "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", + "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", + "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919", + "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", + "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", + "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", + "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", + "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", + "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", + "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", + "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6", + "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", + "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", + "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", + "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", + "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", + "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", + "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", + "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1", + "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff" + ], + "markers": "python_version >= '3.9'", + "version": "==3.11.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, "parsedatetime": { "hashes": [ "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", @@ -197,112 +1068,414 @@ "index": "pypi", "version": "==2.6" }, + "pillow": { + "hashes": [ + "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", + "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", + "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", + "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", + "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", + "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", + "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", + "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", + "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", + "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", + "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", + "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", + "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", + "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", + "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", + "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", + "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", + "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", + "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", + "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", + "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", + "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", + "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", + "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", + "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", + "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", + "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", + "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", + "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", + "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", + "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", + "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", + "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", + "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", + "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", + "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", + "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", + "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", + "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", + "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", + "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", + "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", + "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", + "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", + "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", + "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", + "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", + "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", + "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", + "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", + "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", + "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", + "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", + "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", + "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", + "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", + "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", + "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", + "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", + "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", + "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", + "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", + "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", + "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", + "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", + "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", + "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", + "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", + "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", + "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", + "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", + "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", + "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", + "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", + "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", + "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", + "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", + "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", + "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", + "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", + "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", + "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", + "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", + "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", + "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", + "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", + "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", + "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", + "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", + "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", + "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1" + ], + "markers": "python_version >= '3.10'", + "version": "==12.0.0" + }, + "propcache": { + "hashes": [ + "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", + "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", + "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", + "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", + "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", + "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", + "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", + "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", + "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", + "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", + "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", + "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", + "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", + "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", + "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", + "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", + "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", + "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", + "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", + "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", + "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", + "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", + "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", + "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", + "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", + "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", + "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", + "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", + "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", + "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", + "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", + "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", + "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", + "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", + "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", + "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", + "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", + "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", + "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", + "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", + "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", + "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", + "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", + "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", + "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", + "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", + "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", + "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", + "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", + "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", + "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", + "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", + "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", + "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", + "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", + "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", + "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", + "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", + "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", + "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", + "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", + "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", + "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", + "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", + "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", + "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", + "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", + "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", + "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", + "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", + "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", + "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", + "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", + "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", + "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", + "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", + "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", + "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", + "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", + "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", + "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", + "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", + "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", + "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", + "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", + "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", + "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", + "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", + "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", + "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", + "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", + "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", + "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", + "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", + "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", + "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", + "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", + "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", + "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", + "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", + "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", + "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", + "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", + "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", + "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", + "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", + "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.1" + }, + "pycares": { + "hashes": [ + "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", + "sha256:066f3caa07c85e1a094aebd9e7a7bb3f3b2d97cff2276665693dd5c0cc81cf84", + "sha256:0aed0974eab3131d832e7e84a73ddb0dddbc57393cd8c0788d68a759a78c4a7b", + "sha256:1571a7055c03a95d5270c914034eac7f8bfa1b432fc1de53d871b821752191a4", + "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", + "sha256:1dbbf0cfb39be63598b4cdc2522960627bf2f523e49c4349fb64b0499902ec7c", + "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", + "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", + "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", + "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", + "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", + "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", + "sha256:30d197180af626bb56f17e1fa54640838d7d12ed0f74665a3014f7155435b199", + "sha256:30feeab492ac609f38a0d30fab3dc1789bd19c48f725b2955bcaaef516e32a21", + "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", + "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", + "sha256:35ff1ec260372c97ed688efd5b3c6e5481f2274dea08f6c4ea864c195a9673c6", + "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", + "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", + "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", + "sha256:3bd81ad69f607803f531ff5cfa1262391fa06e78488c13495cee0f70d02e0287", + "sha256:3d5300a598ad48bbf169fba1f2b2e4cf7ab229e7c1a48d8c1166f9ccf1755cb3", + "sha256:3db6b6439e378115572fa317053f3ee6eecb39097baafe9292320ff1a9df73e3", + "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", + "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", + "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", + "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", + "sha256:5344d52efa37df74728505a81dd52c15df639adffd166f7ddca7a6318ecdb605", + "sha256:5d69e2034160e1219665decb8140e439afc7a7afcfd4adff08eb0f6142405c3e", + "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", + "sha256:5e1ab899bb0763dea5d6569300aab3a205572e6e2d0ef1a33b8cf2b86d1312a4", + "sha256:6195208b16cce1a7b121727710a6f78e8403878c1017ab5a3f92158b048cec34", + "sha256:66c310773abe42479302abf064832f4a37c8d7f788f4d5ee0d43cbad35cf5ff4", + "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", + "sha256:6f751f5a0e4913b2787f237c2c69c11a53f599269012feaa9fb86d7cef3aec26", + "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", + "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", + "sha256:742fbaa44b418237dbd6bf8cdab205c98b3edb334436a972ad341b0ea296fb47", + "sha256:7570e0b50db619b2ee370461c462617225dc3a3f63f975c6f117e2f0c94f82ca", + "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", + "sha256:77bf82dc0beb81262bf1c7f546e1c1fde4992e5c8a2343b867ca201b85f9e1aa", + "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", + "sha256:7aba9a312a620052133437f2363aae90ae4695ee61cb2ee07cbb9951d4c69ddd", + "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", + "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", + "sha256:83a7401d7520fa14b00d85d68bcca47a0676c69996e8515d53733972286f9739", + "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", + "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", + "sha256:87dab618fe116f1936f8461df5970fcf0befeba7531a36b0a86321332ff9c20b", + "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", + "sha256:910ce19a549f493fb55cfd1d7d70960706a03de6bfc896c1429fc5d6216df77e", + "sha256:9518514e3e85646bac798d94d34bf5b8741ee0cb580512e8450ce884f526b7cf", + "sha256:95bc81f83fadb67f7f87914f216a0e141555ee17fd7f56e25aa0cc165e99e53b", + "sha256:96e07d5a8b733d753e37d1f7138e7321d2316bb3f0f663ab4e3d500fabc82807", + "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", + "sha256:9a00408105901ede92e318eecb46d0e661d7d093d0a9b1224c71b5dd94f79e83", + "sha256:9d0c543bdeefa4794582ef48f3c59e5e7a43d672a4bfad9cbbd531e897911690", + "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", + "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", + "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", + "sha256:aca981fc00c8af8d5b9254ea5c2f276df8ece089b081af1ef4856fbcfc7c698a", + "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", + "sha256:b50ca218a3e2e23cbda395fd002d030385202fbb8182aa87e11bea0a568bd0b8", + "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", + "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", + "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", + "sha256:c2971af3a4094280f7c24293ff4d361689c175c1ebcbea6b3c1560eaff7cb240", + "sha256:c2af7a9d3afb63da31df1456d38b91555a6c147710a116d5cc70ab1e9f457a4f", + "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", + "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", + "sha256:cb711a66246561f1cae51244deef700eef75481a70d99611fd3c8ab5bd69ab49", + "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", + "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", + "sha256:d2a3526dbf6cb01b355e8867079c9356a8df48706b4b099ac0bf59d4656e610d", + "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", + "sha256:d5fe089be67bc5927f0c0bd60c082c79f22cf299635ee3ddd370ae2a6e8b4ae0", + "sha256:dc54a21586c096df73f06f9bdf594e8d86d7be84e5d4266358ce81c04c3cc88c", + "sha256:dcd4a7761fdfb5aaac88adad0a734dd065c038f5982a8c4b0dd28efa0bd9cc7c", + "sha256:dde02314eefb85dce3cfdd747e8b44c69a94d442c0d7221b7de151ee4c93f0f5", + "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", + "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", + "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", + "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", + "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", + "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", + "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", + "sha256:f199702740f3b766ed8c70efb885538be76cb48cd0cb596b948626f0b825e07a", + "sha256:f4695153333607e63068580f2979b377b641a03bc36e02813659ffbea2b76fe2", + "sha256:f6c602c5e3615abbf43dbdf3c6c64c65e76e5aa23cb74e18466b55d4a2095468", + "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", + "sha256:ff3d25883b7865ea34c00084dd22a7be7c58fd3131db6b25c35eafae84398f9d", + "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7" + ], + "markers": "python_version >= '3.9'", + "version": "==4.11.0" + }, + "pycparser": { + "hashes": [ + "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", + "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" + ], + "markers": "python_version >= '3.8'", + "version": "==2.23" + }, "pymongo": { - "extras": [ - "srv" - ], - "hashes": [ - "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857", - "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9", - "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05", - "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc", - "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1", - "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b", - "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b", - "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22", - "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e", - "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0", - "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52", - "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7", - "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13", - "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e", - "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e", - "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d", - "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978", - "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222", - "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755", - "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca", - "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240", - "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800", - "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693", - "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3", - "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024", - "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209", - "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7", - "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf", - "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7", - "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824", - "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919", - "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055", - "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55", - "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18", - "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81", - "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34", - "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff", - "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7", - "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c", - "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527", - "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a", - "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f", - "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0", - "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66", - "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3", - "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50", - "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1", - "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9", - "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8", - "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e", - "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83", - "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a", - "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88", - "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a", - "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b", - "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f", - "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152", - "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f", - "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35", - "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527", - "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5", - "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41", - "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2", - "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9", - "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684", - "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6", - "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4", - "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547", - "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46", - "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41", - "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb", - "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30", - "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7", - "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de", - "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e", - "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea", - "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26", - "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5", - "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a", - "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544", - "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023", - "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d", - "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5", - "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51", - "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e", - "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c", - "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93", - "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a", - "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2", - "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22", - "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018", - "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538", - "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d", - "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34", - "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0", - "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a", - "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb", - "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed" + "hashes": [ + "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584", + "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1", + "sha256:1246a82fa6dd73ac2c63aa7e463752d5d1ca91e0c7a23396b78f21273befd3a7", + "sha256:17d13458baf4a6a9f2e787d95adf8ec50d412accb9926a044bd1c41029c323b2", + "sha256:17fc94d1e067556b122eeb09e25c003268e8c0ea1f2f78e745b33bb59a1209c4", + "sha256:1f681722c9f27e86c49c2e8a838e61b6ecf2285945fd1798bd01458134257834", + "sha256:21c0a95a4db72562fd0805e2f76496bf432ba2e27a5651f4b9c670466260c258", + "sha256:292fd5a3f045751a823a54cdea75809b2216a62cc5f74a1a96b337db613d46a8", + "sha256:2c96dde79bdccd167b930a709875b0cd4321ac32641a490aebfa10bdcd0aa99b", + "sha256:2f3d66f7c495efc3cfffa611b36075efe86da1860a7df75522a6fe499ee10383", + "sha256:2fd3b99520f2bb013960ac29dece1b43f2f1b6d94351ca33ba1b1211ecf79a09", + "sha256:300eaf83ad053e51966be1839324341b08eaf880d3dc63ada7942d5912e09c49", + "sha256:3561fa96c3123275ec5ccf919e595547e100c412ec0894e954aa0da93ecfdb9e", + "sha256:390c4954c774eda280898e73aea36482bf20cba3ecb958dbb86d6a68b9ecdd68", + "sha256:39a13d8f7141294404ce46dfbabb2f2d17e9b1192456651ae831fa351f86fbeb", + "sha256:446417a34ff6c2411ce3809e17ce9a67269c9f1cb4966b01e49e0c590cc3c6b3", + "sha256:45aebbd369ca79b7c46eaea5b04d2e4afca4eda117b68965a07a9da05d774e4d", + "sha256:47ffb068e16ae5e43580d5c4e3b9437f05414ea80c32a1e5cac44a835859c259", + "sha256:482ca9b775747562ce1589df10c97a0e62a604ce5addf933e5819dd967c5e23c", + "sha256:49fd6e158cf75771b2685a8a221a40ab96010ae34dd116abd06371dc6c38ab60", + "sha256:4a0a054e9937ec8fdb465835509b176f6b032851c8648f6a5d1b19932d0eacd6", + "sha256:52f40c4b8c00bc53d4e357fe0de13d031c4cddb5d201e1a027db437e8d2887f8", + "sha256:58d0f4123855f05c0649f9b8ee083acc5b26e7f4afde137cd7b8dc03e9107ff3", + "sha256:5bf879a6ed70264574d4d8fb5a467c2a64dc76ecd72c0cb467c4464f849c8c77", + "sha256:5c78237e878e0296130e398151b0d4aa6c9eaf82e38fb6e0aaae2029bc7ef0ce", + "sha256:5c85a4c72b7965033f95c94c42dac27d886c01dbc23fe337ccb14f052a0ccc29", + "sha256:5f6feb678f26171f2a6b2cbb340949889154c7067972bd4cc129b62161474f08", + "sha256:6a054d282dd922ac400b6f47ea3ef58d8b940968d76d855da831dc739b7a04de", + "sha256:71413cd8f091ae25b1fec3af7c2e531cf9bdb88ce4079470e64835f6a664282a", + "sha256:76a8d4de8dceb69f6e06736198ff6f7e1149515ef946f192ff2594d2cc98fc53", + "sha256:77353978be9fc9e5fe56369682efed0aac5f92a2a1570704d62b62a3c9e1a24f", + "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5", + "sha256:7c0fd3de3a12ff0a8113a3f64cedb01f87397ab8eaaffa88d7f18ca66cd39385", + "sha256:7dd2a49f088890ca08930bbf96121443b48e26b02b84ba0a3e1ae2bf2c5a9b48", + "sha256:82a490f1ade4ec6a72068e3676b04c126e3043e69b38ec474a87c6444cf79098", + "sha256:86b1b5b63f4355adffc329733733a9b71fdad88f37a9dc41e163aed2130f9abc", + "sha256:89e45d7fa987f4e246cdf43ff001e3f911f73eb19ba9dabc2a6d80df5c97883b", + "sha256:8bd6dd736f5d07a825caf52c38916d5452edc0fac7aee43ec67aba6f61c2dbb7", + "sha256:8d4b01a48369ea6d5bc83fea535f56279f806aa3e4991189f0477696dd736289", + "sha256:90ad56bd1d769d2f44af74f0fd0c276512361644a3c636350447994412cbc9a1", + "sha256:9483521c03f6017336f54445652ead3145154e8d3ea06418e52cea57fee43292", + "sha256:959ef69c5e687b6b749fbf2140c7062abdb4804df013ae0507caabf30cba6875", + "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8", + "sha256:982107c667921e896292f4be09c057e2f1a40c645c9bfc724af5dd5fb8398094", + "sha256:9897a837677e3814873d0572f7e5d53c23ce18e274f3b5b87f05fb6eea22615b", + "sha256:9b03db2fe37c950aff94b29ded5c349b23729bccd90a0a5907bbf807d8c77298", + "sha256:9bc9f99e7702fdb0dcc3ff1dd490adc5d20b3941ad41e58f887d4998b9922a14", + "sha256:9df2db6bd91b07400879b6ec89827004c0c2b55fc606bb62db93cafb7677c340", + "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010", + "sha256:b33d59bf6fa1ca1d7d96d4fccff51e41312358194190d53ef70a84c070f5287e", + "sha256:b3a0ec660d61efb91c16a5962ec937011fe3572c4338216831f102e53d294e5c", + "sha256:b63bac343b79bd209e830aac1f5d9d552ff415f23a924d3e51abbe3041265436", + "sha256:bd0497c564b0ae34fb816464ffc09986dd9ca29e2772a0f7af989e472fecc2ad", + "sha256:c4fdd8e6eab8ff77c1c8041792b5f760d48508623cd10b50d5639e73f1eec049", + "sha256:c57dad9f289d72af1d7c47a444c4d9fa401f951cedbbcc54c7dd0c2107d6d786", + "sha256:c7eb497519f42ac89c30919a51f80e68a070cfc2f3b0543cac74833cd45a6b9c", + "sha256:cfa4a0a0f024a0336640e1201994e780a17bda5e6a7c0b4d23841eb9152e868b", + "sha256:d09d895c7f08bcbed4d2e96a00e52e9e545ae5a37b32d2dc10099b205a21fc6d", + "sha256:d2d4ca446348d850ac4a5c3dc603485640ae2e7805dbb90765c3ba7d79129b37", + "sha256:d66da207ccb0d68c5792eaaac984a0d9c6c8ec609c6bcfa11193a35200dc5992", + "sha256:dc583a1130e2516440b93bb2ecb55cfdac6d5373615ae472a9d1f26801f58749", + "sha256:dcff15b9157c16bc796765d4d3d151df669322acfb0357e4c3ccd056153f0ff4", + "sha256:de3bc878c3be54ae41c2cabc9e9407549ed4fec41f4e279c04e840dddd7c630c", + "sha256:e7cde58ef6470c0da922b65e885fb1ffe04deef81e526bd5dea429290fa358ca", + "sha256:e84dec392cf5f72d365e0aac73f627b0a3170193ebb038c3f7e7df11b7983ee7", + "sha256:f6b0513e5765fdde39f36e6a29a36c67071122b5efa748940ae51075beb5e4bc", + "sha256:fae552767d8e5153ed498f1bca92d905d0d46311d831eefb0f06de38f7695c95", + "sha256:fb384623ece34db78d445dd578a52d28b74e8319f4d9535fbaff79d0eae82b3d", + "sha256:fe4bcb8acfb288e238190397d4a699aeb4adb70e8545a6f4e44f99d4e8096ab1", + "sha256:ff99864085d2c7f4bb672c7167680ceb7d273e9a93c1a8074c986a36dbb71cc6", + "sha256:ffe217d2502f3fba4e2b0dc015ce3b34f157b66dfe96835aa64432e909dd0d95" ], "index": "pypi", - "version": "==3.12.0" + "markers": "python_version >= '3.9'", + "version": "==4.15.3" }, "python-dateutil": { "hashes": [ @@ -310,347 +1483,655 @@ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-dotenv": { "hashes": [ - "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", - "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" + "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", + "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.0.0" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==0.18.0" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "setuptools": { + "hashes": [ + "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", + "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==80.9.0" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" + }, + "tinycss2": { + "hashes": [ + "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", + "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], - "version": "==3.10.0.0" + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + ], + "markers": "python_version >= '3.9'", + "version": "==2.5.0" }, "uvloop": { "hashes": [ - "sha256:0de811931e90ae2da9e19ce70ffad73047ab0c1dba7c6e74f9ae1a3aabeb89bd", - "sha256:1ff05116ede1ebdd81802df339e5b1d4cab1dfbd99295bf27e90b4cec64d70e9", - "sha256:2d8ffe44ae709f839c54bacf14ed283f41bee90430c3b398e521e10f8d117b3a", - "sha256:5cda65fc60a645470b8525ce014516b120b7057b576fa876cdfdd5e60ab1efbb", - "sha256:63a3288abbc9c8ee979d7e34c34e780b2fbab3e7e53d00b6c80271119f277399", - "sha256:7522df4e45e4f25b50adbbbeb5bb9847495c438a628177099d2721f2751ff825", - "sha256:7f4b8a905df909a407c5791fb582f6c03b0d3b491ecdc1cdceaefbc9bf9e08f6", - "sha256:905f0adb0c09c9f44222ee02f6b96fd88b493478fffb7a345287f9444e926030", - "sha256:ae2b325c0f6d748027f7463077e457006b4fdb35a8788f01754aadba825285ee", - "sha256:e71fb9038bfcd7646ca126c5ef19b17e48d4af9e838b2bcfda7a9f55a6552a32" + "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772", + "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", + "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", + "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", + "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", + "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", + "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", + "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", + "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", + "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", + "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", + "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", + "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", + "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", + "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", + "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193", + "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", + "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", + "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", + "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", + "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", + "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242", + "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", + "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", + "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6", + "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", + "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", + "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", + "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", + "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", + "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", + "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa", + "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", + "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", + "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", + "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", + "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", + "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", + "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", + "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", + "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", + "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", + "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820", + "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", + "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", + "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", + "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", + "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", + "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42" ], "markers": "sys_platform != 'win32'", - "version": "==0.15.3" + "version": "==0.22.1" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" }, "yarl": { "hashes": [ - "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" + "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", + "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", + "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", + "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", + "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", + "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", + "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", + "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", + "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", + "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", + "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", + "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", + "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", + "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", + "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", + "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", + "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", + "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", + "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", + "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", + "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", + "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", + "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", + "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", + "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", + "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", + "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", + "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", + "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", + "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", + "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", + "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", + "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", + "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", + "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", + "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", + "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", + "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", + "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", + "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", + "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", + "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", + "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", + "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", + "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", + "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", + "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", + "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", + "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", + "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", + "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", + "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", + "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", + "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", + "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", + "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", + "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", + "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", + "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", + "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", + "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", + "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", + "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", + "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", + "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", + "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", + "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", + "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", + "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", + "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", + "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", + "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", + "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", + "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", + "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", + "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", + "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", + "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", + "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", + "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", + "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", + "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", + "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", + "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", + "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", + "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", + "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", + "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", + "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", + "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", + "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", + "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", + "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", + "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", + "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", + "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", + "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", + "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", + "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", + "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", + "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", + "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", + "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", + "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", + "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", + "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", + "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", + "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", + "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", + "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", + "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", + "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", + "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", + "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", + "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", + "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", + "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", + "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", + "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", + "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", + "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", + "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", + "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", + "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", + "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", + "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", + "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", + "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", + "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", + "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249" ], - "markers": "python_version >= '3.6'", - "version": "==1.6.3" + "markers": "python_version >= '3.9'", + "version": "==1.22.0" + }, + "zstandard": { + "hashes": [ + "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", + "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", + "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3", + "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", + "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", + "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936", + "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", + "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", + "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", + "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", + "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", + "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", + "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", + "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", + "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362", + "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649", + "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb", + "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", + "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", + "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", + "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", + "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", + "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", + "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", + "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", + "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", + "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860", + "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", + "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53", + "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", + "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", + "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", + "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", + "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2", + "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", + "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", + "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", + "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388", + "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530", + "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577", + "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", + "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", + "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", + "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", + "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", + "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", + "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", + "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", + "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", + "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", + "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", + "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", + "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", + "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", + "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", + "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", + "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27", + "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", + "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", + "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", + "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", + "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", + "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", + "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", + "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", + "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", + "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", + "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", + "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df", + "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", + "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", + "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", + "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", + "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0", + "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", + "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", + "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", + "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7", + "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", + "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", + "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", + "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751", + "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", + "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", + "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", + "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", + "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", + "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", + "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", + "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", + "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", + "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", + "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", + "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", + "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", + "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", + "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", + "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", + "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01" + ], + "markers": "python_version >= '3.9'", + "version": "==0.25.0" } }, "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, "astroid": { "hashes": [ - "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925", - "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f" + "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", + "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" ], - "markers": "python_version ~= '3.6'", - "version": "==2.6.5" + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.3" }, "bandit": { "hashes": [ - "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07", - "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608" + "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", + "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b" ], "index": "pypi", - "version": "==1.7.0" + "markers": "python_version >= '3.9'", + "version": "==1.8.6" }, "black": { "hashes": [ - "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04", - "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7" + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", - "version": "==21.6b0" + "markers": "python_version >= '3.8'", + "version": "==23.11.0" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", + "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "markers": "python_version >= '3.10'", + "version": "==8.3.0" }, - "colorama": { + "dill": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", + "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" ], - "index": "pypi", - "version": "==0.4.4" + "markers": "python_version >= '3.8'", + "version": "==0.4.0" }, - "gitdb": { + "isort": { "hashes": [ - "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", - "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], - "markers": "python_version >= '3.4'", - "version": "==4.0.7" + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" }, - "gitpython": { + "markdown-it-py": { "hashes": [ - "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b", - "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8" + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" ], - "markers": "python_version >= '3.6'", - "version": "==3.1.18" + "markers": "python_version >= '3.10'", + "version": "==4.0.0" }, - "isort": { + "mccabe": { "hashes": [ - "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813", - "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.9.2" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", - "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", - "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", - "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", - "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", - "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", - "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", - "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", - "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", - "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", - "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", - "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", - "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", - "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", - "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", - "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", - "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", - "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", - "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", - "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", - "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", - "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.6.0" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, - "mccabe": { + "mdurl": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" ], - "version": "==0.6.1" + "markers": "python_version >= '3.7'", + "version": "==0.1.2" }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], - "version": "==0.4.3" + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==23.2" }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", + "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3" ], - "version": "==0.9.0" + "markers": "python_version >= '3.10'", + "version": "==4.5.0" }, - "pbr": { + "pygments": { "hashes": [ - "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", - "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" ], - "markers": "python_version >= '2.6'", - "version": "==5.6.0" + "markers": "python_version >= '3.8'", + "version": "==2.19.2" }, "pylint": { "hashes": [ - "sha256:1f333dc72ef7f5ea166b3230936ebcfb1f3b722e76c980cb9fe6b9f95e8d3172", - "sha256:748f81e5776d6273a6619506e08f1b48ff9bcb8198366a56821cf11aac14fc87" + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" ], "index": "pypi", - "version": "==2.9.5" + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" - }, - "regex": { - "hashes": [ - "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", - "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", - "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", - "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", - "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", - "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", - "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", - "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", - "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", - "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", - "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", - "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", - "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", - "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", - "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", - "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", - "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", - "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", - "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", - "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", - "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", - "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", - "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", - "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", - "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", - "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", - "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", - "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", - "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", - "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", - "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", - "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", - "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", - "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", - "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", - "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", - "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", - "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", - "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", - "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", - "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" - ], - "version": "==2021.7.6" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '3.8'", + "version": "==6.0.3" }, - "smmap": { + "rich": { "hashes": [ - "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", - "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" + "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", + "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" ], - "markers": "python_version >= '3.5'", - "version": "==4.0.0" + "markers": "python_full_version >= '3.8.0'", + "version": "==14.2.0" }, "stevedore": { "hashes": [ - "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", - "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" + "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf", + "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73" ], - "markers": "python_version >= '3.6'", - "version": "==3.3.0" + "markers": "python_version >= '3.9'", + "version": "==5.5.0" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", + "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", + "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", + "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", + "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", + "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", + "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", + "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", + "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", + "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", + "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", + "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", + "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", + "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", + "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", + "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", + "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", + "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", + "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", + "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", + "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", + "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", + "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", + "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", + "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", + "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", + "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", + "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", + "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", + "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", + "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", + "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, - "wrapt": { + "tomlkit": { "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", + "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" ], - "version": "==1.12.1" + "markers": "python_version >= '3.8'", + "version": "==0.13.3" } } } diff --git a/README.md b/README.md index b95dc335a4..58243cab61 100644 --- a/README.md +++ b/README.md @@ -6,36 +6,36 @@
- +
- + - + Bot instances - - Support + + Support - - Python 3.7 + + Buy Me A Coffee - Made with Python 3.7 + Made with Python 3.10 - + MIT License @@ -48,13 +48,15 @@ Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. -This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! +This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Buy Me A Coffee](https://buymeacoffee.com/modmaildev)**, cool benefits included! + +For up-to-date setup instructions, please visit our [**documentation**](https://docs.modmail.dev/installation) page. ## How does it work? When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.dev/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. ~~Here is an [**example**](https://logs.modmail.dev/example)~~ (demo not available at the moment). ## Features @@ -67,10 +69,10 @@ Our Logviewer will save the threads so you can view previous threads through the * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** - * When you close a thread, Modmail will generate a [log link](https://logs.modmail.dev/example) and post it to your log channel. + * When you close a thread, Modmail will generate a log link and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. - * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). + * Login via Discord to protect your logs ([premium feature](https://buymeacoffee.com/modmaildev/membership)). * See past logs of a user with `?logs`. * Searchable by text queries using `?logs search`. @@ -84,79 +86,34 @@ This list is ever-growing thanks to active development and our exceptional contr ## Installation -Where can I find the Modmail bot invite link? - -Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). - -### Heroku - -You can host this bot on Heroku. - -Installation via Heroku is possible with your web browser alone. -The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. - -To configure automatic updates: - - Login to [GitHub](https://github.com/) and verify your account. - - [Fork the repo](https://github.com/kyb3r/modmail/fork). - - Install the [Pull app](https://github.com/apps/pull) for your fork. - - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - - Turn on auto-deploy for the `master` branch. - -### Hosting for Patreons - -If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +There are a number of options for hosting your very own dedicated Modmail bot. -### Locally +Visit our [**documentation**](https://docs.modmail.dev/installation) page for detailed guidance on how to deploy your Modmail bot. -Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). +### Paid Hosting -Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. +If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Buy Me A Coffee**](https://buymeacoffee.com/modmaildev/membership). -Clone the repo: +## FAQ -```console -$ git clone https://github.com/kyb3r/modmail -$ cd modmail -``` +**Q: Where can I find the Modmail bot invite link?** -Install dependencies: +**A:** Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following our [**documentation**](https://docs.modmail.dev/installation) steps or subscribe to [**Buy Me A Coffee**](https://buymeacoffee.com/modmaildev/membership). -```console -$ pipenv install -``` +**Q: Where can I find out more info about Modmail?** -Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is nonexistent (hidden), create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values. +**A:** You can find more info about Modmail on our [**documentation**](https://docs.modmail.dev) page. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for help and support. -Finally, start Modmail. - -```console -$ pipenv run bot -``` - -#### Docker - -This repo supplies a Dockerfile for simplified deployment. - -You can build your own Docker image: - -```console -$ docker build . --tag=modmail -``` - -Or run directly from a pre-built version from https://hub.docker.com/. +## Plugins -- Kyber's: +Modmail supports the use of third-party plugins to extend or add functionalities to the bot. +Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. -```console -$ docker pull kyb3rr/modmail -``` +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/modmail-dev/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. -And to run your docker image: +To develop your own, check out the [plugins documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). -```console -$ docker run --env-file .env kyb3rr/modmail -``` -- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. +Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/cnUpwrnpYb). ## Sponsors @@ -195,29 +152,24 @@ Discord Advice Center: +
+
+Kistó Bakery: +
+ + + - -Become a sponsor on [Patreon](https://patreon.com/kyber). - -## Plugins - -Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. - -You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. - -To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). - -Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/j5e9p8w). +Become a sponsor on [Buy Me A Coffee](https://buymeacoffee.com/modmaildev/membership). ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/modmail-dev/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. -If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! +If you like this project and would like to show your appreciation, support us on **[Buy Me A Coffee](https://buymeacoffee.com/modmaildev)**! ## Beta Testing -Our [development](https://github.com/kyb3r/modmail/tree/development) branch is where most of our features are tested before public release. Be warned that there could be bugs in various commands so keep it away from any large servers you manage. +Our [development](https://github.com/modmail-dev/modmail/tree/development) branch is where most of our features are tested before public release. Be warned that there could be bugs in various commands so keep it away from any large servers you manage. If you wish to test the new features and play around with them, feel free to join our [Public Test Server](https://discord.gg/v5hTjKC). Bugs can be raised within that server or in our Github issues (state that you are using the development branch though). diff --git a/SPONSORS.json b/SPONSORS.json index ce34dc3f9d..955b839fd7 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -81,7 +81,7 @@ "icon_url": "https://i.imgur.com/cjVtRw5.jpg" }, "image": { - "url": "https://i.imgur.com/1hrjcHd.png" + "url": "https://i.imgur.com/1hrjcHd.png" }, "fields": [ { @@ -91,23 +91,6 @@ ] } }, - { - "embed": { - "title": "Advertise Your Server", - "description": "Advertise Your Server is the leading advertising and growth Discord Server. With over 60,000 members we can help grow your community with our range of services.\n\n__**Advertise Your Server offers everything you need to grow and find servers:**__\n\n:chart_with_upwards_trend: **Discord Growth Experts** to give you advice on how to __grow your server.__ (server/advert reviews, growth tips)\n:dividers: Over 40 different channels for **different server categories.**\n:robot: Our own __custom__ **bump bot.** (Liam)\n:bar_chart: Currently the __BIGGEST__ advertising server on Discord.\n:computer: Our own server __Listing Site__!\n:ticket: Small Servers Program for servers with less than 300 members.\n:dvd: Weekly Podcast, Blog, Email Newsletter and YouTube Tutorials. \n\nhttps://discord.gg/zP8KcF4VQz\nhttps://aysdiscord.com", - "author": { - "name": "Advertise Your Server", - "icon_url": "https://cdn.discordapp.com/attachments/563522692418895872/907067815486427176/logo4.png" - }, - "color": 431075, - "footer": { - "text": "Grow Your Discord Server" - }, - "image": { - "url": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" - } - } - }, { "embed": { "footer": { @@ -128,5 +111,41 @@ "url": "https://discord.gg/uncommon", "title": "uncommon community" } + }, + { + "embed": { + "description": "> Be apart of our community as we start to grow! and embark on a long journey.\n——————————————————-\n**What we offer?**\n\n➺〚🖌️〛Custom Liveries \n➺〚❤️〛Friendly and Growing community.\n➺〚🤝〛Partnerships.\n➺〚🎮〛Daily SSUs. \n➺〚🚨〛Great roleplays.\n➺〚💬〛Kind and Professional staff\n➺〚🎉〛Giveaways!!! \n——————————————————-\n**Emergency Services**\n\n➺〚🚔〛NY Police Force\n➺〚🚒〛Fire & Emergency NY\n➺〚🚧〛NY department of transportation \n\n——————————————————-\n**Whitelisted**\nComing soon!\n——————————————————-\n**What are we looking for!**\n\n➺〚💬〛More members\n➺〚⭐〛Staff Members - **WE'RE HIRING!**\n➺〚🤝〛Partnerships\n➺〚💎〛Boosters\n——————————————————\n\n**[Join now](https://discord.com/invite/qt62qSnKVa)**", + "author": { + "name": "New York Roleplay", + "icon_url": "https://cdn.discordapp.com/icons/1172553254882775111/648d5bc50393a21216527a1aaa61286d.webp" + }, + "color": 431075, + "thumbnail": { + "url": "https://cdn.discordapp.com/icons/1172553254882775111/648d5bc50393a21216527a1aaa61286d.webp" + } + } + }, + { + "embed": { + "title": "CityStore PLC", + "description": "*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", + "color": 15523550 + } + }, + { + "embed": { + "description": "✨ *\"Let's bake it!\"* ✨ \n\nKistó is a very successful and well-known **Bakery Group** on the platform. Its goal is to give every guest the **ultimate bakery experience**. \n\nWe have a wide variety of hand-made treats, from our rich **drinks** to our freshly baked **pastries**. Every item is made with care by our **skilled and passionate team**, redefining what it means to be a modern bakery. \n\n💖 Come visit us today and taste the *sweet side of perfection* at **Kistó Bakery.** \n\n**Roblox Group:** [Click here!](https://www.roblox.com/communities/9318596/Kist#!/about) \n**Discord Server:** [Click here!](https://discord.gg/aGt8Wv3gP9)", + "color": 16736255, + "author": { + "name": "Kistó Bakery", + "icon_url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475883298887/noFilter_10_1.png" + }, + "footer": { + "text": "Proudly serving since 2021" + }, + "image": { + "url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475543429260/0904.png" + } + } } ] diff --git a/app.json b/app.json index 66b5c77752..decd58695c 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "Modmail", "description": "An easy to install Modmail bot for Discord - DM to contact mods!", - "repository": "https://github.com/kyb3r/modmail", + "repository": "https://github.com/modmail-dev/modmail", "env": { "TOKEN": { "description": "Your discord bot's token.", @@ -11,10 +11,6 @@ "description": "The id for the server you are hosting this bot for.", "required": true }, - "MODMAIL_GUILD_ID": { - "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", - "required": false - }, "OWNERS": { "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", "required": true @@ -34,6 +30,10 @@ "GITHUB_TOKEN": { "description": "A github personal access token with the repo scope.", "required": false + }, + "REGISTRY_PLUGINS_ONLY": { + "description": "If set to true, only plugins that are in the registry can be loaded.", + "required": false } } -} \ No newline at end of file +} diff --git a/bot.py b/bot.py index 0ee38be340..6176ac5824 100644 --- a/bot.py +++ b/bot.py @@ -1,28 +1,27 @@ -__version__ = "3.10.3" +__version__ = "4.2.1" import asyncio import copy -import logging +import hashlib import os import re -import signal import string +import struct import sys +import platform import typing -from datetime import datetime +from datetime import datetime, timezone from subprocess import PIPE from types import SimpleNamespace import discord import isodate -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientResponseError from discord.ext import commands, tasks from discord.ext.commands.view import StringView -from emoji import UNICODE_EMOJI -from pkg_resources import parse_version - -from core.utils import tryint +from emoji import is_emoji +from packaging.version import Version try: @@ -48,7 +47,15 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import normalize_alias, truncate +from core.utils import ( + extract_block_timestamp, + normalize_alias, + parse_alias, + truncate, + tryint, + human_join, + extract_forwarded_content, +) logger = getLogger(__name__) @@ -65,32 +72,80 @@ class ModmailBot(commands.Bot): def __init__(self): + self.config = ConfigManager(self) + self.config.populate_cache() + intents = discord.Intents.all() + if not self.config["enable_presence_intent"]: + intents.presences = False + super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` - self._session = None + self.session = None self._api = None - self.metadata_loop = None - self.autoupdate_loop = None self.formatter = SafeFormatter() - self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - self._connected = asyncio.Event() - self.start_time = datetime.utcnow() + self.loaded_cogs = [ + "cogs.modmail", + "cogs.plugins", + "cogs.utility", + "cogs.threadmenu", + ] + self._connected = None + self.start_time = discord.utils.utcnow() self._started = False - self.config = ConfigManager(self) - self.config.populate_cache() - self.threads = ThreadManager(self) + self._message_queues = {} # User ID -> asyncio.Queue for message ordering - self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") - self._configure_logging() + log_dir = os.path.join(temp_dir, "logs") + if not os.path.exists(log_dir): + os.mkdir(log_dir) + self.log_file_path = os.path.join(log_dir, "modmail.log") + configure_logging(self) self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() + def get_guild_icon( + self, + guild: typing.Optional[discord.Guild], + *, + size: typing.Optional[int] = None, + ) -> str: + if guild is None: + guild = self.guild + if guild.icon is None: + return "https://cdn.discordapp.com/embed/avatars/0.png" + if size is None: + return guild.icon.url + return guild.icon.with_size(size).url + + def _resolve_snippet(self, name: str) -> typing.Optional[str]: + """ + Get actual snippet names from direct aliases to snippets. + + If the provided name is a snippet, it's returned unchanged. + If there is an alias by this name, it is parsed to see if it + refers only to a snippet, in which case that snippet name is + returned. + + If no snippets were found, None is returned. + """ + if name in self.snippets: + return name + + try: + (command,) = parse_alias(self.aliases[name]) + except (KeyError, ValueError): + # There is either no alias by this name present or the + # alias has multiple steps. + pass + else: + if command in self.snippets: + return command + @property def uptime(self) -> str: - now = datetime.utcnow() + now = discord.utils.utcnow() delta = now - self.start_time hours, remainder = divmod(int(delta.total_seconds()), 3600) minutes, seconds = divmod(remainder, 60) @@ -133,47 +188,21 @@ def startup(self): logger.info("discord.py: v%s", discord.__version__) logger.line() + async def load_extensions(self): for cog in self.loaded_cogs: + if cog in self.extensions: + continue logger.debug("Loading %s.", cog) try: - self.load_extension(cog) + await self.load_extension(cog) logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) logger.line("debug") - def _configure_logging(self): - level_text = self.config["log_level"].upper() - logging_levels = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, - } - logger.line() - - log_level = logging_levels.get(level_text) - if log_level is None: - log_level = self.config.remove("log_level") - logger.warning("Invalid logging level set: %s.", level_text) - logger.warning("Using default logging level: INFO.") - else: - logger.info("Logging level: %s", level_text) - - logger.info("Log file: %s", self.log_file_name) - configure_logging(self.log_file_name, log_level) - logger.debug("Successfully configured logging.") - @property def version(self): - return parse_version(__version__) - - @property - def session(self) -> ClientSession: - if self._session is None: - self._session = ClientSession(loop=self.loop) - return self._session + return Version(__version__) @property def api(self) -> ApiClient: @@ -194,106 +223,74 @@ async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] def run(self): - loop = self.loop + async def runner(): + async with self: + self._connected = asyncio.Event() + self.session = ClientSession(loop=self.loop) - try: - loop.add_signal_handler(signal.SIGINT, lambda: loop.stop()) - loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) - except NotImplementedError: - pass + if self.config["enable_presence_intent"]: + logger.info("Starting bot with presence intent.") + else: + logger.info("Starting bot without presence intent.") - async def runner(): - try: - retry_intents = False try: await self.start(self.token) except discord.PrivilegedIntentsRequired: - retry_intents = True - if retry_intents: - await self.http.close() - if self.ws is not None and self.ws.open: - await self.ws.close(code=1000) - self._ready.clear() - intents = discord.Intents.default() - intents.members = True - # Try again with members intent - self._connection._intents = intents - logger.warning( - "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." ) - await self.start(self.token) - except discord.PrivilegedIntentsRequired: - logger.critical( - "Privileged intents are not explicitly granted in the discord developers dashboard." - ) - except discord.LoginFailure: - logger.critical("Invalid token") - except Exception: - logger.critical("Fatal exception", exc_info=True) - finally: - if not self.is_closed(): - await self.close() - if self._session: - await self._session.close() - - # noinspection PyUnusedLocal - def stop_loop_on_completion(f): - loop.stop() - - def _cancel_tasks(): - if sys.version_info < (3, 8): - task_retriever = asyncio.Task.all_tasks - else: + except discord.LoginFailure: + logger.critical("Invalid token") + except Exception: + logger.critical("Fatal exception", exc_info=True) + finally: + if self.session: + await self.session.close() + if not self.is_closed(): + await self.close() + + async def _cancel_tasks(): + async with self: task_retriever = asyncio.all_tasks + loop = self.loop + tasks = {t for t in task_retriever() if not t.done() and t.get_coro() != cancel_tasks_coro} - tasks = {t for t in task_retriever(loop=loop) if not t.done()} - - if not tasks: - return + if not tasks: + return - logger.info("Cleaning up after %d tasks.", len(tasks)) - for task in tasks: - task.cancel() - - loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) - logger.info("All tasks finished cancelling.") - - for task in tasks: - if task.cancelled(): - continue - if task.exception() is not None: - loop.call_exception_handler( - { - "message": "Unhandled exception during Client.run shutdown.", - "exception": task.exception(), - "task": task, - } - ) + logger.info("Cleaning up after %d tasks.", len(tasks)) + for task in tasks: + task.cancel() + + await asyncio.gather(*tasks, return_exceptions=True) + logger.info("All tasks finished cancelling.") + + for task in tasks: + try: + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "Unhandled exception during Client.run shutdown.", + "exception": task.exception(), + "task": task, + } + ) + except (asyncio.InvalidStateError, asyncio.CancelledError): + pass - future = asyncio.ensure_future(runner(), loop=loop) - future.add_done_callback(stop_loop_on_completion) try: - loop.run_forever() - except KeyboardInterrupt: + asyncio.run(runner(), debug=bool(os.getenv("DEBUG_ASYNCIO"))) + except (KeyboardInterrupt, SystemExit): logger.info("Received signal to terminate bot and event loop.") finally: - future.remove_done_callback(stop_loop_on_completion) logger.info("Cleaning up tasks.") try: - _cancel_tasks() - if sys.version_info >= (3, 6): - loop.run_until_complete(loop.shutdown_asyncgens()) + cancel_tasks_coro = _cancel_tasks() + asyncio.run(cancel_tasks_coro) finally: logger.info("Closing the event loop.") - if not future.cancelled(): - try: - return future.result() - except KeyboardInterrupt: - # I am unsure why this gets raised here but suppress it anyway - return None - @property def bot_owner_ids(self): owner_ids = self.config["owners"] @@ -327,7 +324,10 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: try: channel = self.main_category.channels[0] self.config["log_channel_id"] = channel.id - logger.warning("No log channel set, setting #%s to be the log channel.", channel.name) + logger.warning( + "No log channel set, setting #%s to be the log channel.", + channel.name, + ) return channel except IndexError: pass @@ -523,6 +523,7 @@ async def on_connect(self): logger.debug("Connected to gateway.") await self.config.refresh() await self.api.setup_indexes() + await self.load_extensions() self._connected.set() async def on_ready(self): @@ -557,6 +558,13 @@ async def on_ready(self): logger.info("Receiving guild ID: %s", self.modmail_guild.id) logger.line() + if "dev" in __version__: + logger.warning( + "You are running a developmental version. This should not be used in production. (v%s)", + __version__, + ) + logger.line() + await self.threads.populate_cache() # closures @@ -565,12 +573,18 @@ async def on_ready(self): logger.line() for recipient_id, items in tuple(closures.items()): - after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds() + after = ( + datetime.fromisoformat(items["time"]).astimezone(timezone.utc) - discord.utils.utcnow() + ).total_seconds() if after <= 0: logger.debug("Closing thread for recipient %s.", recipient_id) after = 0 else: - logger.debug("Thread for recipient %s will be closed after %s seconds.", recipient_id, after) + logger.debug( + "Thread for recipient %s will be closed after %s seconds.", + recipient_id, + after, + ) thread = await self.threads.find(recipient_id=int(recipient_id)) @@ -582,7 +596,7 @@ async def on_ready(self): continue await thread.close( - closer=self.get_user(items["closer_id"]), + closer=await self.get_or_fetch_user(items["closer_id"]), after=after, silent=items["silent"], delete_channel=items["delete_channel"], @@ -591,20 +605,20 @@ async def on_ready(self): ) for log in await self.api.get_open_logs(): - if self.get_channel(int(log["channel_id"])) is None: + if log.get("channel_id") is None or self.get_channel(int(log["channel_id"])) is None: logger.debug("Unable to resolve thread with channel %s.", log["channel_id"]) log_data = await self.api.post_log( log["channel_id"], { "open": False, "title": None, - "closed_at": str(datetime.utcnow()), + "closed_at": str(discord.utils.utcnow()), "close_message": "Channel has been deleted, no closer found.", "closer": { "id": str(self.user.id), "name": self.user.name, "discriminator": self.user.discriminator, - "avatar_url": str(self.user.avatar_url), + "avatar_url": self.user.display_avatar.url, "mod": True, }, }, @@ -612,52 +626,57 @@ async def on_ready(self): if log_data: logger.debug("Successfully closed thread with channel %s.", log["channel_id"]) else: - logger.debug("Failed to close thread with channel %s, skipping.", log["channel_id"]) - - if self.config.get("data_collection"): - self.metadata_loop = tasks.Loop( - self.post_metadata, - seconds=0, - minutes=0, - hours=1, - count=None, - reconnect=True, - loop=None, - ) - self.metadata_loop.before_loop(self.before_post_metadata) - self.metadata_loop.start() - - self.autoupdate_loop = tasks.Loop( - self.autoupdate, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None - ) - self.autoupdate_loop.before_loop(self.before_autoupdate) - self.autoupdate_loop.start() + logger.debug( + "Failed to close thread with channel %s, skipping.", + log["channel_id"], + ) other_guilds = [guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}] if any(other_guilds): logger.warning( "The bot is in more servers other than the main and staff server. " "This may cause data compromise (%s).", - ", ".join(guild.name for guild in other_guilds), + ", ".join(str(guild.name) for guild in other_guilds), ) logger.warning("If the external servers are valid, you may ignore this message.") + self.post_metadata.start() + self.autoupdate.start() + self.log_expiry.start() self._started = True async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() - if name not in UNICODE_EMOJI["en"]: + if not is_emoji(name): try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument as e: - logger.warning("%s is not a valid emoji. %s.", name, e) + logger.warning("%s is not a valid emoji: %s", name, e) raise return name - async def retrieve_emoji(self) -> typing.Tuple[str, str]: + async def get_or_fetch_user(self, id: int) -> discord.User: + """ + Retrieve a User based on their ID. + + This tries getting the user from the cache and falls back to making + an API call if they're not found in the cache. + """ + return self.get_user(id) or await self.fetch_user(id) + + @staticmethod + async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> typing.Optional[discord.Member]: + """ + Attempt to get a member from cache; on failure fetch from the API. + Returns: + The :obj:`discord.Member` or :obj:`None` to indicate the member could not be found. + """ + return guild.get_member(member_id) or await guild.fetch_member(member_id) + + async def retrieve_emoji(self) -> typing.Tuple[str, str]: sent_emoji = self.config["sent_emoji"] blocked_emoji = self.config["blocked_emoji"] @@ -681,7 +700,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: def check_account_age(self, author: discord.Member) -> bool: account_age = self.config.get("account_age") - now = datetime.utcnow() + now = discord.utils.utcnow() try: min_account_age = author.created_at + account_age @@ -695,7 +714,7 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = f"System Message: New Account. Required to wait for {delta}." + new_reason = f"System Message: New Account. User can try again {delta}." self.blocked_users[str(author.id)] = new_reason return False @@ -703,7 +722,7 @@ def check_account_age(self, author: discord.Member) -> bool: def check_guild_age(self, author: discord.Member) -> bool: guild_age = self.config.get("guild_age") - now = datetime.utcnow() + now = discord.utils.utcnow() if not hasattr(author, "joined_at"): logger.warning("Not in guild, cannot verify guild_age, %s.", author.name) @@ -721,7 +740,7 @@ def check_guild_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to guild age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = f"System Message: Recently Joined. Required to wait for {delta}." + new_reason = f"System Message: Recently Joined. User can try again {delta}." self.blocked_users[str(author.id)] = new_reason return False @@ -731,23 +750,14 @@ def check_manual_blocked_roles(self, author: discord.Member) -> bool: if isinstance(author, discord.Member): for r in author.roles: if str(r.id) in self.blocked_roles: - blocked_reason = self.blocked_roles.get(str(r.id)) or "" - now = datetime.utcnow() - - # etc "blah blah blah... until 2019-10-14T21:12:45.559948." - end_time = re.search(r"until ([^`]+?)\.$", blocked_reason) - if end_time is None: - # backwards compat - end_time = re.search(r"%([^%]+?)%", blocked_reason) - if end_time is not None: - logger.warning( - r"Deprecated time message for role %s, block and unblock again to update.", - r.name, - ) + + try: + end_time, after = extract_block_timestamp(blocked_reason, author.id) + except ValueError: + return False if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.blocked_roles.pop(str(r.id)) @@ -763,26 +773,19 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return True blocked_reason = self.blocked_users.get(str(author.id)) or "" - now = datetime.utcnow() if blocked_reason.startswith("System Message:"): # Met the limits already, otherwise it would've been caught by the previous checks logger.debug("No longer internally blocked, user %s.", author.name) self.blocked_users.pop(str(author.id)) return True - # etc "blah blah blah... until 2019-10-14T21:12:45.559948." - end_time = re.search(r"until ([^`]+?)\.$", blocked_reason) - if end_time is None: - # backwards compat - end_time = re.search(r"%([^%]+?)%", blocked_reason) - if end_time is not None: - logger.warning( - r"Deprecated time message for user %s, block and unblock again to update.", - author.name, - ) + + try: + end_time, after = extract_block_timestamp(blocked_reason, author.id) + except ValueError: + return False if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.blocked_users.pop(str(author.id)) @@ -805,7 +808,6 @@ async def is_blocked( channel: discord.TextChannel = None, send_message: bool = False, ) -> bool: - member = self.guild.get_member(author.id) if member is None: # try to find in other guilds @@ -852,7 +854,7 @@ async def is_blocked( async def get_thread_cooldown(self, author: discord.Member): thread_cooldown = self.config.get("thread_cooldown") - now = datetime.utcnow() + now = discord.utils.utcnow() if thread_cooldown == isodate.Duration(): return @@ -870,10 +872,12 @@ async def get_thread_cooldown(self, author: discord.Member): return try: - cooldown = datetime.fromisoformat(last_log_closed_at) + thread_cooldown + cooldown = datetime.fromisoformat(last_log_closed_at).astimezone(timezone.utc) + thread_cooldown except ValueError: logger.warning("Error with 'thread_cooldown'.", exc_info=True) - cooldown = datetime.fromisoformat(last_log_closed_at) + self.config.remove("thread_cooldown") + cooldown = datetime.fromisoformat(last_log_closed_at).astimezone( + timezone.utc + ) + self.config.remove("thread_cooldown") if cooldown > now: # User messaged before thread cooldown ended @@ -884,16 +888,47 @@ async def get_thread_cooldown(self, author: discord.Member): @staticmethod async def add_reaction( - msg, reaction: typing.Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str] + msg, + reaction: typing.Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str], ) -> bool: if reaction != "disable": try: await msg.add_reaction(reaction) - except (discord.HTTPException, discord.InvalidArgument) as e: + except (discord.HTTPException, TypeError) as e: logger.warning("Failed to add reaction %s: %s.", reaction, e) return False return True + async def _queue_dm_message(self, message: discord.Message) -> None: + """Queue DM messages to ensure they're processed in order per user.""" + user_id = message.author.id + + if user_id not in self._message_queues: + self._message_queues[user_id] = asyncio.Queue() + # Start processing task for this user + self.loop.create_task(self._process_user_messages(user_id)) + + await self._message_queues[user_id].put(message) + + async def _process_user_messages(self, user_id: int) -> None: + """Process messages for a specific user in order.""" + queue = self._message_queues[user_id] + + while True: + try: + # Wait for a message with timeout to clean up inactive queues + message = await asyncio.wait_for(queue.get(), timeout=300) # 5 minutes + await self.process_dm_modmail(message) + queue.task_done() + except asyncio.TimeoutError: + # Clean up inactive queue + if queue.empty(): + self._message_queues.pop(user_id, None) + break + except Exception as e: + logger.error(f"Error processing message for user {user_id}: {e}", exc_info=True) + queue.task_done() + async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" blocked = await self._process_blocked(message) @@ -901,10 +936,202 @@ async def process_dm_modmail(self, message: discord.Message) -> None: return sent_emoji, blocked_emoji = await self.retrieve_emoji() - if message.type != discord.MessageType.default: + # Handle forwarded messages (Discord forwards) + # See: https://discord.com/developers/docs/resources/message#message-reference-content-attribution-forwards + # 1. Multi-forward (message_snapshots) + if hasattr(message, "flags") and getattr(message.flags, "has_snapshot", False): + if hasattr(message, "message_snapshots") and message.message_snapshots: + thread = await self.threads.find(recipient=message.author) + if thread is None: + delta = await self.get_thread_cooldown(message.author) + if delta: + await message.channel.send( + embed=discord.Embed( + title=self.config["cooldown_thread_title"], + description=self.config["cooldown_thread_response"].format(delta=delta), + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = await self.threads.create(message.author, message=message) + else: + if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info("A message was blocked from %s due to disabled Modmail.", message.author) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + # Extract forwarded content using utility function + combined_content = extract_forwarded_content(message) or "[Forwarded message with no content]" + + class ForwardedMessage: + def __init__(self, original_message, forwarded_content): + self.author = original_message.author + self.content = forwarded_content + self.attachments = [] + self.stickers = [] + self.created_at = original_message.created_at + self.embeds = [] + self.id = original_message.id + self.flags = original_message.flags + self.message_snapshots = original_message.message_snapshots + self.type = getattr(original_message, "type", None) + + forwarded_msg = ForwardedMessage(message, combined_content) + await thread.send(forwarded_msg) + await self.add_reaction(message, sent_emoji) + self.dispatch("thread_reply", thread, False, message, False, False) + return + else: + message.content = "[Forwarded message with no content]" + # 2. Single-message forward (MessageType.forward) + elif getattr(message, "type", None) == getattr(discord.MessageType, "forward", None): + # Check for message.reference and its type + ref = getattr(message, "reference", None) + if ref and getattr(ref, "type", None) == getattr(discord, "MessageReferenceType", None).forward: + # Try to fetch the referenced message + ref_msg = None + try: + if ref.resolved: + ref_msg = ref.resolved + elif ref.message_id and ref.channel_id: + channel = self.get_channel(ref.channel_id) or ( + await self.fetch_channel(ref.channel_id) + ) + ref_msg = await channel.fetch_message(ref.message_id) + except Exception: + ref_msg = None + if ref_msg: + # Forward the referenced message as if it was sent + thread = await self.threads.find(recipient=message.author) + if thread is None: + delta = await self.get_thread_cooldown(message.author) + if delta: + await message.channel.send( + embed=discord.Embed( + title=self.config["cooldown_thread_title"], + description=self.config["cooldown_thread_response"].format(delta=delta), + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = await self.threads.create(message.author, message=message) + else: + if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A message was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + + # Create a forwarded message wrapper to preserve forward info + class ForwardedMessage: + def __init__(self, original_message, ref_message): + self.author = original_message.author + # Use the utility function to extract content or fallback to ref message content + extracted_content = extract_forwarded_content(original_message) + self.content = ( + extracted_content + or ref_message.content + or "[Forwarded message with no text content]" + ) + self.attachments = getattr(ref_message, "attachments", []) + self.stickers = getattr(ref_message, "stickers", []) + self.created_at = original_message.created_at + self.embeds = getattr(ref_message, "embeds", []) + self.id = original_message.id + self.type = getattr(original_message, "type", None) + self.reference = original_message.reference + + forwarded_msg = ForwardedMessage(message, ref_msg) + await thread.send(forwarded_msg) + await self.add_reaction(message, sent_emoji) + self.dispatch("thread_reply", thread, False, message, False, False) + return + else: + message.content = "[Forwarded message with no content]" + + if message.type not in [discord.MessageType.default, discord.MessageType.reply]: return thread = await self.threads.find(recipient=message.author) + if thread and thread.snoozed: + await thread.restore_from_snooze() + self.threads.cache[thread.id] = thread + # No need to re-fetch the thread - it's already restored and cached properly + + # If the previous thread was closed with delete_channel=True the channel object + # stored on the thread will now be invalid (deleted). In some rare race cases + # the thread can still be returned from the cache (or reconstructed) while the + # channel lookup returns None, causing downstream relay attempts to raise + # discord.NotFound ("Channel not found when trying to send message."). Treat + # this situation as "no active thread" so the user's new DM starts a fresh + # thread instead of silently failing. + try: + if ( + thread + and thread.channel + and isinstance(thread.channel, discord.TextChannel) + and self.get_channel(getattr(thread.channel, "id", None)) is None + ): + logger.info( + "Stale thread detected for %s (channel deleted). Purging cache entry and creating new thread.", + message.author, + ) + # Best-effort removal; ignore if already gone. + self.threads.cache.pop(thread.id, None) + thread = None + except Exception: + # If any attribute access fails, fall back to treating it as closed. + self.threads.cache.pop(getattr(thread, "id", None), None) + thread = None + if thread is None: delta = await self.get_thread_cooldown(message.author) if delta: @@ -917,18 +1144,30 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) return - if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, description=self.config["disabled_new_thread_response"], ) - embed.set_footer(text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url) - logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", + message.author, + ) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) thread = await self.threads.create(message.author, message=message) + # If thread menu is enabled, thread creation is deferred until user selects an option. + if getattr(thread, "_pending_menu", False): + return else: if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: embed = discord.Embed( @@ -938,9 +1177,12 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) embed.set_footer( text=self.config["disabled_current_thread_footer"], - icon_url=self.guild.icon_url, + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A message was blocked from %s due to disabled Modmail.", + message.author, ) - logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -950,6 +1192,49 @@ async def process_dm_modmail(self, message: discord.Message) -> None: except Exception: logger.error("Failed to send message:", exc_info=True) await self.add_reaction(message, blocked_emoji) + + try: + # Re-check channel existence + if thread and thread.channel and isinstance(thread.channel, discord.TextChannel): + if self.get_channel(thread.channel.id) is None: + logger.info( + "Relay failed due to deleted channel for %s; creating new thread.", + message.author, + ) + self.threads.cache.pop(thread.id, None) + new_thread = await self.threads.create(message.author, message=message) + if not getattr(new_thread, "_pending_menu", False) and not new_thread.cancelled: + try: + await new_thread.send(message) + except Exception: + logger.error( + "Failed to relay message after creating new thread:", + exc_info=True, + ) + else: + for user in new_thread.recipients: + if user != message.author: + try: + await new_thread.send(message, user) + except Exception: + logger.error( + "Failed to send message to additional recipient:", + exc_info=True, + ) + await self.add_reaction(message, sent_emoji) + self.dispatch( + "thread_reply", + new_thread, + False, + message, + False, + False, + ) + except Exception: + logger.warning( + "Unexpected failure in DM relay/new-thread follow-up block.", + exc_info=True, + ) else: for user in thread.recipients: # send to all other recipients @@ -963,6 +1248,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, sent_emoji) self.dispatch("thread_reply", thread, False, message, False, False) + def _get_snippet_command(self) -> commands.Command: + """Get the correct reply command based on the snippet config""" + modifiers = "f" + if self.config["plain_snippets"]: + modifiers += "p" + if self.config["anonymous_snippets"]: + modifiers += "a" + + return self.get_command(f"{modifiers}reply") + async def get_contexts(self, message, *, cls=commands.Context): """ Returns all invocation contexts from the message. @@ -973,7 +1268,7 @@ async def get_contexts(self, message, *, cls=commands.Context): ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) thread = await self.threads.find(channel=ctx.channel) - if self._skip_check(message.author.id, self.user.id): + if message.author.id == self.user.id: # type: ignore return [ctx] prefixes = await self.get_prefix() @@ -984,9 +1279,18 @@ async def get_contexts(self, message, *, cls=commands.Context): invoker = view.get_word().lower() + # Check if a snippet is being called. + # This needs to be done before checking for aliases since + # snippets can have multiple words. + try: + # Use removeprefix once PY3.9+ + snippet_text = self.snippets[message.content[len(invoked_prefix) :]] + except KeyError: + snippet_text = None + # Check if there is any aliases being called. alias = self.aliases.get(invoker) - if alias is not None: + if alias is not None and snippet_text is None: ctxs = [] aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) if not aliases: @@ -994,18 +1298,36 @@ async def get_contexts(self, message, *, cls=commands.Context): self.aliases.pop(invoker) for alias in aliases: - view = StringView(invoked_prefix + alias) + command = None + try: + snippet_text = self.snippets[alias] + except KeyError: + command_invocation_text = alias + else: + command = self._get_snippet_command() + command_invocation_text = f"{invoked_prefix}{command} {snippet_text}" + view = StringView(invoked_prefix + command_invocation_text) ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) ctx_.thread = thread discord.utils.find(view.skip_string, prefixes) ctx_.invoked_with = view.get_word().lower() - ctx_.command = self.all_commands.get(ctx_.invoked_with) + ctx_.command = command or self.all_commands.get(ctx_.invoked_with) ctxs += [ctx_] return ctxs ctx.thread = thread - ctx.invoked_with = invoker - ctx.command = self.all_commands.get(invoker) + + if snippet_text is not None: + # Process snippets + ctx.command = self._get_snippet_command() + reply_view = StringView(f"{invoked_prefix}{ctx.command} {snippet_text}") + discord.utils.find(reply_view.skip_string, prefixes) + ctx.invoked_with = reply_view.get_word().lower() + ctx.view = reply_view + else: + ctx.command = self.all_commands.get(invoker) + ctx.invoked_with = invoker + return [ctx] async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context): @@ -1025,7 +1347,12 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if trigger: invoker = re.search(trigger, message.content).group(0) else: - trigger = next(filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys())) + trigger = next( + filter( + lambda x: x.lower() in message.content.lower(), + self.auto_triggers.keys(), + ) + ) if trigger: invoker = trigger.lower() @@ -1066,7 +1393,7 @@ async def get_context(self, message, *, cls=commands.Context): view = StringView(message.content) ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - if self._skip_check(message.author.id, self.user.id): + if message.author.id == self.user.id: return ctx ctx.thread = await self.threads.find(channel=ctx.channel) @@ -1130,7 +1457,7 @@ async def on_message(self, message): color=self.main_color, ) if self.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() if not self.config["silent_alert_on_mention"]: content = self.config["mention"] @@ -1138,6 +1465,19 @@ async def on_message(self, message): content = "" await self.mention_channel.send(content=content, embed=em) + # --- MODERATOR-ONLY MESSAGE LOGGING --- + # If a moderator sends a message directly in a thread channel (not via modmail command), log it + if not message.author.bot and not isinstance(message.channel, discord.DMChannel): + thread = await self.threads.find(channel=message.channel) + if thread is not None: + ctxs = await self.get_contexts(message) + is_command = any(ctx.command for ctx in ctxs) + if not is_command: + # Only log if not a command + perms = message.channel.permissions_for(message.author) + if perms.manage_messages or perms.administrator: + await self.api.append_log(message, type_="internal") + await self.process_commands(message) async def process_commands(self, message): @@ -1145,19 +1485,7 @@ async def process_commands(self, message): return if isinstance(message.channel, discord.DMChannel): - return await self.process_dm_modmail(message) - - if message.content.startswith(self.prefix): - cmd = message.content[len(self.prefix) :].strip() - - # Process snippets - cmd = cmd.lower() - if cmd in self.snippets: - snippet = self.snippets[cmd] - if self.config["anonymous_snippets"]: - message.content = f"{self.prefix}fareply {snippet}" - else: - message.content = f"{self.prefix}freply {snippet}" + return await self._queue_dm_message(message) ctxs = await self.get_contexts(message) for ctx in ctxs: @@ -1169,11 +1497,44 @@ async def process_commands(self, message): ) checks.has_permissions(PermissionLevel.INVALID)(ctx.command) + # Check if thread is unsnoozing and queue command if so + thread = await self.threads.find(channel=ctx.channel) + if thread and thread._unsnoozing: + queued = await thread.queue_command(ctx, ctx.command) + if queued: + # Send a brief acknowledgment that command is queued + try: + await ctx.message.add_reaction("⏳") + except Exception as e: + logger.warning("Failed to add queued-reaction: %s", e) + continue + await self.invoke(ctx) continue thread = await self.threads.find(channel=ctx.channel) if thread is not None: + # If thread is snoozed (moved), auto-unsnooze when a mod sends a message directly in channel + behavior = (self.config.get("snooze_behavior") or "delete").lower() + if thread.snoozed and behavior == "move": + if not thread.snooze_data: + try: + log_entry = await self.api.logs.find_one( + {"recipient.id": str(thread.id), "snoozed": True} + ) + if log_entry: + thread.snooze_data = log_entry.get("snooze_data") + except Exception: + logger.debug( + "Failed to add queued command reaction (⏳).", + exc_info=True, + ) + try: + await thread.restore_from_snooze() + # refresh local cache + self.threads.cache[thread.id] = thread + except Exception as e: + logger.warning("Auto-unsnooze on direct message failed: %s", e) anonymous = False plain = False if self.config.get("anon_reply_without_command"): @@ -1186,9 +1547,9 @@ async def process_commands(self, message): or self.config.get("anon_reply_without_command") or self.config.get("plain_reply_without_command") ): - await thread.reply(message, anonymous=anonymous, plain=plain) - else: - await self.api.append_log(message, type_="internal") + # When replying without a command in a thread channel, use the raw content + # from the sent message as reply text while still preserving attachments. + await thread.reply(message, message.content, anonymous=anonymous, plain=plain) elif ctx.invoked_with: exc = commands.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch("command_error", ctx, exc) @@ -1206,7 +1567,13 @@ async def on_typing(self, channel, user, _): thread = await self.threads.find(recipient=user) if thread: - await thread.channel.trigger_typing() + try: + await thread.channel.typing() + except Exception: + logger.debug( + "Failed to trigger typing indicator in recipient DM.", + exc_info=True, + ) else: if not self.config.get("mod_typing"): return @@ -1216,7 +1583,14 @@ async def on_typing(self, channel, user, _): for user in thread.recipients: if await self.is_blocked(user): continue - await user.trigger_typing() + try: + await user.typing() + except Exception: + logger.debug( + "Failed to trigger typing for recipient %s.", + getattr(user, "id", "?"), + exc_info=True, + ) async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) @@ -1224,25 +1598,36 @@ async def handle_reaction_events(self, payload): return channel = self.get_channel(payload.channel_id) - if not channel: # dm channel not in internal cache - _thread = await self.threads.find(recipient=user) - if not _thread: + thread = None + # dm channel not in internal cache + if not channel: + thread = await self.threads.find(recipient=user) + if not thread: + return + channel = await thread.recipient.create_dm() + if channel.id != payload.channel_id: return - channel = await _thread.recipient.create_dm() + from_dm = isinstance(channel, discord.DMChannel) + from_txt = isinstance(channel, discord.TextChannel) + if not from_dm and not from_txt: + return + + if not thread: + params = {"recipient": user} if from_dm else {"channel": channel} + thread = await self.threads.find(**params) + if not thread: + return + + # thread must exist before doing this API call try: message = await channel.fetch_message(payload.message_id) except (discord.NotFound, discord.Forbidden): return reaction = payload.emoji - close_emoji = await self.convert_emoji(self.config["close_emoji"]) - - if isinstance(channel, discord.DMChannel): - thread = await self.threads.find(recipient=user) - if not thread: - return + if from_dm: if ( payload.event_type == "REACTION_ADD" and message.embeds @@ -1250,7 +1635,7 @@ async def handle_reaction_events(self, payload): and self.config.get("recipient_thread_close") ): ts = message.embeds[0].timestamp - if thread and ts == thread.channel.created_at: + if ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed # closing thread return await thread.close(closer=user) @@ -1270,16 +1655,15 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return else: - thread = await self.threads.find(channel=channel) - if not thread: - return try: - _, *linked_messages = await thread.find_linked_messages(message.id, either_direction=True) + _, *linked_messages = await thread.find_linked_messages( + message1=message, either_direction=True + ) except ValueError as e: logger.warning("Failed to find linked message for reactions: %s", e) return - if self.config["transfer_reactions"] and linked_messages is not [None]: + if self.config["transfer_reactions"] and linked_messages != [None]: if payload.event_type == "REACTION_ADD": for msg in linked_messages: await self.add_reaction(msg, reaction) @@ -1289,7 +1673,7 @@ async def handle_reaction_events(self, payload): for msg in linked_messages: await msg.remove_reaction(reaction, self.user) await message.remove_reaction(reaction, self.user) - except (discord.HTTPException, discord.InvalidArgument) as e: + except (discord.HTTPException, TypeError) as e: logger.warning("Failed to remove reaction: %s", e) async def handle_react_to_contact(self, payload): @@ -1312,7 +1696,10 @@ async def handle_react_to_contact(self, payload): await message.remove_reaction(payload.emoji, member) await message.add_reaction(emoji_fmt) # bot adds as well - if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, @@ -1320,7 +1707,7 @@ async def handle_react_to_contact(self, payload): ) embed.set_footer( text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, + icon_url=self.get_guild_icon(guild=channel.guild, size=128), ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", @@ -1328,6 +1715,19 @@ async def handle_react_to_contact(self, payload): ) return await member.send(embed=embed) + # Check if user has a snoozed thread + existing_thread = await self.threads.find(recipient=member) + if existing_thread and existing_thread.snoozed: + # Unsnooze the thread + await existing_thread.restore_from_snooze() + self.threads.cache[existing_thread.id] = existing_thread + # Send notification to the thread channel + if existing_thread.channel: + await existing_thread.channel.send( + f"ℹ️ {member.mention} reacted to contact and their snoozed thread has been unsnoozed." + ) + return + ctx = await self.get_context(message) await ctx.invoke(self.get_command("contact"), users=[member], manual_trigger=False) @@ -1361,10 +1761,32 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return - audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) - entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) + # Attempt to attribute channel deletion to a moderator via audit logs. + # This requires the "View Audit Log" permission; if missing, skip silently. + if not self.modmail_guild.me.guild_permissions.view_audit_log: + logger.debug( + "Skipping audit log lookup for deleted channel %d: missing view_audit_log permission.", + channel.id, + ) + return + + try: + audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) + found_entry = False + async for entry in audit_logs: + if int(entry.target.id) == channel.id: + found_entry = True + break + except discord.Forbidden: + logger.debug( + "Forbidden when fetching audit logs for deleted channel %d (missing permission).", channel.id + ) + return + except discord.HTTPException as e: + logger.debug("HTTPException when fetching audit logs for deleted channel %d: %s", channel.id, e) + return - if entry is None: + if not found_entry: logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) return @@ -1378,28 +1800,44 @@ async def on_guild_channel_delete(self, channel): await thread.close(closer=mod, silent=True, delete_channel=False) async def on_member_remove(self, member): - if member.guild != self.guild: - return thread = await self.threads.find(recipient=member) if thread: - if self.config["close_on_leave"]: + if member.guild == self.guild and self.config["close_on_leave"]: await thread.close( closer=member.guild.me, message=self.config["close_on_leave_reason"], silent=True, ) else: - embed = discord.Embed( - description=self.config["close_on_leave_reason"], color=self.error_color - ) + if len(self.guilds) > 1: + guild_left = member.guild + remaining_guilds = member.mutual_guilds + + if remaining_guilds: + remaining_guild_names = [guild.name for guild in remaining_guilds] + leave_message = ( + f"The recipient has left {guild_left}. " + f"They are still in {human_join(remaining_guild_names, final='and')}." + ) + else: + leave_message = ( + f"The recipient has left {guild_left}. We no longer share any mutual servers." + ) + else: + leave_message = "The recipient has left the server." + + embed = discord.Embed(description=leave_message, color=self.error_color) await thread.channel.send(embed=embed) async def on_member_join(self, member): - if member.guild != self.guild: - return thread = await self.threads.find(recipient=member) if thread: - embed = discord.Embed(description="The recipient has joined the server.", color=self.mod_color) + if len(self.guilds) > 1: + guild_joined = member.guild + join_message = f"The recipient has joined {guild_joined}." + else: + join_message = "The recipient has joined the server." + embed = discord.Embed(description=join_message, color=self.mod_color) await thread.channel.send(embed=embed) async def on_message_delete(self, message): @@ -1422,7 +1860,13 @@ async def on_message_delete(self, message): return message = message[0] embed = message.embeds[0] - embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) + + if embed.footer.icon: + icon_url = embed.footer.icon.url + else: + icon_url = None + + embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=icon_url) await message.edit(embed=embed) return @@ -1437,7 +1881,12 @@ async def on_message_delete(self, message): await thread.delete_message(message, note=False) embed = discord.Embed(description="Successfully deleted message.", color=self.main_color) except ValueError as e: - if str(e) not in {"DM message not found.", "Malformed thread message."}: + # Treat common non-fatal cases as benign: relay counterpart not present, note embeds, etc. + if str(e) not in { + "DM message not found.", + "Malformed thread message.", + "Thread message not found.", + }: logger.debug("Failed to find linked message to delete: %s", e) embed = discord.Embed(description="Failed to delete message.", color=self.error_color) else: @@ -1475,9 +1924,29 @@ async def on_error(self, event_method, *args, **kwargs): logger.error("Ignoring exception in %s.", event_method) logger.error("Unexpected exception:", exc_info=sys.exc_info()) - async def on_command_error(self, context, exception): + async def on_command_error( + self, + context: commands.Context, + exception: Exception, + *, + unhandled_by_cog: bool = False, + ) -> None: + if not unhandled_by_cog: + command = context.command + if command and command.has_error_handler(): + return + cog = context.cog + if cog and cog.has_error_handler(): + return + if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)): - await context.trigger_typing() + try: + await context.typing() + except Exception: + logger.debug( + "Failed to start typing context for command error feedback.", + exc_info=True, + ) await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) @@ -1508,25 +1977,30 @@ async def on_command_error(self, context, exception): ) logger.warning("CheckFailure: %s", exception) elif isinstance(exception, commands.DisabledCommand): - logger.info("DisabledCommand: %s is trying to run eval but it's disabled", context.author.name) + logger.info( + "DisabledCommand: %s is trying to run eval but it's disabled", + context.author.name, + ) else: logger.error("Unexpected exception:", exc_info=exception) + @tasks.loop(hours=1) async def post_metadata(self): info = await self.application_info() + delta = discord.utils.utcnow() - self.start_time data = { "bot_id": self.user.id, "bot_name": str(self.user), - "avatar_url": str(self.user.avatar_url), + "avatar_url": self.user.display_avatar.url, "guild_id": self.guild_id, "guild_name": self.guild.name, "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - self.start_time).total_seconds(), + "uptime": delta.total_seconds(), "latency": f"{self.ws.latency * 1000:.4f}", "version": str(self.version), "selfhosted": True, - "last_updated": str(datetime.utcnow()), + "last_updated": str(discord.utils.utcnow()), } if info.team is not None: @@ -1538,61 +2012,82 @@ async def post_metadata(self): } ) else: - data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) + data.update( + { + "owner_name": info.owner.name, + "owner_id": info.owner.id, + "team": False, + } + ) async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") + @post_metadata.before_loop async def before_post_metadata(self): await self.wait_for_connected() + if not self.config.get("data_collection") or not self.guild: + self.post_metadata.cancel() + return + logger.debug("Starting metadata loop.") logger.line("debug") - if not self.guild: - self.metadata_loop.cancel() + @tasks.loop(hours=1) async def autoupdate(self): changelog = await Changelog.from_url(self) latest = changelog.latest_version - if self.version < parse_version(latest.version): - if self.hosting_method == HostingMethod.HEROKU: + if self.version < Version(latest.version): + error = None + data = {} + try: + # update fork if gh_token exists data = await self.api.update_repository() + except InvalidConfigError: + pass + except ClientResponseError as exc: + error = exc + if self.hosting_method == HostingMethod.HEROKU: + if error is not None: + logger.error(f"Autoupdate failed! Status: {error.status}.") + logger.error(f"Error message: {error.message}") + self.autoupdate.cancel() + return - embed = discord.Embed(color=self.main_color) + commit_data = data.get("data") + if not commit_data: + return + + logger.info("Bot has been updated.") - commit_data = data["data"] + if not self.config["update_notifications"]: + return + + embed = discord.Embed(color=self.main_color) + message = commit_data["commit"]["message"] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] user = data["user"] + embed.add_field( + name="Merge Commit", + value=f"[`{short_sha}`]({html_url}) {message} - {user['username']}", + ) embed.set_author( name=user["username"] + " - Updating Bot", icon_url=user["avatar_url"], url=user["url"], ) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.version} -> v{latest.version}") embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) - if commit_data: - message = commit_data["commit"]["message"] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", - value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", - ) - logger.info("Bot has been updated.") - channel = self.log_channel - if self.config["update_notifications"]: - await channel.send(embed=embed) + channel = self.update_channel + await channel.send(embed=embed) else: - try: - # update fork if gh_token exists - await self.api.update_repository() - except InvalidConfigError: - pass - command = "git pull" proc = await asyncio.create_subprocess_shell( command, @@ -1606,44 +2101,78 @@ async def autoupdate(self): if err and not res: logger.warning(f"Autoupdate failed: {err}") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() return elif res != "Already up to date.": + if os.getenv("PIPENV_ACTIVE"): + # Update pipenv if possible + await asyncio.create_subprocess_shell( + "pipenv sync", + stderr=PIPE, + stdout=PIPE, + ) + message = "" + else: + message = "\n\nDo manually update dependencies if your bot has crashed." + logger.info("Bot has been updated.") channel = self.update_channel - if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): + if self.hosting_method in ( + HostingMethod.PM2, + HostingMethod.SYSTEMD, + ): embed = discord.Embed(title="Bot has been updated", color=self.main_color) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version} {message}" + ) if self.config["update_notifications"]: await channel.send(embed=embed) else: embed = discord.Embed( title="Bot has been updated and is logging out.", - description="If you do not have an auto-restart setup, please manually start the bot.", + description=f"If you do not have an auto-restart setup, please manually start the bot. {message}", color=self.main_color, ) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.version} -> v{latest.version}") if self.config["update_notifications"]: await channel.send(embed=embed) return await self.close() + @autoupdate.before_loop async def before_autoupdate(self): await self.wait_for_connected() logger.debug("Starting autoupdate loop") if self.config.get("disable_autoupdates"): logger.warning("Autoupdates disabled.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() + return if self.hosting_method == HostingMethod.DOCKER: logger.warning("Autoupdates disabled as using Docker.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() + return if not self.config.get("github_token") and self.hosting_method == HostingMethod.HEROKU: logger.warning("GitHub access token not found.") logger.warning("Autoupdates disabled.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() + return + + @tasks.loop(hours=1, reconnect=False) + async def log_expiry(self): + log_expire_after = self.config.get("log_expiration") + if log_expire_after == isodate.Duration(): + return self.log_expiry.stop() + + now = discord.utils.utcnow() + expiration_datetime = now - log_expire_after + # WARNING: comparison is done lexicographically, not by date. + # This is fine as long as the date is in zero-padded ISO format, which it should be. + expired_logs = await self.db.logs.delete_many({"closed_at": {"$lte": str(expiration_datetime)}}) + + logger.info(f"Deleted {expired_logs.deleted_count} expired logs.") def format_channel_name(self, author, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names @@ -1654,18 +2183,28 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): if force_null: name = new_name = "null" else: - if self.config["use_user_id_channel_name"]: + if self.config["use_random_channel_name"]: + to_hash = self.token.split(".")[-1] + str(author.id) + digest = hashlib.md5(to_hash.encode("utf8"), usedforsecurity=False) + name = new_name = digest.hexdigest()[-8:] + elif self.config["use_user_id_channel_name"]: name = new_name = str(author.id) elif self.config["use_timestamp_channel_name"]: name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") else: - name = author.name.lower() + if self.config["use_nickname_channel_name"]: + author_member = self.guild.get_member(author.id) + name = author_member.display_name.lower() + else: + name = author.name.lower() + if force_null: name = "null" - name = new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" - ) + f"-{author.discriminator}" + name = "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" + if author.discriminator != "0": + name += f"-{author.discriminator}" + new_name = name counter = 1 existed = set(c.name for c in guild.text_channels if c != exclude_channel) @@ -1679,17 +2218,42 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): def main(): try: # noinspection PyUnresolvedReferences - import uvloop + import uvloop # type: ignore logger.debug("Setting up with uvloop.") uvloop.install() except ImportError: pass + try: + import cairosvg # noqa: F401 + except OSError: + if os.name == "nt": + if struct.calcsize("P") * 8 != 64: + logger.error( + "Unable to import cairosvg, ensure your Python is a 64-bit version: https://www.python.org/downloads/" + ) + else: + logger.error( + "Unable to import cairosvg, install GTK Installer for Windows and restart your system (https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest)" + ) + else: + if "ubuntu" in platform.version().lower() or "debian" in platform.version().lower(): + logger.error( + "Unable to import cairosvg, try running `sudo apt-get install libpangocairo-1.0-0` or report on our support server with your OS details: https://discord.gg/etJNHCQ" + ) + else: + logger.error( + "Unable to import cairosvg, report on our support server with your OS details: https://discord.gg/etJNHCQ" + ) + sys.exit(0) + # check discord version - if discord.__version__ != "1.7.3": + discord_version = "2.6.3" + if discord.__version__ != discord_version: logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 1.7.3, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected %s, received %s", + discord_version, discord.__version__, ) sys.exit(0) diff --git a/cogs/modmail.py b/cogs/modmail.py index bb3860bfde..b0e38ed9e0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,18 +1,19 @@ import asyncio import re -from datetime import datetime +from datetime import datetime, timezone, timedelta from itertools import zip_longest -from typing import Optional, Union -from types import SimpleNamespace +from typing import Optional, Union, List, Tuple, Literal +import logging import discord from discord.ext import commands +from discord.ext import tasks +from discord.ext.commands.view import StringView from discord.ext.commands.cooldowns import BucketType from discord.role import Role from discord.utils import escape_markdown from dateutil import parser -from natural.date import duration from core import checks from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, getLogger @@ -29,6 +30,87 @@ class Modmail(commands.Cog): def __init__(self, bot): self.bot = bot + self._snoozed_cache = [] + self._auto_unsnooze_task = self.bot.loop.create_task(self.auto_unsnooze_task()) + + async def auto_unsnooze_task(self): + await self.bot.wait_until_ready() + last_db_query = 0 + while not self.bot.is_closed(): + now = datetime.now(timezone.utc) + try: + # Query DB every 2 minutes + if (now.timestamp() - last_db_query) > 120: + snoozed_threads = await self.bot.api.logs.find( + {"snooze_until": {"$gte": now.isoformat()}} + ).to_list(None) + self._snoozed_cache = snoozed_threads or [] + last_db_query = now.timestamp() + # Check cache every 10 seconds + to_unsnooze = [] + for thread_data in list(self._snoozed_cache): + snooze_until = thread_data.get("snooze_until") + recipient = thread_data.get("recipient") + if not recipient or not recipient.get("id"): + continue + thread_id = int(recipient.get("id")) + if snooze_until: + try: + dt = parser.isoparse(snooze_until) + except Exception: + continue + if now >= dt: + to_unsnooze.append(thread_data) + for thread_data in to_unsnooze: + recipient = thread_data.get("recipient") + if not recipient or not recipient.get("id"): + continue + thread_id = int(recipient.get("id")) + thread = self.bot.threads.cache.get(thread_id) or await self.bot.threads.find( + id=thread_id + ) + if thread and thread.snoozed: + await thread.restore_from_snooze() + logging.info(f"[AUTO-UNSNOOZE] Thread {thread_id} auto-unsnoozed.") + try: + channel = thread.channel + if channel: + await channel.send("⏰ This thread has been automatically unsnoozed.") + except Exception as e: + logger.info( + "Failed to notify channel after auto-unsnooze: %s", + e, + ) + self._snoozed_cache.remove(thread_data) + except Exception as e: + logging.error(f"Error in auto_unsnooze_task: {e}") + await asyncio.sleep(10) + + def _resolve_user(self, user_str): + """Helper to resolve a user from mention, ID, or username.""" + import re + + if not user_str: + return None + if user_str.isdigit(): + return int(user_str) + match = re.match(r"<@!?(\d+)>", user_str) + if match: + return int(match.group(1)) + return None + + def _resolve_user(self, user_str): + """Helper to resolve a user from mention, ID, or username.""" + import re + + if not user_str: + return None + if user_str.isdigit(): + return int(user_str) + match = re.match(r"<@!?(\d+)>", user_str) + if match: + return int(match.group(1)) + return None @commands.command() @trigger_typing @@ -94,8 +176,8 @@ async def setup(self, ctx): embed.add_field( name="Thanks for using our bot!", value="If you like what you see, consider giving the " - "[repo a star](https://github.com/kyb3r/modmail) :star: and if you are " - "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", + "[repo a star](https://github.com/modmail-dev/modmail) :star: and if you are " + "feeling extra generous, buy us coffee on [Buy Me A Coffee](https://buymeacoffee.com/modmaildev) :heart:!", ) embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') @@ -144,30 +226,54 @@ async def snippet(self, ctx, *, name: str.lower = None): """ if name is not None: - val = self.bot.snippets.get(name) - if val is None: + if name == "compact": + embeds = [] + + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): + description = format_description(i, names) + embed = discord.Embed(color=self.bot.main_color, description=description) + embed.set_author( + name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128) + ) + embeds.append(embed) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + return + + snippet_name = self.bot._resolve_snippet(name) + + if snippet_name is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: + val = self.bot.snippets[snippet_name] embed = discord.Embed( - title=f'Snippet - "{name}":', description=val, color=self.bot.main_color + title=f'Snippet - "{snippet_name}":', + description=val, + color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.bot.snippets: embed = discord.Embed( - color=self.bot.error_color, description="You dont have any snippets at the moment." + color=self.bot.error_color, + description="You dont have any snippets at the moment.", ) embed.set_footer(text=f'Check "{self.bot.prefix}help snippet add" to add a snippet.') - embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) + embed.set_author( + name="Snippets", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), + ) return await ctx.send(embed=embed) - embeds = [] + embeds = [discord.Embed(color=self.bot.main_color) for _ in range((len(self.bot.snippets) // 10) + 1)] + for embed in embeds: + embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) - for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): - description = format_description(i, names) - embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) - embeds.append(embed) + for i, snippet in enumerate(sorted(self.bot.snippets.items())): + embeds[i // 10].add_field( + name=snippet[0], value=return_or_truncate(snippet[1], 350), inline=False + ) session = EmbedPaginatorSession(ctx, *embeds) await session.run() @@ -178,20 +284,20 @@ async def snippet_raw(self, ctx, *, name: str.lower): """ View the raw content of a snippet. """ - val = self.bot.snippets.get(name) - if val is None: + snippet_name = self.bot._resolve_snippet(name) + if snippet_name is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: - val = truncate(escape_code_block(val), 2048 - 7) + val = truncate(escape_code_block(self.bot.snippets[snippet_name]), 2048 - 7) embed = discord.Embed( - title=f'Raw snippet - "{name}":', + title=f'Raw snippet - "{snippet_name}":', description=f"```\n{val}```", color=self.bot.main_color, ) return await ctx.send(embed=embed) - @snippet.command(name="add") + @snippet.command(name="add", aliases=["create", "make"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_content): """ @@ -212,6 +318,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte color=self.bot.error_color, description=f"A command with the same name already exists: `{name}`.", ) + return await ctx.send(embed=embed) elif name in self.bot.snippets: embed = discord.Embed( title="Error", @@ -246,16 +353,103 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte ) return await ctx.send(embed=embed) + def _fix_aliases(self, snippet_being_deleted: str) -> Tuple[List[str]]: + """ + Remove references to the snippet being deleted from aliases. + + Direct aliases to snippets are deleted, and aliases having + other steps are edited. + + A tuple of dictionaries are returned. The first dictionary + contains a mapping of alias names which were deleted to their + original value, and the second dictionary contains a mapping + of alias names which were edited to their original value. + """ + deleted = {} + edited = {} + + # Using a copy since we might need to delete aliases + for alias, val in self.bot.aliases.copy().items(): + values = parse_alias(val) + + save_aliases = [] + + for val in values: + view = StringView(val) + linked_command = view.get_word().lower() + message = view.read_rest() + + if linked_command == snippet_being_deleted: + continue + + is_valid_snippet = snippet_being_deleted in self.bot.snippets + + if not self.bot.get_command(linked_command) and not is_valid_snippet: + alias_command = self.bot.aliases[linked_command] + save_aliases.extend(normalize_alias(alias_command, message)) + else: + save_aliases.append(val) + + if not save_aliases: + original_value = self.bot.aliases.pop(alias) + deleted[alias] = original_value + else: + original_alias = self.bot.aliases[alias] + new_alias = " && ".join(f'"{a}"' for a in save_aliases) + + if original_alias != new_alias: + self.bot.aliases[alias] = new_alias + edited[alias] = original_alias + + return deleted, edited + @snippet.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" - if name in self.bot.snippets: + deleted_aliases, edited_aliases = self._fix_aliases(name) + + deleted_aliases_string = ",".join(f"`{alias}`" for alias in deleted_aliases) + if len(deleted_aliases) == 1: + deleted_aliases_output = f"The `{deleted_aliases_string}` direct alias has been removed." + elif deleted_aliases: + deleted_aliases_output = ( + f"The following direct aliases have been removed: {deleted_aliases_string}." + ) + else: + deleted_aliases_output = None + + if len(edited_aliases) == 1: + alias, val = edited_aliases.popitem() + edited_aliases_output = ( + f"Steps pointing to this snippet have been removed from the `{alias}` alias" + f" (previous value: `{val}`).`" + ) + elif edited_aliases: + alias_list = "\n".join( + [ + f"- `{alias_name}` (previous value: `{val}`)" + for alias_name, val in edited_aliases.items() + ] + ) + edited_aliases_output = ( + f"Steps pointing to this snippet have been removed from the following aliases:" + f"\n\n{alias_list}" + ) + else: + edited_aliases_output = None + + description = f"Snippet `{name}` is now deleted." + if deleted_aliases_output: + description += f"\n\n{deleted_aliases_output}" + if edited_aliases_output: + description += f"\n\n{edited_aliases_output}" + embed = discord.Embed( title="Removed snippet", color=self.bot.main_color, - description=f"Snippet `{name}` is now deleted.", + description=description, ) self.bot.snippets.pop(name) await self.bot.config.update() @@ -329,7 +523,10 @@ async def move(self, ctx, *, arguments): silent = any(word in silent_words for word in options.split()) await thread.channel.move( - category=category, end=True, sync_permissions=True, reason=f"{ctx.author} moved this thread." + category=category, + end=True, + sync_permissions=True, + reason=f"{ctx.author} moved this thread.", ) if self.bot.config["thread_move_notify"] and not silent: @@ -352,28 +549,35 @@ async def move(self, ctx, *, arguments): await self.bot.add_reaction(ctx.message, sent_emoji) async def send_scheduled_close_message(self, ctx, after, silent=False): - human_delta = human_timedelta(after.dt) - - silent = "*silently* " if silent else "" + """Send a scheduled close notice only to the staff thread channel. + Uses Discord relative timestamp formatting for better UX. + """ + ts = int((after.dt if after.dt.tzinfo else after.dt.replace(tzinfo=timezone.utc)).timestamp()) embed = discord.Embed( title="Scheduled close", - description=f"This thread will close {silent}in {human_delta}.", + description=f"This thread will{' silently' if silent else ''} close .", color=self.bot.error_color, ) - if after.arg and not silent: embed.add_field(name="Message", value=after.arg) - embed.set_footer(text="Closing will be cancelled if a thread message is sent.") embed.timestamp = after.dt - await ctx.send(embed=embed) + thread = getattr(ctx, "thread", None) + if thread and ctx.channel == thread.channel: + await thread.channel.send(embed=embed) @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def close(self, ctx, *, after: UserFriendlyTime = None): + async def close( + self, + ctx, + option: Optional[Literal["silent", "silently", "cancel"]] = "", + *, + after: UserFriendlyTime = None, + ): """ Close the current thread. @@ -387,7 +591,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): Silently close a thread (no message) - `{prefix}close silently` - - `{prefix}close in 10m silently` + - `{prefix}close silently in 10m` Stop a thread from closing: - `{prefix}close cancel` @@ -395,19 +599,16 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): thread = ctx.thread - now = datetime.utcnow() - - close_after = (after.dt - now).total_seconds() if after else 0 - message = after.arg if after else None - silent = str(message).lower() in {"silent", "silently"} - cancel = str(message).lower() == "cancel" + close_after = (after.dt - after.now).total_seconds() if after else 0 + silent = any(x == option for x in {"silent", "silently"}) + cancel = option == "cancel" if cancel: - if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( - color=self.bot.error_color, description="Scheduled close has been cancelled." + color=self.bot.error_color, + description="Scheduled close has been cancelled.", ) else: embed = discord.Embed( @@ -417,7 +618,11 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): return await ctx.send(embed=embed) - if after and after.dt > now: + message = after.arg if after else None + if self.bot.config["require_close_reason"] and message is None: + raise commands.BadArgument("Provide a reason for closing the thread.") + + if after and after.dt > after.now: await self.send_scheduled_close_message(ctx, after, silent) await thread.close(closer=ctx.author, after=close_after, message=message, silent=silent) @@ -502,7 +707,8 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( - color=self.bot.main_color, description=f"{mention} will no longer be notified." + color=self.bot.main_color, + description=f"{mention} will no longer be notified.", ) return await ctx.send(embed=embed) @@ -603,11 +809,18 @@ async def sfw(self, ctx): @checks.thread_only() async def msglink(self, ctx, message_id: int): """Retrieves the link to a message in the current thread.""" - try: - message = await ctx.thread.recipient.fetch_message(message_id) - except discord.NotFound: + found = False + for recipient in ctx.thread.recipients: + try: + message = await recipient.fetch_message(message_id) + found = True + break + except discord.NotFound: + continue + if not found: embed = discord.Embed( - color=self.bot.error_color, description="Message not found or no longer exists." + color=self.bot.error_color, + description="Message not found or no longer exists.", ) else: embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) @@ -627,7 +840,7 @@ def format_log_embeds(self, logs, avatar_url): title = f"Total Results Found ({len(logs)})" for entry in logs: - created_at = parser.parse(entry["created_at"]) + created_at = parser.parse(entry["created_at"]).astimezone(timezone.utc) prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": @@ -636,13 +849,14 @@ def format_log_embeds(self, logs, avatar_url): f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" ) - username = entry["recipient"]["name"] + "#" - username += entry["recipient"]["discriminator"] + username = entry["recipient"]["name"] + if entry["recipient"]["discriminator"] != "0": + username += "#" + entry["recipient"]["discriminator"] embed = discord.Embed(color=self.bot.main_color, timestamp=created_at) embed.set_author(name=f"{title} - {username}", icon_url=avatar_url, url=log_url) embed.url = log_url - embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) + embed.add_field(name="Created", value=human_timedelta(created_at)) closer = entry.get("closer") if closer is None: closer_msg = "Unknown" @@ -653,6 +867,9 @@ def format_log_embeds(self, logs, avatar_url): if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") + if entry.get("title"): + embed.add_field(name="Title", value=entry["title"], inline=False) + embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) if closer is not None: @@ -734,6 +951,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ctx.command.reset_cooldown(ctx) return + to_exec = [] if not silent: description = self.bot.formatter.format( self.bot.config["private_added_to_group_response"], moderator=ctx.author @@ -744,10 +962,13 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=str(ctx.author), + icon_url=ctx.author.display_avatar.url if ctx.author.display_avatar else None, + ) for u in users: - await u.send(embed=em) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_response"], @@ -760,14 +981,19 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + to_exec.append(i.send(embed=em)) await ctx.thread.add_users(users) + if to_exec: + await asyncio.gather(*to_exec) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -813,9 +1039,21 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return + if not users: + em = discord.Embed( + title="Error", + description="No valid users to remove.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + + to_exec = [] if not silent: description = self.bot.formatter.format( - self.bot.config["private_removed_from_group_response"], moderator=ctx.author + self.bot.config["private_removed_from_group_response"], + moderator=ctx.author, ) em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -823,10 +1061,13 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=str(ctx.author), + icon_url=ctx.author.display_avatar.url if ctx.author.display_avatar else None, + ) for u in users: - await u.send(embed=em) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_response"], @@ -839,14 +1080,19 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + to_exec.append(i.send(embed=em)) await ctx.thread.remove_users(users) + if to_exec: + await asyncio.gather(*to_exec) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -896,6 +1142,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return + to_exec = [] if not silent: em = discord.Embed( title=self.bot.config["private_added_to_group_title"], @@ -903,21 +1150,21 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() tag = self.bot.config["mod_tag"] if tag is None: - tag = str(get_top_hoisted_role(ctx.author)) + tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: - name = tag + name = "Anonymous" avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon_url - em.set_footer(text=name, icon_url=avatar_url) + avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) + em.set_footer(text=name, icon_url=avatar_url if avatar_url else None) for u in users: - await u.send(embed=em) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_description_anon"], @@ -929,14 +1176,19 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + to_exec.append(i.send(embed=em)) await ctx.thread.add_users(users) + if to_exec: + await asyncio.gather(*to_exec) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -981,6 +1233,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro ctx.command.reset_cooldown(ctx) return + to_exec = [] if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -988,21 +1241,21 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() tag = self.bot.config["mod_tag"] if tag is None: - tag = str(get_top_hoisted_role(ctx.author)) + tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: - name = tag + name = "Anonymous" avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon_url - em.set_footer(text=name, icon_url=avatar_url) + avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) + em.set_footer(text=name, icon_url=avatar_url if avatar_url else None) for u in users: - await u.send(embed=em) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_description_anon"], @@ -1014,14 +1267,19 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + to_exec.append(i.send(embed=em)) await ctx.thread.remove_users(users) + if to_exec: + await asyncio.gather(*to_exec) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -1036,13 +1294,14 @@ async def logs(self, ctx, *, user: User = None): `user` may be a user ID, mention, or name. """ - await ctx.trigger_typing() + async with safe_typing(ctx): + pass if not user: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(SimpleNamespace(name="member")) - user = thread.recipient or await self.bot.fetch_user(thread.id) + raise commands.MissingRequiredArgument(DummyParam("user")) + user = thread.recipient or await self.bot.get_or_fetch_user(thread.id) default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" icon_url = getattr(user, "avatar_url", default_avatar) @@ -1075,7 +1334,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): user = user if user is not None else ctx.author entries = await self.bot.api.search_closed_by(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( @@ -1087,6 +1346,28 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="key", aliases=["id"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def logs_key(self, ctx, key: str): + """ + Get the log link for the specified log key. + """ + icon_url = ctx.author.avatar.url + + logs = await self.bot.api.find_log_entry(key) + + if not logs: + embed = discord.Embed( + color=self.bot.error_color, + description=f"Log entry `{key}` not found.", + ) + return await ctx.send(embed=embed) + + embeds = self.format_log_embeds(logs, avatar_url=icon_url) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + @logs.command(name="delete", aliases=["wipe"]) @checks.has_permissions(PermissionLevel.OWNER) async def logs_delete(self, ctx, key_or_link: str): @@ -1125,7 +1406,7 @@ async def logs_responded(self, ctx, *, user: User = None): entries = await self.bot.api.get_responded_logs(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( @@ -1146,11 +1427,12 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): Provide a `limit` to specify the maximum number of logs the bot should find. """ - await ctx.trigger_typing() + async with safe_typing(ctx): + pass entries = await self.bot.api.search_by_text(query, limit) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( @@ -1173,10 +1455,10 @@ async def reply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - - async with ctx.typing(): - await ctx.thread.reply(ctx.message) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg) @commands.command(aliases=["formatreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1194,11 +1476,15 @@ async def freply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with ctx.typing(): - await ctx.thread.reply(ctx.message) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg) @commands.command(aliases=["formatanonreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1216,11 +1502,67 @@ async def fareply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, + ) + # Ensure logs record only the reply text, not the command. + ctx.message.content = msg + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, anonymous=True) + + @commands.command(aliases=["formatplainreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fpreply(self, ctx, *, msg: str = ""): + """ + Reply to a Modmail thread with variables and a plain message. + + Works just like `{prefix}areply`, however with the addition of three variables: + - `{{channel}}` - the `discord.TextChannel` object + - `{{recipient}}` - the `discord.User` object of the recipient + - `{{author}}` - the `discord.User` object of the author + + Supports attachments and images as well as + automatically embedding image URLs. + """ + msg = self.bot.formatter.format( + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, + ) + # Ensure logs record only the reply text, not the command. + ctx.message.content = msg + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, plain=True) + + @commands.command(aliases=["formatplainanonreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fpareply(self, ctx, *, msg: str = ""): + """ + Anonymously reply to a Modmail thread with variables and a plain message. + + Works just like `{prefix}areply`, however with the addition of three variables: + - `{{channel}}` - the `discord.TextChannel` object + - `{{recipient}}` - the `discord.User` object of the recipient + - `{{author}}` - the `discord.User` object of the author + + Supports attachments and images as well as + automatically embedding image URLs. + """ + msg = self.bot.formatter.format( + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with ctx.typing(): - await ctx.thread.reply(ctx.message, anonymous=True) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, anonymous=True, plain=True) @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1235,9 +1577,10 @@ async def areply(self, ctx, *, msg: str = ""): Edit the `anon_username`, `anon_avatar_url` and `anon_tag` config variables to do so. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with ctx.typing(): - await ctx.thread.reply(ctx.message, anonymous=True) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, anonymous=True) @commands.command(aliases=["plainreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1249,9 +1592,10 @@ async def preply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with ctx.typing(): - await ctx.thread.reply(ctx.message, plain=True) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, plain=True) @commands.command(aliases=["plainanonreply", "plainanonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1263,9 +1607,10 @@ async def pareply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with ctx.typing(): - await ctx.thread.reply(ctx.message, anonymous=True, plain=True) + async with safe_typing(ctx): + await ctx.thread.reply(ctx.message, msg, anonymous=True, plain=True) @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1277,9 +1622,16 @@ async def note(self, ctx, *, msg: str = ""): Useful for noting context. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): msg = await ctx.thread.note(ctx.message) await msg.pin() + # Acknowledge and clean up the invoking command message + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + try: + await ctx.message.delete(delay=3) + except (discord.Forbidden, discord.NotFound): + pass @note.command(name="persistent", aliases=["persist"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1289,10 +1641,17 @@ async def note_persistent(self, ctx, *, msg: str = ""): Take a persistent note about the current user. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) + # Acknowledge and clean up the invoking command message + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + try: + await ctx.message.delete(delay=3) + except (discord.Forbidden, discord.NotFound) as e: + logger.debug(f"Failed to delete note command message: {e}") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1326,6 +1685,29 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @checks.has_permissions(PermissionLevel.REGULAR) async def selfcontact(self, ctx): """Creates a thread with yourself""" + # Check if user already has a thread + existing_thread = await self.bot.threads.find(recipient=ctx.author) + if existing_thread: + if existing_thread.snoozed: + # Unsnooze the thread + msg = await ctx.send("ℹ️ You had a snoozed thread. Unsnoozing now...") + await existing_thread.restore_from_snooze() + self.bot.threads.cache[existing_thread.id] = existing_thread + try: + await msg.delete(delay=10) + except (discord.Forbidden, discord.NotFound): + pass + return + else: + # Thread already exists and is active + embed = discord.Embed( + title="Thread not created", + description=f"A thread for you already exists in {existing_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed, delete_after=10) + return + await ctx.invoke(self.contact, users=[ctx.author]) @commands.command(usage=" [category] [options]") @@ -1333,9 +1715,16 @@ async def selfcontact(self, ctx): async def contact( self, ctx, - users: commands.Greedy[Union[discord.Member, discord.User, discord.Role]], + users: commands.Greedy[ + Union[ + Literal["silent", "silently"], + discord.Member, + discord.User, + discord.Role, + ] + ], *, - category: Union[SimilarCategoryConverter, str] = None, + category: SimilarCategoryConverter = None, manual_trigger=True, ): """ @@ -1349,11 +1738,23 @@ async def contact( A maximum of 5 users are allowed. `options` can be `silent` or `silently`. """ - silent = False + silent = any(x in users for x in ("silent", "silently")) + if silent: + try: + users.remove("silent") + except ValueError: + pass + + try: + users.remove("silently") + except ValueError: + pass + if isinstance(category, str): - if "silent" in category or "silently" in category: - silent = True - category = category.strip("silently").strip("silent").strip() + category = category.split() + + category = " ".join(category) + if category: try: category = await SimilarCategoryConverter().convert( ctx, category @@ -1370,9 +1771,14 @@ async def contact( users += u.members users.remove(u) + snoozed_users = [] for u in list(users): exists = await self.bot.threads.find(recipient=u) if exists: + # Check if thread is snoozed + if exists.snoozed: + snoozed_users.append(u) + continue errors.append(f"A thread for {u} already exists.") if exists.channel: errors[-1] += f" in {exists.channel.mention}" @@ -1386,6 +1792,23 @@ async def contact( errors.append(f"{ref} currently blocked from contacting {self.bot.user.name}.") users.remove(u) + # Handle snoozed users - unsnooze them and return early + if snoozed_users: + for u in snoozed_users: + thread = await self.bot.threads.find(recipient=u) + if thread and thread.snoozed: + msg = await ctx.send(f"ℹ️ {u.mention} had a snoozed thread. Unsnoozing now...") + await thread.restore_from_snooze() + self.bot.threads.cache[thread.id] = thread + try: + await msg.delete(delay=10) + except (discord.Forbidden, discord.NotFound) as e: + logger.debug( + f"Failed to delete message (likely already deleted or lacking permissions): {e}" + ) + # Don't try to create a new thread - we just unsnoozed existing ones + return + if len(users) > 5: errors.append("Group conversations only support 5 users.") users = [] @@ -1398,11 +1821,14 @@ async def contact( title = None if manual_trigger: # not react to contact - embed = discord.Embed(title=title, color=self.bot.error_color, description="\n".join(errors)) + embed = discord.Embed( + title=title, + color=self.bot.error_color, + description="\n".join(errors), + ) await ctx.send(embed=embed, delete_after=10) if not users: - # end return creator = ctx.author if manual_trigger else users[0] @@ -1412,12 +1838,16 @@ async def contact( creator=creator, category=category, manual_trigger=manual_trigger, + # The minimum character check is enforced in ThreadManager.create ) if thread.cancelled: return - if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.bot.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): logger.info("Contacting user %s when Modmail DM is disabled.", users[0]) if not silent and not self.bot.config.get("thread_contact_silently"): @@ -1434,8 +1864,10 @@ async def contact( color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{creator}", icon_url=creator.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer( + text=f"{creator}", icon_url=creator.display_avatar.url if creator.display_avatar else None + ) for u in users: await u.send(embed=em) @@ -1455,8 +1887,10 @@ async def contact( if manual_trigger: sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - await asyncio.sleep(5) - await ctx.message.delete() + try: + await ctx.message.delete(delay=5) + except (discord.Forbidden, discord.NotFound): + pass @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @@ -1464,8 +1898,6 @@ async def contact( async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] - roles = [] users = [] now = ctx.message.created_at @@ -1473,51 +1905,29 @@ async def blocked(self, ctx): blocked_users = list(self.bot.blocked_users.items()) for id_, reason in blocked_users: # parse "reason" and check if block is expired - # etc "blah blah blah... until 2019-10-14T21:12:45.559948." - end_time = re.search(r"until ([^`]+?)\.$", reason) - if end_time is None: - # backwards compat - end_time = re.search(r"%([^%]+?)%", reason) - if end_time is not None: - logger.warning( - r"Deprecated time message for user %s, block and unblock again to update.", - id_, - ) + try: + end_time, after = extract_block_timestamp(reason, id_) + except ValueError: + continue if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.bot.blocked_users.pop(str(id_)) logger.debug("No longer blocked, user %s.", id_) continue - - user = self.bot.get_user(int(id_)) - if user: - users.append((user.mention, reason)) - else: - try: - user = await self.bot.fetch_user(id_) - users.append((user.mention, reason)) - except discord.NotFound: - users.append((id_, reason)) + users.append((f"<@{id_}>", reason)) blocked_roles = list(self.bot.blocked_roles.items()) for id_, reason in blocked_roles: # parse "reason" and check if block is expired # etc "blah blah blah... until 2019-10-14T21:12:45.559948." - end_time = re.search(r"until ([^`]+?)\.$", reason) - if end_time is None: - # backwards compat - end_time = re.search(r"%([^%]+?)%", reason) - if end_time is not None: - logger.warning( - r"Deprecated time message for role %s, block and unblock again to update.", - id_, - ) + try: + end_time, after = extract_block_timestamp(reason, id_) + except ValueError: + continue if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.bot.blocked_roles.pop(str(id_)) @@ -1528,43 +1938,54 @@ async def blocked(self, ctx): if role: roles.append((role.mention, reason)) + user_embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] + if users: - embed = embeds[0] + embed = user_embeds[0] for mention, reason in users: line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", + title="Blocked Users", color=self.bot.main_color, description=line, ) - embeds.append(embed) + user_embeds.append(embed) else: embed.description += line else: - embeds[0].description = "Currently there are no blocked users." + user_embeds[0].description = "Currently there are no blocked users." + + if len(user_embeds) > 1: + for n, em in enumerate(user_embeds): + em.title = f"{em.title} [{n + 1}]" - embeds.append(discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")) + role_embeds = [discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")] if roles: - embed = embeds[-1] + embed = role_embeds[-1] for mention, reason in roles: line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: + role_embeds[-1].set_author() embed = discord.Embed( - title="Blocked Roles (Continued)", + title="Blocked Roles", color=self.bot.main_color, description=line, ) - embeds.append(embed) + role_embeds.append(embed) else: embed.description += line else: - embeds[-1].description = "Currently there are no blocked roles." + role_embeds[-1].description = "Currently there are no blocked roles." - session = EmbedPaginatorSession(ctx, *embeds) + if len(role_embeds) > 1: + for n, em in enumerate(role_embeds): + em.title = f"{em.title} [{n + 1}]" + + session = EmbedPaginatorSession(ctx, *user_embeds, *role_embeds) await session.run() @@ -1649,7 +2070,7 @@ async def block( if thread: user_or_role = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(SimpleNamespace(name="user or role")) + raise commands.MissingRequiredArgument(DummyParam("user or role")) else: raise commands.BadArgument(f'User or role "{after.arg}" not found.') @@ -1666,15 +2087,18 @@ async def block( ) return await ctx.send(embed=embed) - reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" + reason = f"by {escape_markdown(str(ctx.author))}" if after is not None: if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') + if after.arg: - reason += f" for `{after.arg}`" + fmt_dt = discord.utils.format_dt(after.dt, "R") if after.dt > after.now: - reason += f" until {after.dt.isoformat()}" + fmt_dt = discord.utils.format_dt(after.dt, "f") + + reason += f" until {fmt_dt}" reason += "." @@ -1726,7 +2150,7 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): if thread: user_or_role = thread.recipient else: - raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) + raise commands.MissingRequiredArgument(DummyParam("user or role")) mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") name = getattr(user_or_role, "name", f"`{user_or_role.id}`") @@ -1767,7 +2191,9 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): ) else: embed = discord.Embed( - title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color + title="Error", + description=f"{mention} is not blocked.", + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1837,10 +2263,10 @@ async def repair(self, ctx): and message.embeds[0].color.value == self.bot.main_color and message.embeds[0].footer.text ): - user_id = match_user_id(message.embeds[0].footer.text) + user_id = match_user_id(message.embeds[0].footer.text, any_string=True) other_recipients = match_other_recipients(ctx.channel.topic) for n, uid in enumerate(other_recipients): - other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipients[n] = await self.bot.get_or_fetch_user(uid) if user_id != -1: recipient = self.bot.get_user(user_id) @@ -1862,11 +2288,12 @@ async def repair(self, ctx): # match username from channel name # username-1234, username-1234_1, username-1234_2 - m = re.match(r"^(.+)-(\d{4})(?:_\d+)?$", ctx.channel.name) + m = re.match(r"^(.+?)(?:-(\d{4}))?(?:_\d+)?$", ctx.channel.name) if m is not None: users = set( filter( - lambda member: member.name == m.group(1) and member.discriminator == m.group(2), + lambda member: member.name == m.group(1) + and (member.discriminator == "0" or member.discriminator == m.group(2)), ctx.guild.members, ) ) @@ -1893,7 +2320,7 @@ async def repair(self, ctx): other_recipients = match_other_recipients(ctx.channel.topic) for n, uid in enumerate(other_recipients): - other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipients[n] = await self.bot.get_or_fetch_user(uid) if recipient is None: self.bot.threads.cache[user.id] = thread = Thread( @@ -1906,7 +2333,9 @@ async def repair(self, ctx): thread.ready = True logger.info("Setting current channel's topic to User ID and created new thread.") await ctx.channel.edit( - reason="Fix broken Modmail thread", name=name, topic=f"User ID: {user.id}" + reason="Fix broken Modmail thread", + name=name, + topic=f"User ID: {user.id}", ) return await self.bot.add_reaction(ctx.message, sent_emoji) @@ -1959,7 +2388,7 @@ async def disable_new(self, ctx): description="Modmail will not create any new threads.", color=self.bot.main_color, ) - if self.bot.config["dm_disabled"] < DMDisabled.NEW_THREADS: + if self.bot.config["dm_disabled"] != DMDisabled.NEW_THREADS: self.bot.config["dm_disabled"] = DMDisabled.NEW_THREADS await self.bot.config.update() @@ -2013,6 +2442,344 @@ async def isenable(self, ctx): return await ctx.send(embed=embed) + @commands.command(usage="[duration]") + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def snooze(self, ctx, *, duration: UserFriendlyTime = None): + """ + Snooze this thread. Behavior depends on config: + - delete (default): deletes the channel and restores it later + - move: moves the channel to the configured snoozed category + Optionally specify a duration, e.g. 'snooze 2d' for 2 days. + Uses config: snooze_default_duration, snooze_title, snooze_text + """ + thread = ctx.thread + if thread.snoozed: + await ctx.send("This thread is already snoozed.") + logging.info(f"[SNOOZE] Thread for {getattr(thread.recipient, 'id', None)} already snoozed.") + return + # Default snooze duration with safe fallback + try: + default_snooze = int(self.bot.config.get("snooze_default_duration", 604800)) + except (ValueError, TypeError): + default_snooze = 604800 + if duration: + snooze_for = int((duration.dt - duration.now).total_seconds()) + snooze_for = min(snooze_for, default_snooze) + else: + snooze_for = default_snooze + + # Capacity pre-check: if behavior is move, ensure snoozed category has room (<49 channels) + behavior = (self.bot.config.get("snooze_behavior") or "delete").lower() + if behavior == "move": + snoozed_cat_id = self.bot.config.get("snoozed_category_id") + target_category = None + if snoozed_cat_id: + try: + target_category = self.bot.modmail_guild.get_channel(int(snoozed_cat_id)) + except Exception: + target_category = None + # Auto-create snoozed category if missing + if not isinstance(target_category, discord.CategoryChannel): + try: + logging.info("Auto-creating snoozed category for move-based snoozing.") + # Hide category by default; only bot can view/manage + overwrites = { + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(view_channel=False) + } + bot_member = self.bot.modmail_guild.me + if bot_member is not None: + overwrites[bot_member] = discord.PermissionOverwrite( + view_channel=True, + send_messages=True, + read_message_history=True, + manage_channels=True, + manage_messages=True, + attach_files=True, + embed_links=True, + add_reactions=True, + ) + target_category = await self.bot.modmail_guild.create_category( + name="Snoozed Threads", + overwrites=overwrites, + reason="Auto-created snoozed category for move-based snoozing", + ) + try: + await self.bot.config.set("snoozed_category_id", target_category.id) + await self.bot.config.update() + except Exception as e: + logging.warning("Failed to persist snoozed_category_id: %s", e) + try: + await ctx.send( + "⚠️ Created snoozed category but failed to save it to config. Please set `snoozed_category_id` manually." + ) + except Exception as e: + logging.info( + "Failed to notify about snoozed category persistence issue: %s", + e, + ) + await ctx.send( + embed=discord.Embed( + title="Snoozed category created", + description=( + f"Created category {target_category.mention if hasattr(target_category, 'mention') else target_category.name} " + "and set it as `snoozed_category_id`." + ), + color=self.bot.main_color, + ) + ) + except Exception as e: + await ctx.send( + embed=discord.Embed( + title="Could not create snoozed category", + description=( + "I couldn't create a category automatically. Please ensure I have Manage Channels " + "permission, or set `snoozed_category_id` manually." + ), + color=self.bot.error_color, + ) + ) + logging.warning("Failed to auto-create snoozed category: %s", e) + # Capacity check after ensuring category exists + if isinstance(target_category, discord.CategoryChannel): + try: + if len(target_category.channels) >= 49: + await ctx.send( + embed=discord.Embed( + title="Snooze unavailable", + description=( + "The configured snoozed category is full (49 channels). " + "Unsnooze or move some channels out before snoozing more." + ), + color=self.bot.error_color, + ) + ) + return + except Exception as e: + logging.debug("Failed to check snoozed category channel count: %s", e) + + # Store snooze_until timestamp for reliable auto-unsnooze + now = datetime.now(timezone.utc) + snooze_until = now + timedelta(seconds=snooze_for) + await self.bot.api.logs.update_one( + {"recipient.id": str(thread.id)}, + { + "$set": { + "snooze_start": now.isoformat(), + "snooze_for": snooze_for, + "snooze_until": snooze_until.isoformat(), + } + }, + ) + embed = discord.Embed( + title=self.bot.config.get("snooze_title") or "Thread Snoozed", + description=self.bot.config.get("snooze_text") or "This thread has been snoozed.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed) + ok = await thread.snooze(moderator=ctx.author, snooze_for=snooze_for) + if ok: + logging.info( + f"[SNOOZE] Thread for {getattr(thread.recipient, 'id', None)} snoozed for {snooze_for}s." + ) + self.bot.threads.cache[thread.id] = thread + else: + await ctx.send("Failed to snooze this thread.") + logging.error(f"[SNOOZE] Failed to snooze thread for {getattr(thread.recipient, 'id', None)}.") + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def unsnooze(self, ctx, *, user: str = None): + """ + Unsnooze a thread: restores the channel and replays messages. + You can specify a user by mention or ID, or run in a thread channel to unsnooze that thread. + Uses config: unsnooze_text + """ + import discord + + thread = None + user_obj = None + if user is not None: + user_id = self._resolve_user(user) + if user_id: + try: + user_obj = await self.bot.get_or_fetch_user(user_id) + except Exception: + logger.debug( + "Failed fetching user during unsnooze; falling back to partial object (%s).", + user_id, + exc_info=True, + ) + user_obj = discord.Object(user_id) + if user_obj: + thread = await self.bot.threads.find(recipient=user_obj) + if not thread: + await ctx.send(f"[DEBUG] No thread found for user {user} (obj: {user_obj}).") + logging.warning(f"[UNSNOOZE] No thread found for user {user} (obj: {user_obj})") + return + elif hasattr(ctx, "thread"): + thread = ctx.thread + else: + await ctx.send("This is not a Modmail thread.") + logging.warning("[UNSNOOZE] Not a Modmail thread context.") + return + if not thread.snoozed: + await ctx.send("This thread is not snoozed.") + logging.info(f"[UNSNOOZE] Thread for {getattr(thread.recipient, 'id', None)} is not snoozed.") + return + + # Manually fetch snooze_data if the thread object doesn't have it + if not thread.snooze_data: + log_entry = await self.bot.api.logs.find_one({"recipient.id": str(thread.id), "snoozed": True}) + if log_entry: + thread.snooze_data = log_entry.get("snooze_data") + + ok = await thread.restore_from_snooze() + if ok: + self.bot.threads.cache[thread.id] = thread + await ctx.send( + self.bot.config.get("unsnooze_text") or "This thread has been unsnoozed and restored." + ) + logging.info(f"[UNSNOOZE] Thread for {getattr(thread.recipient, 'id', None)} unsnoozed.") + else: + await ctx.send("Failed to unsnooze this thread.") + logging.error( + f"[UNSNOOZE] Failed to unsnooze thread for {getattr(thread.recipient, 'id', None)}." + ) + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def snoozed(self, ctx): + """ + List all currently snoozed threads/users. + """ + snoozed_threads = [thread for thread in self.bot.threads.cache.values() if thread.snoozed] + if not snoozed_threads: + await ctx.send("No threads are currently snoozed.") + return + + lines = [] + now = datetime.now(timezone.utc) + for thread in snoozed_threads: + user = thread.recipient.name if thread.recipient else "Unknown" + user_id = thread.id + + since_str = "?" + until_str = "?" + + if thread.snooze_data: + since = thread.snooze_data.get("snooze_start") + duration = thread.snooze_data.get("snooze_for") + + if since: + try: + since_dt = datetime.fromisoformat(since) + since_str = f"" # Discord relative timestamp + except (ValueError, TypeError) as e: + logging.warning(f"[SNOOZED] Invalid snooze_start for {user_id}: {since} ({e})") + else: + logging.warning(f"[SNOOZED] Missing snooze_start for {user_id}") + + if duration and since_str != "?": + try: + until_dt = datetime.fromisoformat(since) + timedelta(seconds=int(duration)) + until_str = f"" + except (ValueError, TypeError) as e: + logging.warning( + f"[SNOOZED] Invalid until time for {user_id}: {since} + {duration} ({e})" + ) + + lines.append(f"- {user} (`{user_id}`) since {since_str}, until {until_str}") + + await ctx.send("Snoozed threads:\n" + "\n".join(lines)) + + async def cog_load(self): + self.snooze_auto_unsnooze.start() + + @tasks.loop(seconds=10) + async def snooze_auto_unsnooze(self): + now = datetime.now(timezone.utc) + snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) + for entry in snoozed: + snooze_until = entry.get("snooze_until") + if snooze_until: + try: + until_dt = datetime.fromisoformat(snooze_until) + if now >= until_dt: + thread = await self.bot.threads.find(recipient_id=int(entry["recipient"]["id"])) + if thread and thread.snoozed: + await thread.restore_from_snooze() + except (ValueError, TypeError) as e: + logger.debug( + "Failed parsing snooze_until timestamp for auto-unsnooze loop: %s", + e, + ) + + @snooze_auto_unsnooze.before_loop + async def _snooze_auto_unsnooze_before(self): + await self.bot.wait_until_ready() -def setup(bot): - bot.add_cog(Modmail(bot)) + async def process_dm_modmail(self, message: discord.Message) -> None: + # ... existing code ... + # Before processing, check if thread is snoozed and auto-unsnooze + thread = await self.threads.find(recipient=message.author) + if thread and thread.snoozed: + await thread.restore_from_snooze() + # Ensure the thread object in the cache is updated with the new channel + self.threads.cache[thread.id] = thread + # ... rest of the method unchanged ... + + @commands.command() + @checks.has_permissions(PermissionLevel.OWNER) + async def clearsnoozed(self, ctx): + """ + List all snoozed threads and ask for confirmation before clearing (unsnoozing) all of them. + Only proceed if the user confirms. + """ + snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) + if not snoozed: + await ctx.send("No threads are currently snoozed.") + return + lines = [] + for entry in snoozed: + user = entry.get("recipient", {}).get("name", "Unknown") + user_id = entry.get("recipient", {}).get("id", "?") + lines.append(f"- {user} (`{user_id}`)") + msg = await ctx.send( + "The following threads are currently snoozed and will be unsnoozed if you confirm:\n" + + "\n".join(lines) + + "\n\nType `yes` to confirm, or anything else to cancel." + ) + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + try: + reply = await self.bot.wait_for("message", check=check, timeout=30) + except asyncio.TimeoutError: + await ctx.send("Timed out. No threads were unsnoozed.") + return + if reply.content.strip().lower() != "yes": + await ctx.send("Cancelled. No threads were unsnoozed.") + return + count = 0 + for entry in snoozed: + user_id = entry.get("recipient", {}).get("id") + if not user_id: + continue + user_obj = None + try: + user_obj = await self.bot.get_or_fetch_user(int(user_id)) + except Exception: + user_obj = discord.Object(int(user_id)) + thread = await self.bot.threads.find(recipient=user_obj) + if thread and thread.snoozed: + ok = await thread.restore_from_snooze() + if ok: + self.bot.threads.cache[thread.id] = thread + count += 1 + await ctx.send(f"Unsnoozed {count} threads.") + + +async def setup(bot): + await bot.add_cog(Modmail(bot)) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4cdd0aba3f..aa4ad5a65c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -6,8 +6,8 @@ import sys import typing import zipfile -from importlib import invalidate_caches from difflib import get_close_matches +from importlib import invalidate_caches from pathlib import Path, PurePath from re import match from site import USER_SITE @@ -15,13 +15,12 @@ import discord from discord.ext import commands - -from pkg_resources import parse_version +from packaging.version import Version from core import checks from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession -from core.utils import truncate, trigger_typing +from core.utils import trigger_typing, truncate, safe_typing logger = getLogger(__name__) @@ -114,7 +113,7 @@ class Plugins(commands.Cog): These addons could have a range of features from moderation to simply making your life as a moderator easier! Learn how to create a plugin yourself here: - https://github.com/kyb3r/modmail/wiki/Plugins + https://github.com/modmail-dev/modmail/wiki/Plugins """ def __init__(self, bot): @@ -123,21 +122,22 @@ def __init__(self, bot): self.loaded_plugins = set() self._ready_event = asyncio.Event() - self.bot.loop.create_task(self.populate_registry()) - + async def cog_load(self): + await self.populate_registry() if self.bot.config.get("enable_plugins"): - self.bot.loop.create_task(self.initial_load_plugins()) + await self.initial_load_plugins() else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") async def populate_registry(self): - url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" - async with self.bot.session.get(url) as resp: - self.registry = json.loads(await resp.text()) + url = "https://raw.githubusercontent.com/modmail-dev/modmail/master/plugins/registry.json" + try: + async with self.bot.session.get(url) as resp: + self.registry = json.loads(await resp.text()) + except asyncio.TimeoutError: + logger.warning("Failed to fetch registry. Loading with empty registry") async def initial_load_plugins(self): - await self.bot.wait_for_connected() - for plugin_name in list(self.bot.config["plugins"]): try: plugin = Plugin.from_string(plugin_name, strict=True) @@ -251,14 +251,18 @@ async def load_plugin(self, plugin): if stderr: logger.debug("[stderr]\n%s.", stderr.decode()) - logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + logger.error( + "Failed to download requirements for %s.", + plugin.ext_string, + exc_info=True, + ) raise InvalidPluginError(f"Unable to download requirements: ```\n{stderr.decode()}\n```") if os.path.exists(USER_SITE): sys.path.insert(0, USER_SITE) try: - self.bot.load_extension(plugin.ext_string) + await self.bot.load_extension(plugin.ext_string) logger.info("Loaded plugin: %s", plugin.ext_string.split(".")[-1]) self.loaded_plugins.add(plugin) @@ -266,8 +270,18 @@ async def load_plugin(self, plugin): logger.error("Plugin load failure: %s", plugin.ext_string, exc_info=True) raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc - async def parse_user_input(self, ctx, plugin_name, check_version=False): + async def unload_plugin(self, plugin: Plugin) -> None: + try: + await self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError as exc: + raise exc + + ext_parent = ".".join(plugin.ext_string.split(".")[:-1]) + for module in list(sys.modules.keys()): + if module == ext_parent or module.startswith(ext_parent + "."): + del sys.modules[module] + async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self.bot.config["enable_plugins"]: embed = discord.Embed( description="Plugins are disabled, enable them by setting `ENABLE_PLUGINS=true`", @@ -292,7 +306,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if check_version: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < Version(required_version): embed = discord.Embed( description="Your bot's version is too low. " f"This plugin requires version `{required_version}`.", @@ -304,6 +318,14 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin(user, repo, plugin_name, branch) else: + if self.bot.config.get("registry_plugins_only"): + embed = discord.Embed( + description="This plugin is not in the registry. To install this plugin, " + "you must set `REGISTRY_PLUGINS_ONLY=no` or remove this key in your .env file.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed) + return try: plugin = Plugin.from_string(plugin_name) except InvalidPluginError: @@ -343,7 +365,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): return if str(plugin) in self.bot.config["plugins"]: - embed = discord.Embed(description="This plugin is already installed.", color=self.bot.error_color) + embed = discord.Embed( + description="This plugin is already installed.", + color=self.bot.error_color, + ) return await ctx.send(embed=embed) if plugin.name in self.bot.cogs: @@ -372,7 +397,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", + description=f"Failed to download plugin, check logs for error.\n{type(e).__name__}: {e}", color=self.bot.error_color, ) @@ -382,7 +407,6 @@ async def plugins_add(self, ctx, *, plugin_name: str): await self.bot.config.update() if self.bot.config.get("enable_plugins"): - invalidate_caches() try: @@ -391,7 +415,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", + description=f"Failed to load plugin, check logs for error.\n{type(e).__name__}: {e}", color=self.bot.error_color, ) @@ -432,7 +456,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if self.bot.config.get("enable_plugins"): try: - self.bot.unload_extension(plugin.ext_string) + await self.unload_plugin(plugin) self.loaded_plugins.remove(plugin) except (commands.ExtensionNotLoaded, KeyError): logger.warning("Plugin was never loaded.") @@ -453,7 +477,8 @@ async def plugins_remove(self, ctx, *, plugin_name: str): pass # dir not empty embed = discord.Embed( - description="The plugin is successfully uninstalled.", color=self.bot.main_color + description="The plugin is successfully uninstalled.", + color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -467,16 +492,18 @@ async def update_plugin(self, ctx, plugin_name): embed = discord.Embed(description="Plugin is not installed.", color=self.bot.error_color) return await ctx.send(embed=embed) - async with ctx.typing(): + async with safe_typing(ctx): embed = discord.Embed( - description=f"Successfully updated {plugin.name}.", color=self.bot.main_color + description=f"Successfully updated {plugin.name}.", + color=self.bot.main_color, ) await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: - self.bot.unload_extension(plugin.ext_string) + await self.unload_plugin(plugin) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) + try: await self.load_plugin(plugin) except Exception: @@ -484,12 +511,12 @@ async def update_plugin(self, ctx, plugin_name): description=f"Failed to update {plugin.name}. This plugin will now be removed from your bot.", color=self.bot.error_color, ) - self.bot.config["plugins"].remove(plugin_name) - logger.debug("Failed to update %s. Removed plugin from config.", plugin_name) + self.bot.config["plugins"].remove(str(plugin)) + logger.debug("Failed to update %s. Removed plugin from config.", plugin) else: - logger.debug("Updated %s.", plugin_name) + logger.debug("Updated %s.", plugin) else: - logger.debug("Updated %s.", plugin_name) + logger.debug("Updated %s.", plugin) return await ctx.send(embed=embed) @plugins.command(name="update") @@ -523,17 +550,20 @@ async def plugins_reset(self, ctx): for ext in list(self.bot.extensions): if not ext.startswith("plugins."): continue + logger.error("Unloading plugin: %s.", ext) try: - logger.error("Unloading plugin: %s.", ext) - self.bot.unload_extension(ext) - except Exception: - logger.error("Failed to unload plugin: %s.", ext) - else: - if not self.loaded_plugins: - continue plugin = next((p for p in self.loaded_plugins if p.ext_string == ext), None) if plugin: + await self.unload_plugin(plugin) self.loaded_plugins.remove(plugin) + else: + await self.bot.unload_extension(ext) + except Exception: + logger.error("Failed to unload plugin: %s.", ext) + + for module in list(sys.modules.keys()): + if module.startswith("plugins."): + del sys.modules[module] self.bot.config["plugins"].clear() await self.bot.config.update() @@ -549,7 +579,8 @@ async def plugins_reset(self, ctx): logger.warning("Removing %s.", entry.name) embed = discord.Embed( - description="Successfully purged all plugins from the bot.", color=self.bot.main_color + description="Successfully purged all plugins from the bot.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -577,7 +608,8 @@ async def plugins_loaded(self, ctx): if not self.loaded_plugins: embed = discord.Embed( - description="There are no plugins currently loaded.", color=self.bot.error_color + description="There are no plugins currently loaded.", + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -619,6 +651,14 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N registry = sorted(self.registry.items(), key=lambda elem: elem[0]) + if not registry: + embed = discord.Embed( + color=self.bot.error_color, + description="Registry is empty. This could be because it failed to load.", + ) + await ctx.send(embed=embed) + return + if isinstance(plugin_name, int): index = plugin_name - 1 if index < 0: @@ -637,7 +677,10 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches)) + embed.add_field( + name="Perhaps you meant:", + value="\n".join(f"`{m}`" for m in matches), + ) return await ctx.send(embed=embed) @@ -669,7 +712,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N embed.set_footer(text="This plugin is currently loaded.") else: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < Version(required_version): embed.set_footer( text="Your bot is unable to install this plugin, " f"minimum required version is v{required_version}." @@ -730,12 +773,15 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) + embed.set_author( + name="Plugin Registry", + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, + ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() -def setup(bot): - bot.add_cog(Plugins(bot)) +async def setup(bot): + await bot.add_cog(Plugins(bot)) diff --git a/cogs/threadmenu.py b/cogs/threadmenu.py new file mode 100644 index 0000000000..7f9e193844 --- /dev/null +++ b/cogs/threadmenu.py @@ -0,0 +1,830 @@ +import json +import asyncio +from copy import copy as _copy + +import discord +from discord.ext import commands + +from core import checks +from core.models import PermissionLevel + + +class ThreadCreationMenuCore(commands.Cog): + """Core-integrated thread menu configuration and management. + + This Cog exposes the same commands as the legacy plugin to manage menu options, + but stores settings in core config (no plugin DB). + """ + + def __init__(self, bot): + self.bot = bot + + # ----- helpers ----- + def _get_conf(self) -> dict: + return { + "enabled": bool(self.bot.config.get("thread_creation_menu_enabled")), + "options": self.bot.config.get("thread_creation_menu_options") or {}, + "submenus": self.bot.config.get("thread_creation_menu_submenus") or {}, + "timeout": int(self.bot.config.get("thread_creation_menu_timeout") or 20), + "close_on_timeout": bool(self.bot.config.get("thread_creation_menu_close_on_timeout")), + "anonymous_menu": bool(self.bot.config.get("thread_creation_menu_anonymous_menu")), + "embed_text": self.bot.config.get("thread_creation_menu_embed_text") + or "Please select an option.", + "dropdown_placeholder": self.bot.config.get("thread_creation_menu_dropdown_placeholder") + or "Select an option to contact the staff team.", + "embed_title": self.bot.config.get("thread_creation_menu_embed_title"), + "embed_footer": self.bot.config.get("thread_creation_menu_embed_footer"), + "embed_thumbnail_url": self.bot.config.get("thread_creation_menu_embed_thumbnail_url"), + "embed_footer_icon_url": self.bot.config.get("thread_creation_menu_embed_footer_icon_url"), + "embed_color": self.bot.config.get("thread_creation_menu_embed_color"), + } + + async def _save_conf(self, conf: dict): + await self.bot.config.set("thread_creation_menu_enabled", conf.get("enabled", False)) + await self.bot.config.set("thread_creation_menu_options", conf.get("options", {}), convert=False) + await self.bot.config.set("thread_creation_menu_submenus", conf.get("submenus", {}), convert=False) + await self.bot.config.set("thread_creation_menu_timeout", conf.get("timeout", 20)) + await self.bot.config.set( + "thread_creation_menu_close_on_timeout", conf.get("close_on_timeout", False) + ) + await self.bot.config.set("thread_creation_menu_anonymous_menu", conf.get("anonymous_menu", False)) + await self.bot.config.set( + "thread_creation_menu_embed_text", conf.get("embed_text", "Please select an option.") + ) + await self.bot.config.set( + "thread_creation_menu_dropdown_placeholder", + conf.get("dropdown_placeholder", "Select an option to contact the staff team."), + ) + await self.bot.config.set("thread_creation_menu_embed_title", conf.get("embed_title")) + await self.bot.config.set("thread_creation_menu_embed_footer", conf.get("embed_footer")) + await self.bot.config.set("thread_creation_menu_embed_thumbnail_url", conf.get("embed_thumbnail_url")) + await self.bot.config.set( + "thread_creation_menu_embed_footer_icon_url", conf.get("embed_footer_icon_url") + ) + if conf.get("embed_color"): + try: + await self.bot.config.set("thread_creation_menu_embed_color", conf.get("embed_color")) + except Exception: + pass + await self.bot.config.update() + + # ----- commands ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @commands.group(invoke_without_command=True) + async def threadmenu(self, ctx): + """Thread-creation menu settings (core).""" + await ctx.send_help(ctx.command) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="toggle") + async def threadmenu_toggle(self, ctx): + """Enable or disable the thread-creation menu. + + Toggles the global on/off state. When disabled, users won't see + or be able to use the interactive thread creation select menu. + """ + conf = self._get_conf() + conf["enabled"] = not conf["enabled"] + await self._save_conf(conf) + await ctx.send(f"Thread-creation menu is now {'enabled' if conf['enabled'] else 'disabled'}.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="show") + async def threadmenu_show(self, ctx): + """Show all current main-menu options. + + Lists every option (label + description) configured in the root + (non-submenu) select menu so you can review what users will see. + """ + conf = self._get_conf() + if not conf["options"]: + return await ctx.send("There are no options in the main menu.") + embed = discord.Embed(title="Main menu", color=discord.Color.blurple()) + for v in conf["options"].values(): + embed.add_field(name=v["label"], value=v["description"], inline=False) + await ctx.send(embed=embed) + + # ----- options ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.group(name="option", invoke_without_command=True) + async def threadmenu_option(self, ctx): + """Manage main-menu options (add/remove/edit/show). + + Use subcommands: + - add: interactive wizard to create an option + - remove