From dce65143049e80266aeb6712403793d3d9de3331 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Fri, 12 Jan 2024 13:34:57 -0600 Subject: [PATCH 01/37] Make package reported logs an embed --- src/bot/exts/dragonfly/dragonfly.py | 35 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 8d4fff9..4187dc7 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -18,6 +18,28 @@ log.setLevel(logging.INFO) +def _build_package_report_log_embed( + *, + member: discord.User | discord.Member, + package_name: str, + package_version: str, + description: str | None, + inspector_url: str, +) -> discord.Embed: + embed = discord.Embed( + title=f"Package reported: {package_name} v{package_version}", + color=discord.Colour.red(), + description=description or "*No description provided*", + timestamp=datetime.now(tz=UTC), + ) + + embed.set_author(name=member.name, icon_url=member.display_avatar.url) + embed.add_field(name="Reported by", value=member.mention) + embed.add_field(name="Inspector URL", value=f"[Inspector URL]({inspector_url})") + + return embed + + class ConfirmReportModal(discord.ui.Modal): """Modal for confirming a report.""" @@ -87,13 +109,14 @@ async def on_submit(self: Self, interaction: discord.Interaction) -> None: log_channel = interaction.client.get_channel(Channels.reporting) if isinstance(log_channel, discord.abc.Messageable): - await log_channel.send( - f"User {interaction.user.mention} " - f"reported package `{self.package.name}` " - f"with additional_description `{additional_information_override}`" - f"with inspector_url `{inspector_url_override}`", + embed = _build_package_report_log_embed( + member=interaction.user, + package_name=self.package.name, + package_version=self.package.version, + description=additional_information_override, + inspector_url=inspector_url_override or self.package.inspector_url, ) - + await log_channel.send(embed=embed) try: await self.bot.dragonfly_services.report_package( name=self.package.name, From cc3d04cf5e33f633271d86230a58948f9ab8c0ac Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Sun, 4 Feb 2024 18:35:45 -0600 Subject: [PATCH 02/37] Reestablish versioning (#211) * Upgrade dependencies Signed-off-by: Bradley Reynolds * Bump the ci-dependencies group with 3 updates Bumps the ci-dependencies group with 3 updates: [actions/dependency-review-action](https://github.com/actions/dependency-review-action), [darbiadev/.github](https://github.com/darbiadev/.github) and [getsentry/action-release](https://github.com/getsentry/action-release). Updates `actions/dependency-review-action` from 3.1.2 to 4.0.0 - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/fde92acd0840415674c16b39c7d703fc28bc511e...4901385134134e04cec5fbe5ddfe3b2c5bd5d976) Updates `darbiadev/.github` from 4.0.1 to 13.0.0 - [Release notes](https://github.com/darbiadev/.github/releases) - [Commits](https://github.com/darbiadev/.github/compare/9160d4ddd590c15fe8a1f6d1704bf8806969d2b6...ea97d99e1520c46080c4c9032a69552e491474ac) Updates `getsentry/action-release` from 1.4.1 to 1.7.0 - [Release notes](https://github.com/getsentry/action-release/releases) - [Commits](https://github.com/getsentry/action-release/compare/4744f6a65149f441c5f396d5b0877307c0db52c7...e769183448303de84c5a06aaaddf9da7be26d6c7) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-major dependency-group: ci-dependencies - dependency-name: darbiadev/.github dependency-type: direct:production update-type: version-update:semver-major dependency-group: ci-dependencies - dependency-name: getsentry/action-release dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ci-dependencies ... Signed-off-by: dependabot[bot] * Fixup lints Signed-off-by: Bradley Reynolds * Set version to 5.0.0 Signed-off-by: Bradley Reynolds * Build container on PR Signed-off-by: Bradley Reynolds * Setup lockfile for DevContainer Signed-off-by: Bradley Reynolds --------- Signed-off-by: Bradley Reynolds Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .devcontainer/devcontainer-lock.json | 14 ++++ .github/dependabot.yaml | 5 ++ ...ld-push.yaml => container-build-push.yaml} | 7 +- .github/workflows/dependency-review.yaml | 2 +- .github/workflows/python-ci.yaml | 8 +-- .github/workflows/sentry-release.yaml | 2 +- .pre-commit-config.yaml | 4 +- Containerfile => Dockerfile | 0 docs/source/changelog.rst | 3 + docs/source/conf.py | 6 +- make.ps1 | 21 +++--- pyproject.toml | 28 ++++++-- requirements/requirements-dev.in | 1 + requirements/requirements-dev.txt | 24 ++++--- requirements/requirements-docs.txt | 41 +++++------ requirements/requirements-tests.txt | 6 +- requirements/requirements.txt | 68 +++++++++---------- src/bot/__main__.py | 12 ++-- src/bot/bot.py | 12 ++-- src/bot/constants.py | 8 +-- src/bot/dragonfly_services.py | 12 ++-- src/bot/exts/audit.py | 12 ++-- src/bot/exts/core/error_handler.py | 30 ++++---- src/bot/exts/core/log.py | 8 +-- src/bot/exts/core/ping.py | 2 +- src/bot/exts/core/sync.py | 8 +-- src/bot/exts/dragonfly/dragonfly.py | 34 +++++----- src/bot/exts/utilities/internal.py | 28 ++++---- src/bot/log.py | 6 +- src/bot/utils/__init__.py | 4 +- src/bot/utils/function.py | 10 +-- src/bot/utils/helpers.py | 2 +- src/bot/utils/lock.py | 14 ++-- src/bot/utils/messages.py | 6 +- 34 files changed, 243 insertions(+), 205 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json rename .github/workflows/{docker-build-push.yaml => container-build-push.yaml} (67%) rename Containerfile => Dockerfile (100%) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..a0f61ec --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/powershell:1": { + "version": "1.2.0", + "resolved": "ghcr.io/devcontainers/features/powershell@sha256:3b8a159d67a68419cbc13f09413fc3523c1a8f13d64bfb3aa7119df4fd324d0e", + "integrity": "sha256:3b8a159d67a68419cbc13f09413fc3523c1a8f13d64bfb3aa7119df4fd324d0e" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "1.3.2", + "resolved": "ghcr.io/devcontainers/features/python@sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006", + "integrity": "sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006" + } + } +} diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 863847f..611de21 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,5 +1,10 @@ version: 2 updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/container-build-push.yaml similarity index 67% rename from .github/workflows/docker-build-push.yaml rename to .github/workflows/container-build-push.yaml index f526399..2bbb841 100644 --- a/.github/workflows/docker-build-push.yaml +++ b/.github/workflows/container-build-push.yaml @@ -1,4 +1,4 @@ -name: "Docker Build and Push" +name: "Container Build and Push" on: push: @@ -6,6 +6,7 @@ on: - main tags: - v* + pull_request: permissions: contents: read @@ -16,4 +17,6 @@ permissions: jobs: build-push: - uses: darbiadev/.github/.github/workflows/docker-build-push.yaml@9160d4ddd590c15fe8a1f6d1704bf8806969d2b6 # v4.0.1 + uses: darbiadev/.github/.github/workflows/docker-build-push.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 + with: + file-name: Dockerfile diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index cb31094..22f739e 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -15,6 +15,6 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: "Dependency Review" - uses: actions/dependency-review-action@fde92acd0840415674c16b39c7d703fc28bc511e # v3.1.2 + uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 with: config-file: darbiadev/.github/.github/dependency-review-config.yaml@f185cc076161b47921c6fb6da4c1fd5e40b50bff # v3.0.0 diff --git a/.github/workflows/python-ci.yaml b/.github/workflows/python-ci.yaml index 28e8b79..b0f7aa2 100644 --- a/.github/workflows/python-ci.yaml +++ b/.github/workflows/python-ci.yaml @@ -8,13 +8,13 @@ on: jobs: pre-commit: - uses: darbiadev/.github/.github/workflows/generic-precommit.yaml@9160d4ddd590c15fe8a1f6d1704bf8806969d2b6 # v4.0.1 + uses: darbiadev/.github/.github/workflows/generic-precommit.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 with: python-version: "3.11" lint: needs: pre-commit - uses: darbiadev/.github/.github/workflows/python-lint.yaml@9160d4ddd590c15fe8a1f6d1704bf8806969d2b6 # v4.0.1 + uses: darbiadev/.github/.github/workflows/python-lint.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 with: python-version: "3.11" @@ -25,7 +25,7 @@ jobs: os: [ ubuntu-latest ] python-version: [ "3.11" ] - uses: darbiadev/.github/.github/workflows/python-test.yaml@9160d4ddd590c15fe8a1f6d1704bf8806969d2b6 # v4.0.1 + uses: darbiadev/.github/.github/workflows/python-test.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 with: os: ${{ matrix.os }} python-version: ${{ matrix.python-version }} @@ -37,6 +37,6 @@ jobs: pages: write id-token: write - uses: darbiadev/.github/.github/workflows/github-pages-python-sphinx.yaml@9160d4ddd590c15fe8a1f6d1704bf8806969d2b6 # v4.0.1 + uses: darbiadev/.github/.github/workflows/github-pages-python-sphinx.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 with: python-version: "3.11" diff --git a/.github/workflows/sentry-release.yaml b/.github/workflows/sentry-release.yaml index cbc2104..f7c5e32 100644 --- a/.github/workflows/sentry-release.yaml +++ b/.github/workflows/sentry-release.yaml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: "Create Sentry release" - uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7 # v1.4.1 + uses: getsentry/action-release@e769183448303de84c5a06aaaddf9da7be26d6c7 # v1.7.0 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a28812b..98da483 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -13,7 +13,7 @@ repos: args: [ --fix=lf ] - id: end-of-file-fixer - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.4" + rev: "v0.2.0" hooks: - id: ruff - id: ruff-format diff --git a/Containerfile b/Dockerfile similarity index 100% rename from Containerfile rename to Dockerfile diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c606234..320de9c 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,9 @@ Changelog ========= +- :release:`5.0.0 <4th February 2024>` +- :feature:`-` Reestablish versioning + - :release:`4.1.0 <25th October 2023>` - :feature:`188` Make Dragonfly embeds consistent diff --git a/docs/source/conf.py b/docs/source/conf.py index 5e2a052..936a864 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ def linkcode_resolve(domain: str, info: dict) -> str: if not info["module"]: return None - import importlib - import inspect - import types + import importlib # noqa: PLC0415 + import inspect # noqa: PLC0415 + import types # noqa: PLC0415 mod = importlib.import_module(info["module"]) diff --git a/make.ps1 b/make.ps1 index 0ac4b79..64fe589 100644 --- a/make.ps1 +++ b/make.ps1 @@ -11,7 +11,7 @@ COMMANDS install-dev install local package in editable mode update-deps update the dependencies upgrade-deps upgrade the dependencies - lint run `pre-commit` and `black` and `ruff` + lint run `pre-commit` and `ruff` test run `pytest` build-dist run `python -m build` clean delete generated content @@ -41,27 +41,28 @@ function Invoke-Install-Dev function Invoke-Update-Deps { python -m pip install --upgrade pip-tools - pip-compile --resolver=backtracking requirements/requirements.in --output-file requirements/requirements.txt - pip-compile --resolver=backtracking requirements/requirements-dev.in --output-file requirements/requirements-dev.txt - pip-compile --resolver=backtracking requirements/requirements-tests.in --output-file requirements/requirements-tests.txt - pip-compile --resolver=backtracking requirements/requirements-docs.in --output-file requirements/requirements-docs.txt + pip-compile requirements/requirements.in + pip-compile requirements/requirements-dev.in + pip-compile requirements/requirements-tests.in + pip-compile requirements/requirements-docs.in } function Invoke-Upgrade-Deps { python -m pip install --upgrade pip-tools pre-commit pre-commit autoupdate - pip-compile --resolver=backtracking --upgrade requirements/requirements.in --output-file requirements/requirements.txt - pip-compile --resolver=backtracking --upgrade requirements/requirements-dev.in --output-file requirements/requirements-dev.txt - pip-compile --resolver=backtracking --upgrade requirements/requirements-tests.in --output-file requirements/requirements-tests.txt - pip-compile --resolver=backtracking --upgrade requirements/requirements-docs.in --output-file requirements/requirements-docs.txt + pip-compile --upgrade requirements/requirements.in + pip-compile --upgrade requirements/requirements-dev.in + pip-compile --upgrade requirements/requirements-tests.in + pip-compile --upgrade requirements/requirements-docs.in } function Invoke-Lint { pre-commit run --all-files - python -m black . python -m ruff --fix . + python -m ruff format . + python -m mypy --strict src/ } function Invoke-Test diff --git a/pyproject.toml b/pyproject.toml index 368011a..e6c986b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bot" -version = "4.1.0" +version = "5.0.0" dynamic = ["dependencies", "optional-dependencies"] [project.urls] @@ -19,20 +19,22 @@ dev = { file = ["requirements/requirements-dev.txt"] } tests = { file = ["requirements/requirements-tests.txt"] } docs = { file = ["requirements/requirements-docs.txt"] } -[tool.black] -target-version = ["py311"] -line-length = 120 - [tool.ruff] +preview = true +unsafe-fixes = true target-version = "py311" line-length = 120 + +[tool.ruff.lint] select = ["ALL"] ignore = [ + "CPY001", # (Missing copyright notice at top of file) "G004", # (Logging statement uses f-string) - Developer UX "S101", # (Use of `assert` detected) - This should probably be changed + "PLR6301", # (Method `x` could be a function, class method, or static method) - false positives ] -[tool.ruff.extend-per-file-ignores] +[tool.ruff.lint.extend-per-file-ignores] "docs/*" = [ "INP001", # (File `tests/*.py` is part of an implicit namespace package. Add an `__init__.py`.) - Docs are not modules ] @@ -41,5 +43,17 @@ ignore = [ "S101", # (Use of `assert` detected) - Yes, that's the point ] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" + +[[tool.mypy.overrides]] +module = [ + "coloredlogs", + "pydis_core.*" +] +ignore_missing_imports = true + +[tool.coverage.run] +source = [ + "bot", +] diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in index 1d1a15e..4cb96ee 100644 --- a/requirements/requirements-dev.in +++ b/requirements/requirements-dev.in @@ -4,3 +4,4 @@ pip-tools pre-commit ruff +mypy diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 77eead6..9fca7b7 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -10,33 +10,41 @@ cfgv==3.4.0 # via pre-commit click==8.1.7 # via pip-tools -distlib==0.3.7 +distlib==0.3.8 # via virtualenv -filelock==3.12.4 +filelock==3.13.1 # via # -c requirements/requirements.txt # virtualenv -identify==2.5.30 +identify==2.5.33 # via pre-commit +mypy==1.8.0 + # via -r requirements/requirements-dev.in +mypy-extensions==1.0.0 + # via mypy nodeenv==1.8.0 # via pre-commit packaging==23.2 # via build pip-tools==7.3.0 # via -r requirements/requirements-dev.in -platformdirs==3.11.0 +platformdirs==4.2.0 # via virtualenv -pre-commit==3.5.0 +pre-commit==3.6.0 # via -r requirements/requirements-dev.in pyproject-hooks==1.0.0 # via build pyyaml==6.0.1 # via pre-commit -ruff==0.1.5 +ruff==0.2.0 # via -r requirements/requirements-dev.in -virtualenv==20.24.5 +typing-extensions==4.9.0 + # via + # -c requirements/requirements.txt + # mypy +virtualenv==20.25.0 # via pre-commit -wheel==0.41.2 +wheel==0.42.0 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index 4441f91..a5e0b3b 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -2,45 +2,45 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/requirements-docs.txt requirements/requirements-docs.in +# pip-compile requirements/requirements-docs.in # -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx anyascii==0.3.2 # via sphinx-autoapi -astroid==3.0.0 +astroid==3.0.2 # via sphinx-autoapi -babel==2.13.0 +babel==2.14.0 # via sphinx -beautifulsoup4==4.12.2 +beautifulsoup4==4.12.3 # via furo -certifi==2023.7.22 +certifi==2024.2.2 # via # -c requirements/requirements.txt # requests -charset-normalizer==3.3.0 +charset-normalizer==3.3.2 # via # -c requirements/requirements.txt # requests docutils==0.20.1 # via sphinx -furo==2023.9.10 +furo==2024.1.29 # via -r requirements/requirements-docs.in -idna==3.4 +idna==3.6 # via # -c requirements/requirements.txt # requests imagesize==1.4.1 # via sphinx -jinja2==3.1.2 +jinja2==3.1.3 # via # sphinx # sphinx-autoapi -markupsafe==2.1.3 +markupsafe==2.1.5 # via jinja2 packaging==23.2 # via sphinx -pygments==2.16.1 +pygments==2.17.2 # via # furo # sphinx @@ -65,28 +65,23 @@ sphinx==7.2.6 # releases # sphinx-autoapi # sphinx-basic-ng - # sphinxcontrib-applehelp - # sphinxcontrib-devhelp - # sphinxcontrib-htmlhelp - # sphinxcontrib-qthelp - # sphinxcontrib-serializinghtml sphinx-autoapi==3.0.0 # via -r requirements/requirements-docs.in sphinx-basic-ng==1.0.0b2 # via furo -sphinxcontrib-applehelp==1.0.7 +sphinxcontrib-applehelp==1.0.8 # via sphinx -sphinxcontrib-devhelp==1.0.5 +sphinxcontrib-devhelp==1.0.6 # via sphinx -sphinxcontrib-htmlhelp==2.0.4 +sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.6 +sphinxcontrib-qthelp==1.0.7 # via sphinx -sphinxcontrib-serializinghtml==1.1.9 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx -urllib3==2.0.6 +urllib3==2.2.0 # via # -c requirements/requirements.txt # requests diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index ee4cd90..345badb 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -2,15 +2,15 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/requirements-tests.txt requirements/requirements-tests.in +# pip-compile requirements/requirements-tests.in # iniconfig==2.0.0 # via pytest packaging==23.2 # via pytest -pluggy==1.3.0 +pluggy==1.4.0 # via pytest -pytest==7.4.3 +pytest==8.0.0 # via # -r requirements/requirements-tests.in # pytest-randomly diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d1cb12a..6a8e8d5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,114 +2,108 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in +# pip-compile requirements/requirements.in # aiodns==3.1.1 # via pydis-core -aiohttp==3.8.6 +aiohttp==3.9.3 # via # -r requirements/requirements.in # discord-py aiosignal==1.3.1 # via aiohttp -annotated-types==0.5.0 +annotated-types==0.6.0 # via pydantic arrow==1.3.0 # via -r requirements/requirements.in -async-timeout==4.0.3 +attrs==23.2.0 # via aiohttp -attrs==23.1.0 - # via aiohttp -certifi==2023.7.22 +certifi==2024.2.2 # via # requests # sentry-sdk cffi==1.16.0 # via pycares -charset-normalizer==3.3.0 - # via - # aiohttp - # requests +charset-normalizer==3.3.2 + # via requests coloredlogs==15.0.1 # via -r requirements/requirements.in discord-py==2.3.2 # via # -r requirements/requirements.in # pydis-core -filelock==3.12.4 +filelock==3.13.1 # via tldextract -frozenlist==1.4.0 +frozenlist==1.4.1 # via # aiohttp # aiosignal -greenlet==3.0.0 +greenlet==3.0.3 # via sqlalchemy humanfriendly==10.0 # via coloredlogs -idna==3.4 +idna==3.6 # via # requests # tldextract # yarl -multidict==6.0.4 +multidict==6.0.5 # via # aiohttp # yarl -psycopg[binary]==3.1.12 +psycopg[binary]==3.1.17 # via -r requirements/requirements.in -psycopg-binary==3.1.12 +psycopg-binary==3.1.17 # via psycopg -pycares==4.3.0 +pycares==4.4.0 # via aiodns pycparser==2.21 # via cffi -pydantic==2.4.2 +pydantic==2.6.0 # via # pydantic-settings # pydis-core -pydantic-core==2.10.1 +pydantic-core==2.16.1 # via pydantic -pydantic-settings==2.0.3 +pydantic-settings==2.1.0 # via -r requirements/requirements.in -pydis-core==10.4.0 +pydis-core==10.7.0 # via -r requirements/requirements.in python-dateutil==2.8.2 # via arrow -python-dotenv==1.0.0 +python-dotenv==1.0.1 # via pydantic-settings -rapidfuzz==3.5.2 +rapidfuzz==3.6.1 # via -r requirements/requirements.in -regex==2023.10.3 +regex==2023.12.25 # via -r requirements/requirements.in requests==2.31.0 # via # requests-file # tldextract -requests-file==1.5.1 +requests-file==2.0.0 # via tldextract -sentry-sdk==1.34.0 +sentry-sdk==1.40.0 # via -r requirements/requirements.in six==1.16.0 - # via - # python-dateutil - # requests-file -sqlalchemy==2.0.23 + # via python-dateutil +sqlalchemy==2.0.25 # via -r requirements/requirements.in statsd==4.0.1 # via pydis-core -tldextract==5.1.0 +tldextract==5.1.1 # via -r requirements/requirements.in -types-python-dateutil==2.8.19.14 +types-python-dateutil==2.8.19.20240106 # via arrow -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # psycopg # pydantic # pydantic-core # sqlalchemy -urllib3==2.0.6 +urllib3==2.2.0 # via # requests # sentry-sdk -yarl==1.9.2 +yarl==1.9.4 # via aiohttp diff --git a/src/bot/__main__.py b/src/bot/__main__.py index 19c66b4..2aaac11 100644 --- a/src/bot/__main__.py +++ b/src/bot/__main__.py @@ -22,18 +22,18 @@ def get_prefix(bot_: Bot, message_: discord.Message) -> Callable[[Bot, discord.Message], list[str]]: """Return a callable to check for the bot's prefix.""" extras = constants.Bot.prefix.split(",") - return commands.when_mentioned_or(*extras)(bot_, message_) + return commands.when_mentioned_or(*extras)(bot_, message_) # type: ignore[return-value] async def main() -> None: """Run the bot.""" async with ClientSession(headers={"Content-Type": "application/json"}, timeout=ClientTimeout(total=10)) as session: bot = Bot( - guild_id=constants.Guild.id, - http_session=session, - allowed_roles=list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}), - command_prefix=get_prefix, - intents=intents, + guild_id=constants.Guild.id, # type: ignore[arg-type] + http_session=session, # type: ignore[arg-type] + allowed_roles=list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}), # type: ignore[arg-type] + command_prefix=get_prefix, # type: ignore[arg-type] + intents=intents, # type: ignore[arg-type] ) bot.dragonfly_services = DragonflyServices( diff --git a/src/bot/bot.py b/src/bot/bot.py index f7c585e..99c2564 100644 --- a/src/bot/bot.py +++ b/src/bot/bot.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) -class CommandTree(discord.app_commands.CommandTree): +class CommandTree(discord.app_commands.CommandTree): # type: ignore[type-arg] """Custom command tree that handles errors raised by commands.""" def __init__(self: Self, bot: commands.Bot) -> None: @@ -22,7 +22,7 @@ def __init__(self: Self, bot: commands.Bot) -> None: async def on_error( self: Self, - interaction: discord.Interaction, + interaction: discord.Interaction, # type: ignore[type-arg] error: discord.app_commands.AppCommandError, ) -> None: """Override the default error handler to handle custom errors.""" @@ -50,13 +50,13 @@ async def on_error( raise error -class Bot(BotBase): +class Bot(BotBase): # type: ignore[misc] """Bot implementation.""" def __init__( self: Self, - *args: tuple, - **kwargs: dict, + *args: tuple, # type: ignore[type-arg] + **kwargs: dict, # type: ignore[type-arg] ) -> None: """ Initialise the base bot instance. @@ -83,7 +83,7 @@ async def setup_hook(self: Self) -> None: log.debug("load_extensions") scheduling.create_task(self.load_extensions(exts)) - async def on_error(self: Self, event: str, *args: tuple, **kwargs: dict) -> None: + async def on_error(self: Self, event: str, *args: tuple, **kwargs: dict) -> None: # type: ignore[type-arg] """Log errors raised in event listeners rather than printing them to stderr.""" with push_scope() as scope: scope.set_tag("event", event) diff --git a/src/bot/constants.py b/src/bot/constants.py index 335101f..4313447 100644 --- a/src/bot/constants.py +++ b/src/bot/constants.py @@ -7,7 +7,7 @@ """ from os import getenv -from typing import ClassVar, Self +from typing import ClassVar from pydantic import root_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -125,7 +125,7 @@ class _Roles(EnvConfig, env_prefix="roles_"): class _Guild(EnvConfig, env_prefix="guild_"): - id: int = 1121450543462760448 # noqa: A003 - inside a class, this is fine + id: int = 1121450543462760448 moderation_roles: ClassVar[list[int]] = [Roles.moderators] @@ -171,10 +171,10 @@ class _Colours(EnvConfig, env_prefix="colours_"): gold: int = 0xE6C200 @root_validator(pre=True) - def parse_hex_values(cls: type[Self], values: dict[str, int]) -> dict[str, int]: # noqa: N805 - check this + def parse_hex_values(cls, values: dict[str, int]) -> dict[str, int]: # noqa: N805 - check this """Verify that colors are valid hex.""" for key, value in values.items(): - values[key] = int(value, 16) + values[key] = int(value, 16) # type: ignore[call-overload] return values diff --git a/src/bot/dragonfly_services.py b/src/bot/dragonfly_services.py index 509b267..908d24c 100644 --- a/src/bot/dragonfly_services.py +++ b/src/bot/dragonfly_services.py @@ -34,7 +34,7 @@ class PackageScanResult: score: int @classmethod - def from_dict(cls: type[Self], data: dict) -> Self: + def from_dict(cls: type[Self], data: dict) -> Self: # type: ignore[type-arg] """Create a PackageScanResult from a dictionary.""" return cls( status=ScanStatus(data["status"]), @@ -58,7 +58,7 @@ def __str__(self: Self) -> str: class DragonflyServices: """A class wrapping Dragonfly's API.""" - def __init__( # noqa: PLR0913 -- Maybe pass the entire constants class? + def __init__( # noqa: PLR0913,PLR0917 -- Maybe pass the entire constants class? self: Self, session: ClientSession, base_url: str, @@ -106,7 +106,7 @@ async def make_request( path: str, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, - ) -> dict: + ) -> dict: # type: ignore[type-arg] """Make a request to Dragonfly's API.""" await self._update_token() @@ -124,9 +124,9 @@ async def make_request( if json is not None: args["json"] = json - async with self.session.request(**args) as response: + async with self.session.request(**args) as response: # type: ignore[arg-type] response.raise_for_status() - return await response.json() + return await response.json() # type: ignore[no-any-return] async def get_scanned_packages( self: Self, @@ -143,7 +143,7 @@ async def get_scanned_packages( params["version"] = version if since: - params["since"] = int(since.timestamp()) + params["since"] = int(since.timestamp()) # type: ignore[assignment] data = await self.make_request("GET", "/package", params=params) return [PackageScanResult.from_dict(dct) for dct in data] diff --git a/src/bot/exts/audit.py b/src/bot/exts/audit.py index 3742684..39f83f9 100644 --- a/src/bot/exts/audit.py +++ b/src/bot/exts/audit.py @@ -34,7 +34,7 @@ def __init__( self.current = 0 @ui.button(emoji="◀️") - async def previous(self: Self, interaction: discord.Interaction, _) -> None: # noqa: ANN001 -- What is this? + async def previous(self: Self, interaction: discord.Interaction, _) -> None: # type: ignore[no-untyped-def, type-arg] # noqa: ANN001 -- What is this? """Go to the previous page.""" if self.current == 0: self.current = len(self.embeds) - 1 @@ -44,7 +44,7 @@ async def previous(self: Self, interaction: discord.Interaction, _) -> None: # await interaction.response.edit_message(embed=self.embeds[self.current], view=self) @ui.button(emoji="⏹️") - async def stop(self: Self, interaction: discord.Interaction, button: ui.Button) -> None: + async def stop(self: Self, interaction: discord.Interaction, button: ui.Button) -> None: # type: ignore[override, type-arg] """Stop the paginator.""" self.previous.disabled = True button.disabled = True @@ -53,7 +53,7 @@ async def stop(self: Self, interaction: discord.Interaction, button: ui.Button) await interaction.response.edit_message(embed=self.embeds[self.current], view=self) @ui.button(emoji="▶️") - async def next(self: Self, interaction: discord.Interaction, _) -> None: # noqa: ANN001,A003 + async def next(self: Self, interaction: discord.Interaction, _) -> None: # type: ignore[no-untyped-def, type-arg] # noqa: ANN001 """Go to the next page.""" if self.current == len(self.embeds) - 1: self.current = 0 @@ -62,7 +62,7 @@ async def next(self: Self, interaction: discord.Interaction, _) -> None: # noqa await interaction.response.edit_message(embed=self.embeds[self.current], view=self) - async def interaction_check(self: Self, interaction: discord.Interaction) -> bool: + async def interaction_check(self: Self, interaction: discord.Interaction) -> bool: # type: ignore[type-arg] """Check if the interaction is from the member.""" if interaction.user == self.member: return True @@ -97,8 +97,8 @@ def __init__( """Initialize the cog.""" self.bot = bot - @app_commands.command(name="audit", description="Randomly pick packages and display them") - async def audit(self: Self, interaction: discord.Interaction, hours: int, amount: int) -> None: + @app_commands.command(name="audit", description="Randomly pick packages and display them") # type: ignore[arg-type] + async def audit(self: Self, interaction: discord.Interaction, hours: int, amount: int) -> None: # type: ignore[type-arg] """ Recalls for scanned packages within a given time frame and amount. diff --git a/src/bot/exts/core/error_handler.py b/src/bot/exts/core/error_handler.py index e0a1203..1f22479 100644 --- a/src/bot/exts/core/error_handler.py +++ b/src/bot/exts/core/error_handler.py @@ -27,28 +27,28 @@ def __init__(self: Self, bot: Bot) -> None: self.bot = bot @staticmethod - def revert_cooldown_counter(command: commands.Command, message: Message) -> None: + def revert_cooldown_counter(command: commands.Command, message: Message) -> None: # type: ignore[type-arg] """Undoes the last cooldown counter for user-error cases.""" if command._buckets.valid: # noqa: SLF001 -- Underscored attribute - bucket = command._buckets.get_bucket(message) # noqa: SLF001 -- Underscored attribute - bucket._tokens = min(bucket.rate, bucket._tokens + 1) # noqa: SLF001 -- Underscored attribute + bucket = command._buckets.get_bucket(message) # type: ignore[arg-type] # noqa: SLF001 -- Underscored attribute + bucket._tokens = min(bucket.rate, bucket._tokens + 1) # type: ignore[union-attr] # noqa: SLF001 -- Underscored attribute logging.debug("Cooldown counter reverted as the command was not used correctly.") @staticmethod - def error_embed(message: str, title: Iterable | str = NEGATIVE_REPLIES) -> Embed: + def error_embed(message: str, title: Iterable | str = NEGATIVE_REPLIES) -> Embed: # type: ignore[type-arg] """Build a basic embed with red colour and either a random error title or a title provided.""" embed = Embed(colour=Colours.soft_red) if isinstance(title, str): embed.title = title else: - embed.title = random.choice(title) # noqa: S311 -- wat + embed.title = random.choice(title) # type: ignore[arg-type] # noqa: S311 -- wat embed.description = message return embed @commands.Cog.listener() async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? self: Self, - ctx: commands.Context, + ctx: commands.Context, # type: ignore[type-arg] error: commands.CommandError, ) -> None: """Activates when a command raises an error.""" @@ -70,12 +70,12 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? ) if isinstance(error, commands.CommandNotFound): - await self.send_command_suggestion(ctx, ctx.invoked_with) + await self.send_command_suggestion(ctx, ctx.invoked_with) # type: ignore[arg-type] return if isinstance(error, commands.UserInputError): - self.revert_cooldown_counter(ctx.command, ctx.message) - usage = f"```\n{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}\n```" + self.revert_cooldown_counter(ctx.command, ctx.message) # type: ignore[arg-type] + usage = f"```\n{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}\n```" # type: ignore[union-attr] embed = self.error_embed(f"Your input was invalid: {error}\n\nUsage:{usage}") await ctx.send(embed=embed) return @@ -98,9 +98,9 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? return if isinstance(error, commands.BadArgument): - self.revert_cooldown_counter(ctx.command, ctx.message) + self.revert_cooldown_counter(ctx.command, ctx.message) # type: ignore[arg-type] embed = self.error_embed( - "The argument you provided was invalid: " + "The argument you provided was invalid: " # type: ignore[union-attr] f"{error}\n\nUsage:\n```\n{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}\n```", ) await ctx.send(embed=embed) @@ -113,7 +113,7 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? if isinstance(error, APIError): await ctx.send( embed=self.error_embed( - f"There was an error when communicating with the {error.api}", + f"There was an error when communicating with the {error.api}", # type: ignore[attr-defined] NEGATIVE_REPLIES, ), ) @@ -121,7 +121,7 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? if isinstance(error, MovedCommandError): description = ( - f"This command, `{ctx.prefix}{ctx.command.qualified_name}` has moved to `{error.new_command_name}`.\n" + f"This command, `{ctx.prefix}{ctx.command.qualified_name}` has moved to `{error.new_command_name}`.\n" # type: ignore[attr-defined, union-attr] f"Please use `{error.new_command_name}` instead." ) await ctx.send(embed=self.error_embed(description, NEGATIVE_REPLIES)) @@ -130,7 +130,7 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? with push_scope() as scope: scope.user = {"id": ctx.author.id, "username": str(ctx.author)} - scope.set_tag("command", ctx.command.qualified_name) + scope.set_tag("command", ctx.command.qualified_name) # type: ignore[union-attr] scope.set_tag("message_id", ctx.message.id) scope.set_tag("channel_id", ctx.channel.id) @@ -141,7 +141,7 @@ async def on_command_error( # noqa: C901,PLR0911 -- Probably refactor this? log.exception(f"Unhandled command error: {error!s}", exc_info=error) - async def send_command_suggestion(self: Self, ctx: commands.Context, command_name: str) -> None: + async def send_command_suggestion(self: Self, ctx: commands.Context, command_name: str) -> None: # type: ignore[type-arg] """Send user similar commands if any can be found.""" command_suggestions = [] if similar_command_names := get_command_suggestions(list(self.bot.all_commands.keys()), command_name): diff --git a/src/bot/exts/core/log.py b/src/bot/exts/core/log.py index bac191d..d9922c9 100644 --- a/src/bot/exts/core/log.py +++ b/src/bot/exts/core/log.py @@ -19,7 +19,7 @@ class Log(Cog): def __init__(self: Self, bot: Bot) -> None: self.bot = bot - async def send_log_message( # noqa: PLR0913 -- Maybe refactor this? + async def send_log_message( # noqa: PLR0913,PLR0917 -- Maybe refactor this? self: Self, icon_url: str | None, colour: discord.Colour | int, @@ -33,7 +33,7 @@ async def send_log_message( # noqa: PLR0913 -- Maybe refactor this? additional_embeds: list[discord.Embed] | None = None, timestamp_override: datetime | None = None, footer: str | None = None, - ) -> Context: + ) -> Context: # type: ignore[type-arg] """Generate log embed and send to logging channel.""" # Truncate string directly here to avoid removing newlines embed = discord.Embed(description=text[:4093] + "..." if len(text) > 4096 else text) # noqa: PLR2004 @@ -41,7 +41,7 @@ async def send_log_message( # noqa: PLR0913 -- Maybe refactor this? if title and icon_url: embed.set_author(name=title, icon_url=icon_url) - embed.colour = colour + embed.colour = colour # type: ignore[assignment] embed.timestamp = timestamp_override or datetime.now(tz=UTC) if footer: @@ -64,7 +64,7 @@ async def send_log_message( # noqa: PLR0913 -- Maybe refactor this? for additional_embed in additional_embeds: await channel.send(embed=additional_embed) - return await self.bot.get_context(log_message) # Optionally return for use with antispam + return await self.bot.get_context(log_message) # type: ignore[no-any-return] # Optionally return for use with antispam async def setup(bot: Bot) -> None: diff --git a/src/bot/exts/core/ping.py b/src/bot/exts/core/ping.py index fb291c3..fb84e9d 100644 --- a/src/bot/exts/core/ping.py +++ b/src/bot/exts/core/ping.py @@ -16,7 +16,7 @@ def __init__(self: Self, bot: Bot) -> None: self.bot = bot @commands.command(name="ping") - async def ping(self: Self, ctx: commands.Context) -> None: + async def ping(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] """Ping the bot to see its latency and state.""" embed = Embed( title=":ping_pong: Pong!", diff --git a/src/bot/exts/core/sync.py b/src/bot/exts/core/sync.py index 06edb05..b615f02 100644 --- a/src/bot/exts/core/sync.py +++ b/src/bot/exts/core/sync.py @@ -35,11 +35,11 @@ async def _sync_commands(self: Self) -> list[AppCommand]: ", ".join(command.name for command in synced_commands), ) - return synced_commands + return synced_commands # type: ignore[no-any-return] @commands.command(name="sync") @commands.has_permissions(administrator=True) - async def sync_prefix(self: Self, ctx: commands.Context) -> None: + async def sync_prefix(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] """Prefix command that syncs all application commands.""" synced_commands = await self._sync_commands() @@ -47,9 +47,9 @@ async def sync_prefix(self: Self, ctx: commands.Context) -> None: f"Synced {len(synced_commands)} commands: {', '.join(command.name for command in synced_commands)}", ) - @discord.app_commands.command(name="sync", description="Sync all application commands") + @discord.app_commands.command(name="sync", description="Sync all application commands") # type: ignore[arg-type] @discord.app_commands.checks.has_permissions(administrator=True) - async def sync_slash(self: Self, interaction: discord.Interaction) -> None: + async def sync_slash(self: Self, interaction: discord.Interaction) -> None: # type: ignore[type-arg] """Slash command that syncs all application commands.""" synced_commands = await self._sync_commands() diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 4187dc7..250365e 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -43,7 +43,7 @@ def _build_package_report_log_embed( class ConfirmReportModal(discord.ui.Modal): """Modal for confirming a report.""" - recipient = discord.ui.TextInput( + recipient = discord.ui.TextInput( # type: ignore[var-annotated] label="Recipient", placeholder="Recipient's Email Address", required=False, @@ -51,14 +51,14 @@ class ConfirmReportModal(discord.ui.Modal): style=discord.TextStyle.short, ) - additional_information = discord.ui.TextInput( + additional_information = discord.ui.TextInput( # type: ignore[var-annotated] label="Additional information", placeholder="Additional information", required=False, style=discord.TextStyle.long, ) - inspector_url = discord.ui.TextInput( + inspector_url = discord.ui.TextInput( # type: ignore[var-annotated] label="Inspector URL", placeholder="Inspector URL", required=False, @@ -76,7 +76,7 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: super().__init__() - async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: + async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: # type: ignore[override, type-arg] """Handle errors that occur in the modal.""" if isinstance(error, aiohttp.ClientResponseError): return await interaction.response.send_message(f"Error from upstream: {error.status}", ephemeral=True) @@ -92,7 +92,7 @@ def _build_modal_title(self: Self) -> str: return title - async def on_submit(self: Self, interaction: discord.Interaction) -> None: + async def on_submit(self: Self, interaction: discord.Interaction) -> None: # type: ignore[type-arg] """Submit the report.""" # discord.py returns empty string "" if not filled out, we want it to be `None` additional_information_override = self.additional_information.value or None @@ -141,7 +141,7 @@ def __init__(self: Self, bot: Bot, payload: PackageScanResult) -> None: super().__init__(timeout=None) @discord.ui.button(label="Report", style=discord.ButtonStyle.red) - async def report(self: Self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + async def report(self: Self, interaction: discord.Interaction, button: discord.ui.Button) -> None: # type: ignore[type-arg] """Report a package.""" modal = ConfirmReportModal(package=self.payload, bot=self.bot) await interaction.response.send_modal(modal) @@ -161,13 +161,13 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. ) embed.add_field( - name="\u200B", + name="\u200b", value=f"[Inspector]({scan_result.inspector_url})", inline=True, ) embed.add_field( - name="\u200B", + name="\u200b", value=f"[PyPI](https://pypi.org/project/{scan_result.name}/{scan_result.version})", inline=True, ) @@ -245,7 +245,7 @@ async def before_scan_loop(self: Self) -> None: @commands.has_role(Roles.vipyr_security) @commands.command() - async def start(self: Self, ctx: commands.Context) -> None: + async def start(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] """Start the scan task.""" if self.scan_loop.is_running(): await ctx.send("Task is already running") @@ -255,7 +255,7 @@ async def start(self: Self, ctx: commands.Context) -> None: @commands.has_role(Roles.vipyr_security) @commands.command() - async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: # noqa: FBT001,FBT002 + async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: # type: ignore[type-arg] # noqa: FBT001,FBT002 """Stop the scan task.""" if self.scan_loop.is_running(): if force: @@ -267,9 +267,9 @@ async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: else: await ctx.send("Task is not running") - @discord.app_commands.checks.has_role(Roles.vipyr_security) + @discord.app_commands.checks.has_role(Roles.vipyr_security) # type: ignore[arg-type] @discord.app_commands.command(name="lookup", description="Scans a package") - async def lookup(self: Self, interaction: discord.Interaction, name: str, version: str | None = None) -> None: + async def lookup(self: Self, interaction: discord.Interaction, name: str, version: str | None = None) -> None: # type: ignore[type-arg] """Pull the scan results for a package.""" scan_results = await self.bot.dragonfly_services.get_scanned_packages(name=name, version=version) if scan_results: @@ -279,18 +279,18 @@ async def lookup(self: Self, interaction: discord.Interaction, name: str, versio await interaction.response.send_message("No entries were found with the specified filters.") @commands.group() - async def threshold(self: Self, ctx: commands.Context) -> None: + async def threshold(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] """Group of commands for managing the score threshold.""" if ctx.invoked_subcommand is None: await ctx.send_help(self.threshold) - @threshold.command() - async def get(self: Self, ctx: commands.Context) -> None: + @threshold.command() # type: ignore[arg-type] + async def get(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] """Get the score threshold.""" await ctx.send(f"The current threshold is set to `{self.score_threshold}`") - @threshold.command() - async def set(self: Self, ctx: commands.Context, value: int) -> None: # noqa: A003 + @threshold.command() # type: ignore[arg-type] + async def set(self: Self, ctx: commands.Context, value: int) -> None: # type: ignore[type-arg] """Set the score threshold.""" self.score_threshold = value await ctx.send(f"The current threshold has been set to `{value}`") diff --git a/src/bot/exts/utilities/internal.py b/src/bot/exts/utilities/internal.py index ac45393..1797771 100644 --- a/src/bot/exts/utilities/internal.py +++ b/src/bot/exts/utilities/internal.py @@ -32,13 +32,13 @@ class Internal(Cog): def __init__(self: Self, bot: Bot) -> None: self.bot = bot - self.env = {} + self.env = {} # type: ignore[var-annotated] self.ln = 0 self.stdout = StringIO() self.socket_since = arrow.utcnow() self.socket_event_total = 0 - self.socket_events = Counter() + self.socket_events = Counter() # type: ignore[var-annotated] if DEBUG_MODE: self.eval.add_check(is_owner().predicate) @@ -112,7 +112,7 @@ def _format(self: Self, inp: str, out: Any) -> tuple[str, discord.Embed | None]: if isinstance(out, discord.Embed): # We made an embed? Send that as embed res += "" - res = (res, out) + res = (res, out) # type: ignore[assignment] else: if isinstance(out, str) and out.startswith("Traceback (most recent call last):\n"): @@ -137,11 +137,11 @@ def _format(self: Self, inp: str, out: Any) -> tuple[str, discord.Embed | None]: # Add the output res += pretty - res = (res, None) + res = (res, None) # type: ignore[assignment] - return res # Return (text, embed) + return res # type: ignore[return-value] # Return (text, embed) - async def _eval(self: Self, ctx: Context, code: str) -> discord.Message | None: + async def _eval(self: Self, ctx: Context, code: str) -> discord.Message | None: # type: ignore[type-arg] """Eval the input code string & send an embed to the invoking context.""" self.ln += 1 @@ -210,22 +210,22 @@ async def func(): # (None,) -> Any else: paste_text = f"full contents at {paste_link}" - await ctx.send(f"```py\n{out[:truncate_index]}\n```... response truncated; {paste_text}", embed=embed) + await ctx.send(f"```py\n{out[:truncate_index]}\n```... response truncated; {paste_text}", embed=embed) # type: ignore[arg-type] return None - await ctx.send(f"```py\n{out}```", embed=embed) + await ctx.send(f"```py\n{out}```", embed=embed) # type: ignore[arg-type] return None @group(name="internal", aliases=("int",)) @has_any_role(Roles.administrators, Roles.core_developers) - async def internal_group(self: Self, ctx: Context) -> None: + async def internal_group(self: Self, ctx: Context) -> None: # type: ignore[type-arg] """Internal commands. Top secret!.""" # noqa: D401 if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @internal_group.command(name="eval", aliases=("e",)) + @internal_group.command(name="eval", aliases=("e",)) # type: ignore[arg-type] @has_any_role(Roles.administrators) - async def eval(self: Self, ctx: Context, *, code: str) -> None: # noqa: A003 + async def eval(self: Self, ctx: Context, *, code: str) -> None: # type: ignore[type-arg] """Run eval in a REPL-like format.""" code = code.strip("`") if re.match("py(thon)?\n", code): @@ -235,7 +235,7 @@ async def eval(self: Self, ctx: Context, *, code: str) -> None: # noqa: A003 not re.search( # Check if it's an expression r"^(return|import|for|while|def|class|from|exit|[a-zA-Z0-9]+\s*=)", code, - re.M, + re.MULTILINE, ) and len(code.split("\n")) == 1 ): @@ -243,9 +243,9 @@ async def eval(self: Self, ctx: Context, *, code: str) -> None: # noqa: A003 await self._eval(ctx, code) - @internal_group.command(name="socketstats", aliases=("socket", "stats")) + @internal_group.command(name="socketstats", aliases=("socket", "stats")) # type: ignore[arg-type] @has_any_role(Roles.administrators, Roles.core_developers) - async def socketstats(self: Self, ctx: Context) -> None: + async def socketstats(self: Self, ctx: Context) -> None: # type: ignore[type-arg] """Fetch information on the socket events received from Discord.""" running_s = (arrow.utcnow() - self.socket_since).total_seconds() diff --git a/src/bot/log.py b/src/bot/log.py index ecd0509..430faaf 100644 --- a/src/bot/log.py +++ b/src/bot/log.py @@ -19,10 +19,10 @@ LoggerClass = Logger if TYPE_CHECKING else logging.getLoggerClass() -class CustomLogger(LoggerClass): +class CustomLogger(LoggerClass): # type: ignore[misc, valid-type] """Custom implementation of the `Logger` class with an added `trace` method.""" - def trace(self: Self, msg: str, *args: tuple, **kwargs: dict) -> None: + def trace(self: Self, msg: str, *args: tuple, **kwargs: dict) -> None: # type: ignore[type-arg] """ Log 'msg % args' with severity 'TRACE'. @@ -42,7 +42,7 @@ def get_logger(name: str | None = None) -> CustomLogger: def setup() -> None: """Set up loggers.""" - logging.TRACE = TRACE_LEVEL + logging.TRACE = TRACE_LEVEL # type: ignore[attr-defined] logging.addLevelName(TRACE_LEVEL, "TRACE") logging.setLoggerClass(CustomLogger) diff --git a/src/bot/utils/__init__.py b/src/bot/utils/__init__.py index 9bba78c..e60a4b0 100644 --- a/src/bot/utils/__init__.py +++ b/src/bot/utils/__init__.py @@ -9,10 +9,10 @@ __all__ = [ "CogABCMeta", + "PasteTooLongError", + "PasteUploadError", "find_nth_occurrence", "has_lines", "pad_base64", "send_to_paste_service", - "PasteUploadError", - "PasteTooLongError", ] diff --git a/src/bot/utils/function.py b/src/bot/utils/function.py index ce996a5..3a189b7 100644 --- a/src/bot/utils/function.py +++ b/src/bot/utils/function.py @@ -13,7 +13,7 @@ Argument = int | str BoundArgs = OrderedDict[str, Any] -Decorator = Callable[[Callable], Callable] +Decorator = Callable[[Callable], Callable] # type: ignore[type-arg] ArgValGetter = Callable[[BoundArgs], Any] @@ -36,7 +36,7 @@ def get_arg_value(name_or_pos: Argument, arguments: BoundArgs) -> Any: # noqa: arg_pos = name_or_pos try: - name, value = arg_values[arg_pos] + _name, value = arg_values[arg_pos] except IndexError as exception: msg = f"Argument position {arg_pos} is out of bounds." raise ValueError(msg) from exception @@ -79,7 +79,7 @@ def wrapper(args: BoundArgs) -> Any: # noqa: ANN401 return decorator_func(wrapper) -def get_bound_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> BoundArgs: +def get_bound_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> BoundArgs: # type: ignore[type-arg] """ Bind `args` and `kwargs` to `func` and return a mapping of parameter names to argument values. @@ -96,7 +96,7 @@ def update_wrapper_globals( wrapper: types.FunctionType, wrapped: types.FunctionType, *, - ignored_conflict_names: set[str] = frozenset(), + ignored_conflict_names: set[str] = frozenset(), # type: ignore[assignment] ) -> types.FunctionType: """ Update globals of `wrapper` with the globals from `wrapped`. @@ -141,7 +141,7 @@ def command_wraps( assigned: Sequence[str] = functools.WRAPPER_ASSIGNMENTS, updated: Sequence[str] = functools.WRAPPER_UPDATES, *, - ignored_conflict_names: set[str] = frozenset(), + ignored_conflict_names: set[str] = frozenset(), # type: ignore[assignment] ) -> Callable[[types.FunctionType], types.FunctionType]: """Update the decorated function to look like `wrapped` and update globals for discordpy forwardref evaluation.""" diff --git a/src/bot/utils/helpers.py b/src/bot/utils/helpers.py index a9f78de..0d12bc3 100644 --- a/src/bot/utils/helpers.py +++ b/src/bot/utils/helpers.py @@ -28,7 +28,7 @@ def has_lines(string: str, count: int) -> bool: split = string.split("\n", count - 1) # Make sure the last part isn't empty, which would happen if there was a final newline. - return split[-1] and len(split) == count + return split[-1] and len(split) == count # type: ignore[return-value] def pad_base64(data: str) -> str: diff --git a/src/bot/utils/lock.py b/src/bot/utils/lock.py index 6eb0420..62585cf 100644 --- a/src/bot/utils/lock.py +++ b/src/bot/utils/lock.py @@ -16,7 +16,7 @@ from .exceptions import LockedResourceError log = get_logger(__name__) -__lock_dicts = defaultdict(WeakValueDictionary) +__lock_dicts = defaultdict(WeakValueDictionary) # type: ignore[var-annotated] _IdCallableReturn = Hashable | Awaitable[Hashable] _IdCallable = Callable[[function.BoundArgs], _IdCallableReturn] @@ -41,7 +41,7 @@ def __enter__(self: Self) -> None: self._active_count += 1 self._event.clear() - def __exit__(self: Self, _exc_type, _exc_val, _exc_tb) -> None: # noqa: ANN001 + def __exit__(self: Self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 """Decrement the count of the active holders; if 0 is reached set the internal event.""" self._active_count -= 1 if not self._active_count: @@ -58,7 +58,7 @@ def lock( *, raise_error: bool = False, wait: bool = False, -) -> Callable: +) -> Callable: # type: ignore[type-arg] """ Turn the decorated coroutine function into a mutually exclusive operation on a `resource_id`. @@ -80,8 +80,8 @@ def lock( def decorator(func: types.FunctionType) -> types.FunctionType: name = func.__name__ - @command_wraps(func) - async def wrapper(*args: tuple, **kwargs: dict) -> Any: # noqa: ANN401 -- matches signature of upstream + @command_wraps(func) # type: ignore[arg-type] + async def wrapper(*args: tuple, **kwargs: dict) -> Any: # type: ignore[type-arg] # noqa: ANN401 -- matches signature of upstream log.trace(f"{name}: mutually exclusive decorator called") if callable(resource_id): @@ -114,7 +114,7 @@ async def wrapper(*args: tuple, **kwargs: dict) -> Any: # noqa: ANN401 -- match else: log.info(f"{name}: aborted because resource {namespace!r}:{id_!r} is locked") if raise_error: - raise LockedResourceError(str(namespace), id_) + raise LockedResourceError(str(namespace), id_) # type: ignore[arg-type] return None return wrapper @@ -129,7 +129,7 @@ def lock_arg( *, raise_error: bool = False, wait: bool = False, -) -> Callable: +) -> Callable: # type: ignore[type-arg] """ Apply the `lock` decorator using the value of the arg at the given name/position as the ID. diff --git a/src/bot/utils/messages.py b/src/bot/utils/messages.py index 067141e..5c815a9 100644 --- a/src/bot/utils/messages.py +++ b/src/bot/utils/messages.py @@ -13,7 +13,7 @@ def format_user(user: discord.abc.User) -> str: return f"{user.mention} (`{user.id}`)" -async def get_discord_message(ctx: Context, text: str) -> Message | str: +async def get_discord_message(ctx: Context, text: str) -> Message | str: # type: ignore[return, type-arg] """ Attempt to convert a given `text` to a discord Message object and return it. @@ -24,7 +24,7 @@ async def get_discord_message(ctx: Context, text: str) -> Message | str: return await MessageConverter().convert(ctx, text) -async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Embed | None]: +async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Embed | None]: # type: ignore[type-arg] """ Attempt to extract the text and embed from a possible link to a discord Message. @@ -40,7 +40,7 @@ async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Embed | None msg = await get_discord_message(ctx, text) # Ensure the user has read permissions for the channel the message is in if isinstance(msg, Message): - permissions = msg.channel.permissions_for(ctx.author) + permissions = msg.channel.permissions_for(ctx.author) # type: ignore[arg-type] if permissions.read_messages: text = msg.clean_content # Take first embed because we can't send multiple embeds From 930bd4b0134c4ab882212b2f83398eb82548e35a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:04:08 -0600 Subject: [PATCH 03/37] Bump the python-dependencies group with 1 update (#213) Bumps the python-dependencies group with 1 update: [psycopg[binary]](https://github.com/psycopg/psycopg). Updates `psycopg[binary]` from 3.1.17 to 3.1.18 - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.1.17...3.1.18) --- updated-dependencies: - dependency-name: psycopg[binary] dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6a8e8d5..b209e1a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -51,9 +51,11 @@ multidict==6.0.5 # via # aiohttp # yarl -psycopg[binary]==3.1.17 - # via -r requirements/requirements.in -psycopg-binary==3.1.17 +psycopg[binary]==3.1.18 + # via + # -r requirements/requirements.in + # psycopg +psycopg-binary==3.1.18 # via psycopg pycares==4.4.0 # via aiodns From ecb72f58b62d7c871b9c6e22a2f7a8cc5d5e0aec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:20:40 +0000 Subject: [PATCH 04/37] Bump ghcr.io/devcontainers/features/powershell from 1.2.0 to 1.3.2 Bumps ghcr.io/devcontainers/features/powershell from 1.2.0 to 1.3.2. --- updated-dependencies: - dependency-name: ghcr.io/devcontainers/features/powershell dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .devcontainer/devcontainer-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index a0f61ec..1a3dc07 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -1,9 +1,9 @@ { "features": { "ghcr.io/devcontainers/features/powershell:1": { - "version": "1.2.0", - "resolved": "ghcr.io/devcontainers/features/powershell@sha256:3b8a159d67a68419cbc13f09413fc3523c1a8f13d64bfb3aa7119df4fd324d0e", - "integrity": "sha256:3b8a159d67a68419cbc13f09413fc3523c1a8f13d64bfb3aa7119df4fd324d0e" + "version": "1.3.2", + "resolved": "ghcr.io/devcontainers/features/powershell@sha256:c11122f0fc8352fcf3a1c2eab1023daab9db7982a83725af803307fd18fb64f4", + "integrity": "sha256:c11122f0fc8352fcf3a1c2eab1023daab9db7982a83725af803307fd18fb64f4" }, "ghcr.io/devcontainers/features/python:1": { "version": "1.3.2", @@ -11,4 +11,4 @@ "integrity": "sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006" } } -} +} \ No newline at end of file From d5af6b3714c761c447f106e573c19650a7351f3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:19:51 -0600 Subject: [PATCH 05/37] Bump the ci-dependencies group with 1 update (#219) Bumps the ci-dependencies group with 1 update: [actions/dependency-review-action](https://github.com/actions/dependency-review-action). Updates `actions/dependency-review-action` from 4.0.0 to 4.1.3 - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/4901385134134e04cec5fbe5ddfe3b2c5bd5d976...9129d7d40b8c12c1ed0f60400d00c92d437adcce) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ci-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 22f739e..4d6bda1 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -15,6 +15,6 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: "Dependency Review" - uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 + uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 with: config-file: darbiadev/.github/.github/dependency-review-config.yaml@f185cc076161b47921c6fb6da4c1fd5e40b50bff # v3.0.0 From 7818d458fabf365fa0ad4377165f5edd69251453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:20:03 -0600 Subject: [PATCH 06/37] Bump the python-dependencies group with 7 updates (#216) Bumps the python-dependencies group with 7 updates: | Package | From | To | | --- | --- | --- | | [pip-tools](https://github.com/jazzband/pip-tools) | `7.3.0` | `7.4.0` | | [pre-commit](https://github.com/pre-commit/pre-commit) | `3.6.0` | `3.6.2` | | [ruff](https://github.com/astral-sh/ruff) | `0.2.0` | `0.3.0` | | [pydantic-settings](https://github.com/pydantic/pydantic-settings) | `2.1.0` | `2.2.1` | | [sentry-sdk](https://github.com/getsentry/sentry-python) | `1.40.0` | `1.40.6` | | [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) | `2.0.25` | `2.0.27` | | [pytest](https://github.com/pytest-dev/pytest) | `8.0.0` | `8.0.2` | Updates `pip-tools` from 7.3.0 to 7.4.0 - [Release notes](https://github.com/jazzband/pip-tools/releases) - [Changelog](https://github.com/jazzband/pip-tools/blob/main/CHANGELOG.md) - [Commits](https://github.com/jazzband/pip-tools/compare/7.3.0...7.4.0) Updates `pre-commit` from 3.6.0 to 3.6.2 - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v3.6.2) Updates `ruff` from 0.2.0 to 0.3.0 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.2.0...v0.3.0) Updates `pydantic-settings` from 2.1.0 to 2.2.1 - [Release notes](https://github.com/pydantic/pydantic-settings/releases) - [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.1.0...v2.2.1) Updates `sentry-sdk` from 1.40.0 to 1.40.6 - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/1.40.0...1.40.6) Updates `sqlalchemy` from 2.0.25 to 2.0.27 - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Updates `pytest` from 8.0.0 to 8.0.2 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0...8.0.2) --- updated-dependencies: - dependency-name: pip-tools dependency-type: direct:development update-type: version-update:semver-minor dependency-group: python-dependencies - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-dependencies - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-minor dependency-group: python-dependencies - dependency-name: pydantic-settings dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-dependencies - dependency-name: sentry-sdk dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-dependencies - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements-dev.txt | 10 ++++++---- requirements/requirements-tests.txt | 2 +- requirements/requirements.txt | 10 ++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 9fca7b7..c596e92 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -26,17 +26,19 @@ nodeenv==1.8.0 # via pre-commit packaging==23.2 # via build -pip-tools==7.3.0 +pip-tools==7.4.0 # via -r requirements/requirements-dev.in platformdirs==4.2.0 # via virtualenv -pre-commit==3.6.0 +pre-commit==3.6.2 # via -r requirements/requirements-dev.in pyproject-hooks==1.0.0 - # via build + # via + # build + # pip-tools pyyaml==6.0.1 # via pre-commit -ruff==0.2.0 +ruff==0.3.0 # via -r requirements/requirements-dev.in typing-extensions==4.9.0 # via diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 345badb..a972890 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -10,7 +10,7 @@ packaging==23.2 # via pytest pluggy==1.4.0 # via pytest -pytest==8.0.0 +pytest==8.0.2 # via # -r requirements/requirements-tests.in # pytest-randomly diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b209e1a..84b6769 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -52,9 +52,7 @@ multidict==6.0.5 # aiohttp # yarl psycopg[binary]==3.1.18 - # via - # -r requirements/requirements.in - # psycopg + # via -r requirements/requirements.in psycopg-binary==3.1.18 # via psycopg pycares==4.4.0 @@ -67,7 +65,7 @@ pydantic==2.6.0 # pydis-core pydantic-core==2.16.1 # via pydantic -pydantic-settings==2.1.0 +pydantic-settings==2.2.1 # via -r requirements/requirements.in pydis-core==10.7.0 # via -r requirements/requirements.in @@ -85,11 +83,11 @@ requests==2.31.0 # tldextract requests-file==2.0.0 # via tldextract -sentry-sdk==1.40.0 +sentry-sdk==1.40.6 # via -r requirements/requirements.in six==1.16.0 # via python-dateutil -sqlalchemy==2.0.25 +sqlalchemy==2.0.27 # via -r requirements/requirements.in statsd==4.0.1 # via pydis-core From 85d43e738e165c1357fd61af0f87339ff7ee413f Mon Sep 17 00:00:00 2001 From: Xithrius Date: Sat, 2 Mar 2024 23:57:11 -0800 Subject: [PATCH 07/37] Appeased the formatter --- .devcontainer/devcontainer-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 1a3dc07..da28fbb 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -11,4 +11,4 @@ "integrity": "sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006" } } -} \ No newline at end of file +} From 5e316bab4be5e511e310fdcea82507da9d10eb69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 08:52:11 -0600 Subject: [PATCH 08/37] Bump ghcr.io/devcontainers/features/python from 1.3.2 to 1.4.1 (#218) * Bump ghcr.io/devcontainers/features/python from 1.3.2 to 1.4.1 Bumps ghcr.io/devcontainers/features/python from 1.3.2 to 1.4.1. --- updated-dependencies: - dependency-name: ghcr.io/devcontainers/features/python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Appeased the formatter --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xithrius Co-authored-by: Robin <74519799+Robin5605@users.noreply.github.com> --- .devcontainer/devcontainer-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index da28fbb..2a15b3e 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -6,9 +6,9 @@ "integrity": "sha256:c11122f0fc8352fcf3a1c2eab1023daab9db7982a83725af803307fd18fb64f4" }, "ghcr.io/devcontainers/features/python:1": { - "version": "1.3.2", - "resolved": "ghcr.io/devcontainers/features/python@sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006", - "integrity": "sha256:585d4d8ad574891e2ffa2b1a5823a363fc1562121bdedea1c441daf3560f7006" + "version": "1.4.1", + "resolved": "ghcr.io/devcontainers/features/python@sha256:d7e393af2440444dddb3c275cf7f90c899a24f8e853e4d6315e1be3be7e1d49f", + "integrity": "sha256:d7e393af2440444dddb3c275cf7f90c899a24f8e853e4d6315e1be3be7e1d49f" } } } From ba0aad8053ab7bace3cf7a2380c549485501fedb Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Thu, 7 Mar 2024 17:44:28 -0600 Subject: [PATCH 09/37] Add reporter URL configuration option --- src/bot/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bot/constants.py b/src/bot/constants.py index 4313447..44312d5 100644 --- a/src/bot/constants.py +++ b/src/bot/constants.py @@ -63,6 +63,8 @@ class _DragonflyConfig(EnvConfig, env_prefix="dragonfly_"): threshold: int = 8 timeout: int = 25 + reporter_url: str = "" + DragonflyConfig = _DragonflyConfig() From 8c93c827a412efb604d4363333bfc584e27b785a Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Thu, 7 Mar 2024 19:10:14 -0600 Subject: [PATCH 10/37] Add get username command --- src/bot/exts/dragonfly/dragonfly.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 250365e..b8533db 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -215,6 +215,15 @@ def __init__(self: Self, bot: Bot) -> None: self.since = datetime.now(tz=UTC) - timedelta(seconds=DragonflyConfig.interval) super().__init__() + @commands.hybrid_command(name="username") + async def get_username_command(self, ctx: commands.Context) -> None: + """Get the username of the currently logged in user to the PyPI Observation API.""" + async with self.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: + json = await res.json() + username = json["username"] + + await ctx.send(username) + @tasks.loop(seconds=DragonflyConfig.interval) async def scan_loop(self: Self) -> None: """Loop that runs the scan task.""" From 8713042d0483333c1d63c8cb1db12276ea129f2f Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Thu, 7 Mar 2024 19:24:19 -0600 Subject: [PATCH 11/37] Ignore type error --- src/bot/exts/dragonfly/dragonfly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index b8533db..fddf7a7 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -215,7 +215,7 @@ def __init__(self: Self, bot: Bot) -> None: self.since = datetime.now(tz=UTC) - timedelta(seconds=DragonflyConfig.interval) super().__init__() - @commands.hybrid_command(name="username") + @commands.hybrid_command(name="username") # type: ignore [arg-type] async def get_username_command(self, ctx: commands.Context) -> None: """Get the username of the currently logged in user to the PyPI Observation API.""" async with self.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: From 0a89003deef1e21ebb0457d6f1b47f32c83f6967 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Thu, 7 Mar 2024 19:28:05 -0600 Subject: [PATCH 12/37] Add generic for `commands.Context` --- src/bot/exts/dragonfly/dragonfly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index fddf7a7..3395969 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -216,9 +216,9 @@ def __init__(self: Self, bot: Bot) -> None: super().__init__() @commands.hybrid_command(name="username") # type: ignore [arg-type] - async def get_username_command(self, ctx: commands.Context) -> None: + async def get_username_command(self, ctx: commands.Context[Bot]) -> None: """Get the username of the currently logged in user to the PyPI Observation API.""" - async with self.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: + async with ctx.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: json = await res.json() username = json["username"] From 381b16ea2be9d8cdfe565e35b09dc2ce6fe3d2c3 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 10 Mar 2024 15:41:20 -0500 Subject: [PATCH 13/37] Make DragonflyServices an argument to bot By making the DragonflyServices an argument to the bot, we get all the nice typehinting which we normally don't do by simply setting it as an instance attribute. --- src/bot/__main__.py | 19 ++++++++++--------- src/bot/bot.py | 3 +++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/bot/__main__.py b/src/bot/__main__.py index 19c66b4..e913728 100644 --- a/src/bot/__main__.py +++ b/src/bot/__main__.py @@ -28,15 +28,7 @@ def get_prefix(bot_: Bot, message_: discord.Message) -> Callable[[Bot, discord.M async def main() -> None: """Run the bot.""" async with ClientSession(headers={"Content-Type": "application/json"}, timeout=ClientTimeout(total=10)) as session: - bot = Bot( - guild_id=constants.Guild.id, - http_session=session, - allowed_roles=list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}), - command_prefix=get_prefix, - intents=intents, - ) - - bot.dragonfly_services = DragonflyServices( + dragonfly_services = DragonflyServices( session=session, base_url=constants.Dragonfly.base_url, auth_url=constants.Dragonfly.auth_url, @@ -47,6 +39,15 @@ async def main() -> None: password=constants.Dragonfly.password, ) + bot = Bot( + guild_id=constants.Guild.id, + http_session=session, + allowed_roles=list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}), + command_prefix=get_prefix, + intents=intents, + dragonfly_services=dragonfly_services, + ) + await bot.start(constants.Bot.token) diff --git a/src/bot/bot.py b/src/bot/bot.py index f7c585e..7df5902 100644 --- a/src/bot/bot.py +++ b/src/bot/bot.py @@ -10,6 +10,7 @@ from sentry_sdk import push_scope from bot import exts +from bot.dragonfly_services import DragonflyServices log = logging.getLogger(__name__) @@ -55,6 +56,7 @@ class Bot(BotBase): def __init__( self: Self, + dragonfly_services: DragonflyServices, *args: tuple, **kwargs: dict, ) -> None: @@ -71,6 +73,7 @@ def __init__( **kwargs, ) + self.dragonfly_services = dragonfly_services self.all_extensions: frozenset[str] | None = None async def setup_hook(self: Self) -> None: From f61c71ba1c73785ba31c0c112fffdc97d344157e Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 10 Mar 2024 18:47:49 -0500 Subject: [PATCH 14/37] Add use_email field in report function --- src/bot/dragonfly_services.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bot/dragonfly_services.py b/src/bot/dragonfly_services.py index 480f838..2d734d0 100644 --- a/src/bot/dragonfly_services.py +++ b/src/bot/dragonfly_services.py @@ -153,6 +153,7 @@ async def report_package( # noqa: PLR0913 inspector_url: str | None, additional_information: str | None, recipient: str | None, + use_email: bool = False, ) -> None: """Report a package to Dragonfly.""" data = { @@ -161,5 +162,6 @@ async def report_package( # noqa: PLR0913 "inspector_url": inspector_url, "additional_information": additional_information, "recipient": recipient, + "use_email": use_email, } await self.make_request("POST", "/report", json=data) From 54deb68cb39f0d3ea533b347200658482d095c3b Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 10 Mar 2024 19:11:58 -0500 Subject: [PATCH 15/37] Create dataclass model for report endpoint Create a dataclass model for the report endpoint so that we aren't passing around 7 different arguments. --- src/bot/dragonfly_services.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/bot/dragonfly_services.py b/src/bot/dragonfly_services.py index 2d734d0..89b74fa 100644 --- a/src/bot/dragonfly_services.py +++ b/src/bot/dragonfly_services.py @@ -1,6 +1,7 @@ """Interacting with the Dragonfly API.""" from dataclasses import dataclass +import dataclasses from datetime import UTC, datetime, timedelta from enum import Enum from typing import Any, Self @@ -54,6 +55,14 @@ def __str__(self: Self) -> str: """Return a string representation of the package scan result.""" return f"{self.name} {self.version}" +@dataclass +class PackageReport: + name: str + version: str + inspector_url: str | None + additional_information: str | None + recipient: str | None + use_email: bool class DragonflyServices: """A class wrapping Dragonfly's API.""" @@ -146,22 +155,10 @@ async def get_scanned_packages( data = await self.make_request("GET", "/package", params=params) return [PackageScanResult.from_dict(dct) for dct in data] - async def report_package( # noqa: PLR0913 + async def report_package( self: Self, - name: str, - version: str, - inspector_url: str | None, - additional_information: str | None, - recipient: str | None, - use_email: bool = False, + report: PackageReport, ) -> None: """Report a package to Dragonfly.""" - data = { - "name": name, - "version": version, - "inspector_url": inspector_url, - "additional_information": additional_information, - "recipient": recipient, - "use_email": use_email, - } + data = dataclasses.asdict(report) await self.make_request("POST", "/report", json=data) From 2ebc889c29d07f998ee8894734558e7bce21178c Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 10 Mar 2024 19:29:35 -0500 Subject: [PATCH 16/37] Add ability to use Observations API --- src/bot/exts/dragonfly/dragonfly.py | 195 +++++++++++++++++++++------- 1 file changed, 151 insertions(+), 44 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 76c3f14..deb2952 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -11,16 +11,55 @@ from bot.bot import Bot from bot.constants import Channels, DragonflyConfig, Roles -from bot.dragonfly_services import PackageScanResult +from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult log = getLogger(__name__) log.setLevel(logging.INFO) -class ConfirmReportModal(discord.ui.Modal): - """Modal for confirming a report.""" +def _build_modal_title(name: str, version: str) -> str: + """Build the modal title.""" + title = f"Confirm report for {name} v{version}" + if len(title) >= 45: # noqa: PLR2004 + title = title[:42] + "..." + + return title + + +async def handle_submit( + *, + report: PackageReport, + interaction: discord.Interaction, + dragonfly_services: DragonflyServices, +) -> None: + """Handle modal submit.""" + log.info( + "User %s reported package %s@%s with additional_information '%s' and inspector_url '%s'", + interaction.user, + report.name, + report.version, + report.additional_information, + report.inspector_url, + ) + + log_channel = interaction.client.get_channel(Channels.reporting) + if isinstance(log_channel, discord.abc.Messageable): + await log_channel.send( + f"User {interaction.user.mention} " + f"reported package `{report.name}` " + f"with additional_description `{report.additional_information}`" + f"with inspector_url `{report.inspector_url}`", + ) + + await dragonfly_services.report_package(report) + + await interaction.response.send_message("Reported!", ephemeral=True) + - recipient = discord.ui.TextInput( +class ConfirmEmailReportModal(discord.ui.Modal): + """Modal for confirming an email report.""" + + recipient = discord.ui.TextInput( # type: ignore[var-annotated] label="Recipient", placeholder="Recipient's Email Address", required=False, @@ -48,7 +87,7 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: self.bot = bot # set dynamic properties here because we can't set dynamic class attributes - self.title = self._build_modal_title() + self.title = _build_modal_title(package.name, package.version) self.inspector_url.default = package.inspector_url super().__init__() @@ -56,56 +95,124 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: """Handle errors that occur in the modal.""" if isinstance(error, aiohttp.ClientResponseError): - return await interaction.response.send_message(f"Error from upstream: {error.status}", ephemeral=True) + message = ( + f"Error from upstream: {error.status}\n" + f"```{error.message}```\n" + f"Retry using Observation API instead?" + ) + view = ReportMethodSwitchConfirmationView(previous_modal=self) + return await interaction.response.send_message(message, view=view, ephemeral=True) await interaction.response.send_message("An unexpected error occured.", ephemeral=True) raise error - def _build_modal_title(self: Self) -> str: - """Build the modal title.""" - title = f"Confirm report for {self.package.name} v{self.package.version}" - if len(title) >= 45: # noqa: PLR2004 - title = title[:42] + "..." + async def on_submit(self: Self, interaction: discord.Interaction) -> None: + """Modal submit callback.""" + report = PackageReport( + name=self.package.name, + version=self.package.version, + inspector_url=self.inspector_url.value or None, + additional_information=self.additional_information.value or None, + recipient=self.recipient.value or None, + use_email=True, + ) + + await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) + + +class ConfirmReportModal(discord.ui.Modal): + """Modal for confirming a report through the Observations API.""" + + additional_information = discord.ui.TextInput( + label="Additional information", + placeholder="Additional information", + required=True, + style=discord.TextStyle.long, + ) - return title + inspector_url = discord.ui.TextInput( + label="Inspector URL", + placeholder="Inspector URL", + required=False, + style=discord.TextStyle.short, + ) + + def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: + """Initialize the modal.""" + self.package = package + self.bot = bot + + # set dynamic properties here because we can't set dynamic class attributes + self.title = _build_modal_title(package.name, package.version) + self.inspector_url.default = package.inspector_url + self.recipient = None + + super().__init__() + + async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: + """Handle errors that occur in the modal.""" + if isinstance(error, aiohttp.ClientResponseError): + message = f"Error from upstream: {error.status}\n```{error.message}```\nRetry using email instead?" + view = ReportMethodSwitchConfirmationView(previous_modal=self) + return await interaction.response.send_message(message, view=view, ephemeral=True) + + await interaction.response.send_message("An unexpected error occured.", ephemeral=True) + raise error async def on_submit(self: Self, interaction: discord.Interaction) -> None: - """Submit the report.""" - # discord.py returns empty string "" if not filled out, we want it to be `None` - additional_information_override = self.additional_information.value or None - inspector_url_override = self.inspector_url.value or None - - log.info( - "User %s reported package %s@%s with additional_information '%s' and inspector_url '%s'", - interaction.user, - self.package.name, - self.package.version, - additional_information_override, - inspector_url_override, + """Modal submit callback.""" + report = PackageReport( + name=self.package.name, + version=self.package.version, + inspector_url=self.inspector_url.value or None, + additional_information=self.additional_information.value, + recipient=None, + use_email=False, ) - log_channel = interaction.client.get_channel(Channels.reporting) - if isinstance(log_channel, discord.abc.Messageable): - await log_channel.send( - f"User {interaction.user.mention} " - f"reported package `{self.package.name}` " - f"with additional_description `{additional_information_override}`" - f"with inspector_url `{inspector_url_override}`", - ) + await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) - try: - await self.bot.dragonfly_services.report_package( - name=self.package.name, - version=self.package.version, - inspector_url=inspector_url_override, - additional_information=additional_information_override, - recipient=self.recipient.value, - ) - await interaction.response.send_message("Reported!", ephemeral=True) - except: - await interaction.response.send_message("An unexpected error occured!", ephemeral=True) - raise +class ReportMethodSwitchConfirmationView(discord.ui.View): + """Prompt user if they want to switch reporting methods (email/API). + + View sent when reporting via the Observation API fails, and we want to ask the + user if they want to switch to another method of sending reports. + """ + + def __init__(self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal) -> None: + super().__init__() + self.previous_modal = previous_modal + self.package = previous_modal.package + self.bot = previous_modal.bot + + @discord.ui.button(label="Yes", style=discord.ButtonStyle.green) + async def confirm(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: + """Confirm button callback.""" + if isinstance(self.previous_modal, ConfirmReportModal): + modal = ConfirmEmailReportModal(package=self.package, bot=self.bot) + else: + modal = ConfirmReportModal(package=self.package, bot=self.bot) + + await interaction.response.send_modal(modal) + + self.disable_all() + await interaction.edit_original_response(view=self) + + @discord.ui.button(label="No, retry the operation", style=discord.ButtonStyle.red) + async def cancel(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: + """Cancel button callback.""" + modal = type(self.previous_modal)(package=self.package, bot=self.bot) + + await interaction.response.send_modal(modal) + + self.disable_all() + await interaction.edit_original_response(view=self) + + def disable_all(self: Self) -> None: + """Disable both confirm and cancel buttons.""" + self.confirm.disabled = True + self.cancel.disabled = True class ReportView(discord.ui.View): From 9cdc75a89088d94bfdc777a5c63cfb223094f4c9 Mon Sep 17 00:00:00 2001 From: Robin <74519799+Robin5605@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:31:31 -0500 Subject: [PATCH 17/37] Pin python-lint action to 9.0.0 The upgrade from [`v9.0.0...v10.0.0`](https://github.com/darbiadev/.github/compare/v9.0.0...v10.0.0) introduces `mypy` to the linting, which fails CI because up until now we've been adhering to Pyright Signed-off-by: Robin <74519799+Robin5605@users.noreply.github.com> --- .github/workflows/python-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yaml b/.github/workflows/python-ci.yaml index b0f7aa2..48843bd 100644 --- a/.github/workflows/python-ci.yaml +++ b/.github/workflows/python-ci.yaml @@ -14,7 +14,7 @@ jobs: lint: needs: pre-commit - uses: darbiadev/.github/.github/workflows/python-lint.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 + uses: darbiadev/.github/.github/workflows/python-lint.yaml@6a6eb74ae11149881c29adf5a5f7af23349c8762 # v9.0.0 with: python-version: "3.11" From ca3f8571823cd332cc4c1879960e8b85a9fb5584 Mon Sep 17 00:00:00 2001 From: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:20:52 +0000 Subject: [PATCH 18/37] Update dragonfly.py If the scan's score is above the pre-configured threshold, display a malicious package embed. Otherwise return a different embed to reflect the fact that the package does not seem to be malicious. Signed-off-by: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> --- src/bot/exts/dragonfly/dragonfly.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 146d207..bbbc73e 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -263,12 +263,16 @@ async def report(self: Self, interaction: discord.Interaction, button: discord.u def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord.Embed: """Build the embed that shows the results of a package scan.""" + + condition = scan_result.score >= DragonflyConfig.threshold + title, color = 'Malicious', 0xF70606 if condition else 'Benign', 0x4CBB17 + embed = discord.Embed( - title=f"Malicious package found: {scan_result.name} @ {scan_result.version}", + title=f"{title} package found: {scan_result.name} @ {scan_result.version}", description=f"```YARA rules matched: {', '.join(scan_result.rules) or 'None'}```", - color=0xF70606, + color=color, ) - + embed.add_field( name="\u200b", value=f"[Inspector]({scan_result.inspector_url})", @@ -280,7 +284,7 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. value=f"[PyPI](https://pypi.org/project/{scan_result.name}/{scan_result.version})", inline=True, ) - + return embed From d3b076cb66fc903f0829c26331f1378640e66db8 Mon Sep 17 00:00:00 2001 From: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:28:20 +0000 Subject: [PATCH 19/37] Fix ruff complaints Signed-off-by: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> --- src/bot/exts/dragonfly/dragonfly.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index bbbc73e..af91662 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -263,9 +263,8 @@ async def report(self: Self, interaction: discord.Interaction, button: discord.u def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord.Embed: """Build the embed that shows the results of a package scan.""" - condition = scan_result.score >= DragonflyConfig.threshold - title, color = 'Malicious', 0xF70606 if condition else 'Benign', 0x4CBB17 + title, color = "Malicious", 0xF70606 if condition else "Benign", 0x4CBB17 embed = discord.Embed( title=f"{title} package found: {scan_result.name} @ {scan_result.version}", From 270eea81df14dac640982ac1145bb387844959f1 Mon Sep 17 00:00:00 2001 From: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:31:49 +0000 Subject: [PATCH 20/37] Fix ruff complaints part 2 Signed-off-by: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> --- src/bot/exts/dragonfly/dragonfly.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index af91662..0fb8a29 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -271,7 +271,7 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. description=f"```YARA rules matched: {', '.join(scan_result.rules) or 'None'}```", color=color, ) - + embed.add_field( name="\u200b", value=f"[Inspector]({scan_result.inspector_url})", @@ -283,7 +283,7 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. value=f"[PyPI](https://pypi.org/project/{scan_result.name}/{scan_result.version})", inline=True, ) - + return embed From 11b58d39152173bd7391047f796008c26da03b76 Mon Sep 17 00:00:00 2001 From: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:47:48 +0000 Subject: [PATCH 21/37] Linter no work wtf? Signed-off-by: IlluminatiFish <45714340+IlluminatiFish@users.noreply.github.com> --- src/bot/exts/dragonfly/dragonfly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 0fb8a29..45ce50b 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -264,7 +264,7 @@ async def report(self: Self, interaction: discord.Interaction, button: discord.u def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord.Embed: """Build the embed that shows the results of a package scan.""" condition = scan_result.score >= DragonflyConfig.threshold - title, color = "Malicious", 0xF70606 if condition else "Benign", 0x4CBB17 + title, color = ("Malicious", 0xF70606) if condition else ("Benign", 0x4CBB17) embed = discord.Embed( title=f"{title} package found: {scan_result.name} @ {scan_result.version}", From 5c2f54feb5147e2d919e2e8e3ec6fb36f086e5b9 Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Wed, 27 Mar 2024 21:23:59 -0500 Subject: [PATCH 22/37] Set global HTTP session timeout to 30s (#230) Hopefully this helps fix https://vipyrsec.sentry.io/issues/5052320225 Signed-off-by: Bradley Reynolds --- src/bot/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/__main__.py b/src/bot/__main__.py index 2cd4468..9e92a9e 100644 --- a/src/bot/__main__.py +++ b/src/bot/__main__.py @@ -27,7 +27,7 @@ def get_prefix(bot_: Bot, message_: discord.Message) -> Callable[[Bot, discord.M async def main() -> None: """Run the bot.""" - async with ClientSession(headers={"Content-Type": "application/json"}, timeout=ClientTimeout(total=10)) as session: + async with ClientSession(headers={"Content-Type": "application/json"}, timeout=ClientTimeout(total=30)) as session: dragonfly_services = DragonflyServices( session=session, base_url=constants.Dragonfly.base_url, From b8493891d2c4fc6df4d38b4ac3de4b2e683e72fa Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Fri, 24 May 2024 00:53:18 -0500 Subject: [PATCH 23/37] Add configuration options --- src/bot/constants.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/bot/constants.py b/src/bot/constants.py index 44312d5..b7e12af 100644 --- a/src/bot/constants.py +++ b/src/bot/constants.py @@ -33,6 +33,18 @@ class _Miscellaneous(EnvConfig): Miscellaneous = _Miscellaneous() + +class _ThreatIntelFeed(EnvConfig, env_prefix="tif_"): + """Threat Intelligence Feed Configuration.""" + + repository: str = "pypi/pypi-observation-reports-private" + interval: int = 10 * 60 # 10 minutes + access_token: str = "" + channel_id: int = 1121471544355455058 + + +ThreatIntelFeed = _ThreatIntelFeed() + FILE_LOGS = Miscellaneous.file_logs DEBUG_MODE = Miscellaneous.debug From 5056d5826069e53a3a0d6662380bb31c10a0ac0a Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Fri, 24 May 2024 00:53:30 -0500 Subject: [PATCH 24/37] Add threat intelligence feed cog --- src/bot/exts/dragonfly/threat_intel_feed.py | 166 ++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/bot/exts/dragonfly/threat_intel_feed.py diff --git a/src/bot/exts/dragonfly/threat_intel_feed.py b/src/bot/exts/dragonfly/threat_intel_feed.py new file mode 100644 index 0000000..4f90cea --- /dev/null +++ b/src/bot/exts/dragonfly/threat_intel_feed.py @@ -0,0 +1,166 @@ +"""Threat Intelligence Feed Cog.""" + +import json +import logging +import re +from io import BytesIO +from logging import getLogger +from typing import Any +from zipfile import ZipFile + +import aiohttp +import discord +from discord.ext import commands, tasks + +from bot import constants +from bot.bot import Bot +from bot.dragonfly_services import PackageScanResult + +log = getLogger(__name__) +log.setLevel(logging.INFO) + +_p = re.compile(r"https://inspector.pypi.io/project/(?P\w+)/(?P[\w.]+)/.*") + + +def build_github_link_from_path(path: str) -> str: + """Build a GitHub link to the given path.""" + segments = path.split("/") + path = "/".join(segments[1:]) + + return f"https://github.com/{constants.ThreatIntelFeed.repository}/blob/main/{path}" + + +def parse_package_info_from_inspector_url(inspector_url: str) -> tuple[str, str] | None: + """Return a tuple of package name and version, parsed from the inspector URL. None if it couldn't be parsed.""" + if g := _p.match(inspector_url): + name = g.group("name") + version = g.group("version") + + return name, version + + return None + + +def search(d: dict, key: Any) -> Any | None: # noqa: ANN401 - we can't know the type of the dict ahead of time + """Recursively search for the first occurence of a key in a dict. None if not found.""" + for k, v in d.items(): + if k == key: + return v + + if isinstance(v, dict) and (val := search(v, key)): + return val + + return None + + +def build_embed(package: PackageScanResult, path: str, inspector_url: str) -> discord.Embed: + """Return the embed to be sent in the threat intelligence feed.""" + if package.reported_at: + ts = discord.utils.format_dt(package.reported_at, style="F") + description = f"We already reported this package at {ts}" + color = discord.Color.green() + else: + description = f"We didn't catch this package! Here are our matched rules: ```{', '.join(package.rules)}```" + color = discord.Colour.red() + + embed = discord.Embed( + title=f"New Report: {package.name} v{package.version}", + description=description, + color=color, + url=build_github_link_from_path(path), + ) + + embed.add_field(name="Inspector URL", value=f"[Inspector URL]({inspector_url})") + + return embed + + +def build_package_not_found_embed(name: str, version: str, path: str) -> discord.Embed: + """Return the embed for when a report was filed for a package which we don't have records for.""" + return discord.Embed( + title="Package not found!", + description=( + f"A report was filed for {name} v{version}, " + "however we don't have any records for this package in our database. " + "This means that we are missing packages, please investigate this!" + ), + color=discord.Color.red(), + url=build_github_link_from_path(path), + ) + + +async def fetch_zipfile(http_client: aiohttp.ClientSession) -> ZipFile: + """Download the source zipfile from GitHub for the feed source repository.""" + url = f"https://api.github.com/repos/{constants.ThreatIntelFeed.repository}/zipball" + headers = {"Authorization": f"Bearer {constants.ThreatIntelFeed.access_token}"} + + async with http_client.get(url, headers=headers) as res: + res.raise_for_status() + b = await res.content.read() + + buffer = BytesIO(b) + return ZipFile(buffer) + + +class ThreatIntelFeed(commands.Cog): + """Threat Intelligence Feed Cog.""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + self.reports_seen: set[str] = set() + + @tasks.loop(seconds=constants.ThreatIntelFeed.interval) + async def watcher(self) -> None: + """Watch the GitHub repository for changes.""" + zipfile = await fetch_zipfile(self.bot.http_session) + paths = {path for path in zipfile.namelist() if path.endswith(".json")} + + channel = self.bot.get_channel(constants.ThreatIntelFeed.channel_id) + if not isinstance(channel, discord.abc.Messageable): + log.error("Threat intel feed channel is not messageable") + return + + # The first time around, just add all the reports to our "seen reports" set + if len(self.reports_seen) == 0: + self.reports_seen |= paths + return + + for path in paths: + if path in self.reports_seen: + continue + + content = json.loads(zipfile.read(path).decode()) + inspector_url: str | None = search(content, "inspector_url") + if not inspector_url: + log.error("Inspector URL not found in %s, skipping", path) + continue + + match parse_package_info_from_inspector_url(inspector_url): + case name, version: + results = await self.bot.dragonfly_services.get_scanned_packages(name=name, version=version) + package = results[0] if results else None + + if package: + embed = build_embed(package, path, inspector_url) + else: + embed = build_package_not_found_embed(name, version, path) + + await channel.send(embed=embed) + + case None: + log.error('Unable to parse inspector URL: "%s" in %s, skipping', inspector_url, path) + continue + + @watcher.before_loop + async def before_watcher(self) -> None: + """Before first task run hook.""" + await self.bot.wait_until_ready() + + +async def setup(bot: Bot) -> None: + """Extension setup.""" + cog = ThreatIntelFeed(bot) + task = cog.watcher + if not task.is_running: + task.start() + await bot.add_cog(cog) From b897f4a8585f89dc86fea2a0afdcb4a6f9c889db Mon Sep 17 00:00:00 2001 From: Robin <74519799+Robin5605@users.noreply.github.com> Date: Fri, 24 May 2024 19:58:36 -0500 Subject: [PATCH 25/37] Change default interval length Change the default interval to fetch the repository contents to one hour Co-authored-by: Rem <128343390+import-pandas-as-numpy@users.noreply.github.com> Signed-off-by: Robin <74519799+Robin5605@users.noreply.github.com> --- src/bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/constants.py b/src/bot/constants.py index b7e12af..052928e 100644 --- a/src/bot/constants.py +++ b/src/bot/constants.py @@ -38,7 +38,7 @@ class _ThreatIntelFeed(EnvConfig, env_prefix="tif_"): """Threat Intelligence Feed Configuration.""" repository: str = "pypi/pypi-observation-reports-private" - interval: int = 10 * 60 # 10 minutes + interval: int = 60 * 60 # 1 hour access_token: str = "" channel_id: int = 1121471544355455058 From 59dd2a86064db54687aa2eff9e2ca042d03a92c3 Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Thu, 30 May 2024 22:27:15 +0100 Subject: [PATCH 26/37] Update dragonfly.py Updated view for package alerts, adding a triage system #242 Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 289 ++++++++++++++++++++++++++-- 1 file changed, 275 insertions(+), 14 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 45ce50b..3ad2551 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -1,5 +1,7 @@ """Download the most recent packages from PyPI and use Dragonfly to check them for malware.""" +from __future__ import annotations + import logging from datetime import UTC, datetime, timedelta from logging import getLogger @@ -127,9 +129,13 @@ async def on_error(self: Self, interaction: discord.Interaction, error: Exceptio f"Retry using Observation API instead?" ) view = ReportMethodSwitchConfirmationView(previous_modal=self) - return await interaction.response.send_message(message, view=view, ephemeral=True) + return await interaction.response.send_message( + message, view=view, ephemeral=True + ) - await interaction.response.send_message("An unexpected error occured.", ephemeral=True) + await interaction.response.send_message( + "An unexpected error occured.", ephemeral=True + ) raise error async def on_submit(self: Self, interaction: discord.Interaction) -> None: @@ -143,7 +149,11 @@ async def on_submit(self: Self, interaction: discord.Interaction) -> None: use_email=True, ) - await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) + await handle_submit( + report=report, + interaction=interaction, + dragonfly_services=self.bot.dragonfly_services, + ) class ConfirmReportModal(discord.ui.Modal): @@ -175,14 +185,20 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: super().__init__() - async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: + async def on_error( + self: Self, interaction: discord.Interaction, error: Exception + ) -> None: """Handle errors that occur in the modal.""" if isinstance(error, aiohttp.ClientResponseError): message = f"Error from upstream: {error.status}\n```{error.message}```\nRetry using email instead?" view = ReportMethodSwitchConfirmationView(previous_modal=self) - return await interaction.response.send_message(message, view=view, ephemeral=True) + return await interaction.response.send_message( + message, view=view, ephemeral=True + ) - await interaction.response.send_message("An unexpected error occured.", ephemeral=True) + await interaction.response.send_message( + "An unexpected error occured.", ephemeral=True + ) raise error async def on_submit(self: Self, interaction: discord.Interaction) -> None: @@ -196,7 +212,11 @@ async def on_submit(self: Self, interaction: discord.Interaction) -> None: use_email=False, ) - await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) + await handle_submit( + report=report, + interaction=interaction, + dragonfly_services=self.bot.dragonfly_services, + ) class ReportMethodSwitchConfirmationView(discord.ui.View): @@ -206,14 +226,18 @@ class ReportMethodSwitchConfirmationView(discord.ui.View): user if they want to switch to another method of sending reports. """ - def __init__(self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal) -> None: + def __init__( + self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal + ) -> None: super().__init__() self.previous_modal = previous_modal self.package = previous_modal.package self.bot = previous_modal.bot @discord.ui.button(label="Yes", style=discord.ButtonStyle.green) - async def confirm(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: + async def confirm( + self: Self, interaction: discord.Interaction, _button: discord.ui.Button + ) -> None: """Confirm button callback.""" if isinstance(self.previous_modal, ConfirmReportModal): modal = ConfirmEmailReportModal(package=self.package, bot=self.bot) @@ -226,7 +250,9 @@ async def confirm(self: Self, interaction: discord.Interaction, _button: discord await interaction.edit_original_response(view=self) @discord.ui.button(label="No, retry the operation", style=discord.ButtonStyle.red) - async def cancel(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: + async def cancel( + self: Self, interaction: discord.Interaction, _button: discord.ui.Button + ) -> None: """Cancel button callback.""" modal = type(self.previous_modal)(package=self.package, bot=self.bot) @@ -261,6 +287,201 @@ async def report(self: Self, interaction: discord.Interaction, button: discord.u await interaction.edit_original_response(view=self) +class NoteModal(discord.ui.Modal, title="Add a note"): + """A modal that allows users to add a note to a package""" + + _interaction: discord.Interaction | None = None + note_content = discord.ui.TextInput( + label="Content", + placeholder="Enter the note content here", + min_length=1, + max_length=1000, # Don't want to overfill the embed + ) + + def __init__(self, embed: discord.Embed, view: discord.ui.View): + super().__init__() + + self.embed = embed + self.view = view + + async def on_submit(self, interaction: discord.Interaction) -> None: + if not interaction.response.is_done(): + await interaction.response.defer() + self._interaction = interaction + + content = f"{self.note_content.value} • {interaction.user.mention}" + + # We need to check what fields the embed has to determine where to add the note + # If the embed has no fields, we add the note and return + # Otherwise, we need to make sure the note is added after the event log + # This involves clearing the fields and re-adding them in the correct order + # Which is why we save the event log in a variable + + match len(self.embed.fields): + case 0: # Package is awaiting triage, no notes or event log + notes = [content] + event_log = None + case 1: # Package either has notes or event log + if self.embed.fields[0].name == "Notes": + notes = [self.embed.fields[0].value, content] + else: + event_log = self.embed.fields[0].value + notes = [content] + self.embed.clear_fields() + case 2: # Package has both notes and event log + if self.embed.fields[0].name == "Notes": + notes = [self.embed.fields[0].value, content] + event_log = self.embed.fields[1].value + else: + notes = [self.embed.fields[1].value, content] + event_log = self.embed.fields[0].value + self.embed.clear_fields() + + self.embed.add_field(name="Notes", value="\n".join(notes), inline=False) + + if event_log: + self.embed.add_field(name="Event log", value=event_log, inline=False) + + await interaction.message.edit(embed=self.embed, view=self.view) + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + + await interaction.response.send_message( + "An unexpected error occured.", ephemeral=True + ) + raise error + + @property + def interaction(self) -> discord.Interaction | None: + return self._interaction + + +class MalwareView(discord.ui.View): + """View for the malware triage system""" + + message: discord.Message | None = None + + def __init__( + self: Self, embed: discord.Embed, bot: Bot, payload: PackageScanResult + ) -> None: + self.embed = embed + self.bot = bot + self.payload = payload + self.event_log = [] + + super().__init__() + + async def enable_button(self, button_label: str) -> None: + for button in self.children: + if button.label == button_label: + button.disabled = False + + async def add_event(self, message: str) -> None: + # Much like earlier, we need to check the fields of the embed to determine where to add the event log + match len(self.embed.fields): + case 0: + pass + case 1: + if self.embed.fields[0].name == "Event log": + self.embed.clear_fields() + case 2: + if self.embed.fields[0].name == "Event log": + self.embed.clear_fields() + elif self.embed.fields[1].name == "Event log": + self.embed.remove_field(1) + + self.event_log.append( + message + ) # For future reference, we save the event log in a variable + self.embed.add_field( + name="Event log", value="\n".join(self.event_log), inline=False + ) + + async def update_status(self, status: str) -> None: + self.embed.set_footer(text=status) + + def get_timestamp( + self, + ) -> ( + int + ): # This function returns the current timestamp in Discord's timestamp format + return f"" + + @discord.ui.button(label="Report", style=discord.ButtonStyle.red) + async def report(self: Self, interaction: discord.Interaction, button: discord.ui.Button) -> None: # type: ignore[type-arg] + """Report a package.""" + modal = ConfirmReportModal(package=self.payload, bot=self.bot) + await interaction.response.send_modal(modal) + + timed_out = await modal.wait() + if not timed_out: + button.disabled = True + await interaction.edit_original_response(view=self) + + @discord.ui.button( + label="Report", + style=discord.ButtonStyle.red, + ) + async def report( + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + ) -> None: + await self.enable_button("Approve") + await self.add_event( + f"Reported by {interaction.user.mention} • {self.get_timestamp()}" + ) + await self.update_status("Flagged as malicious") + + self.embed.color = discord.Color.red() + + modal = ConfirmReportModal(package=self.payload, bot=self.bot) + await interaction.response.send_modal(modal) + + timed_out = await modal.wait() + if not timed_out: + button.disabled = True + await interaction.edit_original_response(view=self, embed=self.embed) + + @discord.ui.button( + label="Approve", + style=discord.ButtonStyle.green, + ) + async def approve( + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + ) -> None: + await self.enable_button("Report") + await self.add_event( + f"Approved by {interaction.user.mention} • {self.get_timestamp()}" + ) + await self.update_status("Flagged as benign") + + button.disabled = True + + self.embed.color = discord.Color.green() + await interaction.response.edit_message(view=self, embed=self.embed) + + @discord.ui.button( + label="Add note", + style=discord.ButtonStyle.grey, + ) + async def add_note( + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + ) -> None: + await interaction.response.send_modal(NoteModal(embed=self.embed, view=self)) + + async def on_error( + self, + interaction: discord.Interaction[discord.Client], + error: Exception, + ) -> None: + + await interaction.response.send_message( + "An unexpected error occured.", ephemeral=True + ) + raise error + + def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord.Embed: """Build the embed that shows the results of a package scan.""" condition = scan_result.score >= DragonflyConfig.threshold @@ -287,7 +508,31 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. return embed -def _build_all_packages_scanned_embed(scan_results: list[PackageScanResult]) -> discord.Embed: +def _build_package_scan_result_triage_embed( + scan_result: PackageScanResult, +) -> discord.Embed: + """Build the embed for the malware triage system""" + + embed = discord.Embed( + title="View on Inspector", + description="\n".join(scan_result.rules), + url=scan_result.inspector_url, + color=discord.Color.orange(), + timestamp=datetime.now(UTC), + ) + embed.set_author( + name=f"{scan_result.name}@{scan_result.version}", + url=f"https://pypi.org/project/{scan_result.name}/{scan_result.version}", + icon_url="https://seeklogo.com/images/P/pypi-logo-5B953CE804-seeklogo.com.png", + ) + embed.set_footer(text="Awaiting triage") + + return embed + + +def _build_all_packages_scanned_embed( + scan_results: list[PackageScanResult], +) -> discord.Embed: """Build the embed that shows a list of all packages scanned.""" if scan_results: description = "\n".join(map(str, scan_results)) @@ -307,12 +552,22 @@ async def run( scan_results = await bot.dragonfly_services.get_scanned_packages(since=since) for result in scan_results: if result.score >= score: + """ embed = _build_package_scan_result_embed(result) await alerts_channel.send( f"<@&{DragonflyConfig.alerts_role_id}>", embed=embed, view=ReportView(bot, result), ) + """ + embed = _build_package_scan_result_triage_embed(result) + view = MalwareView(embed=embed, bot=bot, payload=result) + + view.message = await alerts_channel.send( + f"<@&{DragonflyConfig.alerts_role_id}>", + embed=embed, + view=view, + ) await logs_channel.send(embed=_build_all_packages_scanned_embed(scan_results)) @@ -330,7 +585,9 @@ def __init__(self: Self, bot: Bot) -> None: @commands.hybrid_command(name="username") # type: ignore [arg-type] async def get_username_command(self, ctx: commands.Context[Bot]) -> None: """Get the username of the currently logged in user to the PyPI Observation API.""" - async with ctx.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: + async with ctx.bot.http_session.get( + DragonflyConfig.reporter_url + "/echo" + ) as res: json = await res.json() username = json["username"] @@ -392,12 +649,16 @@ async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: @discord.app_commands.command(name="lookup", description="Scans a package") async def lookup(self: Self, interaction: discord.Interaction, name: str, version: str | None = None) -> None: # type: ignore[type-arg] """Pull the scan results for a package.""" - scan_results = await self.bot.dragonfly_services.get_scanned_packages(name=name, version=version) + scan_results = await self.bot.dragonfly_services.get_scanned_packages( + name=name, version=version + ) if scan_results: embed = _build_package_scan_result_embed(scan_results[0]) await interaction.response.send_message(embed=embed) else: - await interaction.response.send_message("No entries were found with the specified filters.") + await interaction.response.send_message( + "No entries were found with the specified filters." + ) @commands.group() async def threshold(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] From c6e355e2479b45d449fdb328dcb6b180c53beced Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Fri, 31 May 2024 16:30:00 +0100 Subject: [PATCH 27/37] Linted with ruff * Removed type annotation * Removed extra remove method * Removed commented out method Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 100 ++++++++++++---------------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 3ad2551..41cae32 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -1,20 +1,19 @@ """Download the most recent packages from PyPI and use Dragonfly to check them for malware.""" -from __future__ import annotations - import logging from datetime import UTC, datetime, timedelta from logging import getLogger -from typing import Self +from typing import TYPE_CHECKING, Self import aiohttp import discord import sentry_sdk -from discord.ext import commands, tasks - -from bot.bot import Bot from bot.constants import Channels, DragonflyConfig, Roles from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult +from discord.ext import commands, tasks + +if TYPE_CHECKING: + from bot.bot import Bot log = getLogger(__name__) log.setLevel(logging.INFO) @@ -130,11 +129,11 @@ async def on_error(self: Self, interaction: discord.Interaction, error: Exceptio ) view = ReportMethodSwitchConfirmationView(previous_modal=self) return await interaction.response.send_message( - message, view=view, ephemeral=True + message, view=view, ephemeral=True, ) await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True + "An unexpected error occured.", ephemeral=True, ) raise error @@ -186,18 +185,18 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: super().__init__() async def on_error( - self: Self, interaction: discord.Interaction, error: Exception + self: Self, interaction: discord.Interaction, error: Exception, ) -> None: """Handle errors that occur in the modal.""" if isinstance(error, aiohttp.ClientResponseError): message = f"Error from upstream: {error.status}\n```{error.message}```\nRetry using email instead?" view = ReportMethodSwitchConfirmationView(previous_modal=self) return await interaction.response.send_message( - message, view=view, ephemeral=True + message, view=view, ephemeral=True, ) await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True + "An unexpected error occured.", ephemeral=True, ) raise error @@ -227,7 +226,7 @@ class ReportMethodSwitchConfirmationView(discord.ui.View): """ def __init__( - self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal + self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal, ) -> None: super().__init__() self.previous_modal = previous_modal @@ -236,7 +235,7 @@ def __init__( @discord.ui.button(label="Yes", style=discord.ButtonStyle.green) async def confirm( - self: Self, interaction: discord.Interaction, _button: discord.ui.Button + self: Self, interaction: discord.Interaction, _button: discord.ui.Button, ) -> None: """Confirm button callback.""" if isinstance(self.previous_modal, ConfirmReportModal): @@ -251,7 +250,7 @@ async def confirm( @discord.ui.button(label="No, retry the operation", style=discord.ButtonStyle.red) async def cancel( - self: Self, interaction: discord.Interaction, _button: discord.ui.Button + self: Self, interaction: discord.Interaction, _button: discord.ui.Button, ) -> None: """Cancel button callback.""" modal = type(self.previous_modal)(package=self.package, bot=self.bot) @@ -288,7 +287,7 @@ async def report(self: Self, interaction: discord.Interaction, button: discord.u class NoteModal(discord.ui.Modal, title="Add a note"): - """A modal that allows users to add a note to a package""" + """A modal that allows users to add a note to a package.""" _interaction: discord.Interaction | None = None note_content = discord.ui.TextInput( @@ -298,13 +297,14 @@ class NoteModal(discord.ui.Modal, title="Add a note"): max_length=1000, # Don't want to overfill the embed ) - def __init__(self, embed: discord.Embed, view: discord.ui.View): + def __init__(self, embed: discord.Embed, view: discord.ui.View) -> None: super().__init__() self.embed = embed self.view = view async def on_submit(self, interaction: discord.Interaction) -> None: + """Modal submit callback.""" if not interaction.response.is_done(): await interaction.response.defer() self._interaction = interaction @@ -345,26 +345,27 @@ async def on_submit(self, interaction: discord.Interaction) -> None: await interaction.message.edit(embed=self.embed, view=self.view) async def on_error( - self, interaction: discord.Interaction, error: Exception + self, interaction: discord.Interaction, error: Exception, ) -> None: - + """Handle errors that occur in the modal.""" await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True + "An unexpected error occured.", ephemeral=True, ) raise error @property def interaction(self) -> discord.Interaction | None: + """Get the interaction that triggered the modal.""" return self._interaction class MalwareView(discord.ui.View): - """View for the malware triage system""" + """View for the malware triage system.""" message: discord.Message | None = None def __init__( - self: Self, embed: discord.Embed, bot: Bot, payload: PackageScanResult + self: Self, embed: discord.Embed, bot: Bot, payload: PackageScanResult, ) -> None: self.embed = embed self.bot = bot @@ -374,11 +375,13 @@ def __init__( super().__init__() async def enable_button(self, button_label: str) -> None: + """Enables a button by its label.""" for button in self.children: if button.label == button_label: button.disabled = False async def add_event(self, message: str) -> None: + """Add an event to the event log.""" # Much like earlier, we need to check the fields of the embed to determine where to add the event log match len(self.embed.fields): case 0: @@ -393,43 +396,35 @@ async def add_event(self, message: str) -> None: self.embed.remove_field(1) self.event_log.append( - message + message, ) # For future reference, we save the event log in a variable self.embed.add_field( - name="Event log", value="\n".join(self.event_log), inline=False + name="Event log", value="\n".join(self.event_log), inline=False, ) async def update_status(self, status: str) -> None: + """Update the status of the package in the embed.""" self.embed.set_footer(text=status) def get_timestamp( self, ) -> ( int - ): # This function returns the current timestamp in Discord's timestamp format - return f"" - - @discord.ui.button(label="Report", style=discord.ButtonStyle.red) - async def report(self: Self, interaction: discord.Interaction, button: discord.ui.Button) -> None: # type: ignore[type-arg] - """Report a package.""" - modal = ConfirmReportModal(package=self.payload, bot=self.bot) - await interaction.response.send_modal(modal) - - timed_out = await modal.wait() - if not timed_out: - button.disabled = True - await interaction.edit_original_response(view=self) + ): + """Returns the current timestamp in seconds.""" + return f"" @discord.ui.button( label="Report", style=discord.ButtonStyle.red, ) async def report( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], ) -> None: + """Report package and update the embed.""" await self.enable_button("Approve") await self.add_event( - f"Reported by {interaction.user.mention} • {self.get_timestamp()}" + f"Reported by {interaction.user.mention} • {self.get_timestamp()}", ) await self.update_status("Flagged as malicious") @@ -448,11 +443,12 @@ async def report( style=discord.ButtonStyle.green, ) async def approve( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], ) -> None: + """Approve package and update the embed.""" await self.enable_button("Report") await self.add_event( - f"Approved by {interaction.user.mention} • {self.get_timestamp()}" + f"Approved by {interaction.user.mention} • {self.get_timestamp()}", ) await self.update_status("Flagged as benign") @@ -466,8 +462,9 @@ async def approve( style=discord.ButtonStyle.grey, ) async def add_note( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView] + self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], ) -> None: + """Add note to the embed.""" await interaction.response.send_modal(NoteModal(embed=self.embed, view=self)) async def on_error( @@ -475,9 +472,9 @@ async def on_error( interaction: discord.Interaction[discord.Client], error: Exception, ) -> None: - + """Handle errors that occur in the view.""" await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True + "An unexpected error occured.", ephemeral=True, ) raise error @@ -511,8 +508,7 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. def _build_package_scan_result_triage_embed( scan_result: PackageScanResult, ) -> discord.Embed: - """Build the embed for the malware triage system""" - + """Build the embed for the malware triage system.""" embed = discord.Embed( title="View on Inspector", description="\n".join(scan_result.rules), @@ -552,14 +548,6 @@ async def run( scan_results = await bot.dragonfly_services.get_scanned_packages(since=since) for result in scan_results: if result.score >= score: - """ - embed = _build_package_scan_result_embed(result) - await alerts_channel.send( - f"<@&{DragonflyConfig.alerts_role_id}>", - embed=embed, - view=ReportView(bot, result), - ) - """ embed = _build_package_scan_result_triage_embed(result) view = MalwareView(embed=embed, bot=bot, payload=result) @@ -586,7 +574,7 @@ def __init__(self: Self, bot: Bot) -> None: async def get_username_command(self, ctx: commands.Context[Bot]) -> None: """Get the username of the currently logged in user to the PyPI Observation API.""" async with ctx.bot.http_session.get( - DragonflyConfig.reporter_url + "/echo" + DragonflyConfig.reporter_url + "/echo", ) as res: json = await res.json() username = json["username"] @@ -650,14 +638,14 @@ async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: async def lookup(self: Self, interaction: discord.Interaction, name: str, version: str | None = None) -> None: # type: ignore[type-arg] """Pull the scan results for a package.""" scan_results = await self.bot.dragonfly_services.get_scanned_packages( - name=name, version=version + name=name, version=version, ) if scan_results: embed = _build_package_scan_result_embed(scan_results[0]) await interaction.response.send_message(embed=embed) else: await interaction.response.send_message( - "No entries were found with the specified filters." + "No entries were found with the specified filters.", ) @commands.group() From ce891c59d921d5270546208d68012ecb7c125057 Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Fri, 31 May 2024 16:39:25 +0100 Subject: [PATCH 28/37] Fixed line length changes Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 46 ++++++++--------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 41cae32..165b0ee 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -1,5 +1,7 @@ """Download the most recent packages from PyPI and use Dragonfly to check them for malware.""" +from __future__ import annotations + import logging from datetime import UTC, datetime, timedelta from logging import getLogger @@ -128,9 +130,7 @@ async def on_error(self: Self, interaction: discord.Interaction, error: Exceptio f"Retry using Observation API instead?" ) view = ReportMethodSwitchConfirmationView(previous_modal=self) - return await interaction.response.send_message( - message, view=view, ephemeral=True, - ) + return await interaction.response.send_message(message, view=view, ephemeral=True) await interaction.response.send_message( "An unexpected error occured.", ephemeral=True, @@ -148,11 +148,7 @@ async def on_submit(self: Self, interaction: discord.Interaction) -> None: use_email=True, ) - await handle_submit( - report=report, - interaction=interaction, - dragonfly_services=self.bot.dragonfly_services, - ) + await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) class ConfirmReportModal(discord.ui.Modal): @@ -184,20 +180,14 @@ def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: super().__init__() - async def on_error( - self: Self, interaction: discord.Interaction, error: Exception, - ) -> None: + async def on_error(self: Self, interaction: discord.Interaction, error: Exception) -> None: """Handle errors that occur in the modal.""" if isinstance(error, aiohttp.ClientResponseError): message = f"Error from upstream: {error.status}\n```{error.message}```\nRetry using email instead?" view = ReportMethodSwitchConfirmationView(previous_modal=self) - return await interaction.response.send_message( - message, view=view, ephemeral=True, - ) + return await interaction.response.send_message(message, view=view, ephemeral=True) - await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True, - ) + await interaction.response.send_message("An unexpected error occured.", ephemeral=True) raise error async def on_submit(self: Self, interaction: discord.Interaction) -> None: @@ -211,11 +201,7 @@ async def on_submit(self: Self, interaction: discord.Interaction) -> None: use_email=False, ) - await handle_submit( - report=report, - interaction=interaction, - dragonfly_services=self.bot.dragonfly_services, - ) + await handle_submit(report=report, interaction=interaction, dragonfly_services=self.bot.dragonfly_services) class ReportMethodSwitchConfirmationView(discord.ui.View): @@ -225,18 +211,14 @@ class ReportMethodSwitchConfirmationView(discord.ui.View): user if they want to switch to another method of sending reports. """ - def __init__( - self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal, - ) -> None: + def __init__(self: Self, previous_modal: ConfirmReportModal | ConfirmEmailReportModal) -> None: super().__init__() self.previous_modal = previous_modal self.package = previous_modal.package self.bot = previous_modal.bot @discord.ui.button(label="Yes", style=discord.ButtonStyle.green) - async def confirm( - self: Self, interaction: discord.Interaction, _button: discord.ui.Button, - ) -> None: + async def confirm(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: """Confirm button callback.""" if isinstance(self.previous_modal, ConfirmReportModal): modal = ConfirmEmailReportModal(package=self.package, bot=self.bot) @@ -249,9 +231,7 @@ async def confirm( await interaction.edit_original_response(view=self) @discord.ui.button(label="No, retry the operation", style=discord.ButtonStyle.red) - async def cancel( - self: Self, interaction: discord.Interaction, _button: discord.ui.Button, - ) -> None: + async def cancel(self: Self, interaction: discord.Interaction, _button: discord.ui.Button) -> None: """Cancel button callback.""" modal = type(self.previous_modal)(package=self.package, bot=self.bot) @@ -526,9 +506,7 @@ def _build_package_scan_result_triage_embed( return embed -def _build_all_packages_scanned_embed( - scan_results: list[PackageScanResult], -) -> discord.Embed: +def _build_all_packages_scanned_embed(scan_results: list[PackageScanResult]) -> discord.Embed: """Build the embed that shows a list of all packages scanned.""" if scan_results: description = "\n".join(map(str, scan_results)) From 2d45e06cd43906ad785ce166575a6ff1ef44e678 Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Fri, 31 May 2024 16:40:59 +0100 Subject: [PATCH 29/37] Missed a couple Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 165b0ee..2801c4f 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -132,9 +132,7 @@ async def on_error(self: Self, interaction: discord.Interaction, error: Exceptio view = ReportMethodSwitchConfirmationView(previous_modal=self) return await interaction.response.send_message(message, view=view, ephemeral=True) - await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True, - ) + await interaction.response.send_message("An unexpected error occured.", ephemeral=True) raise error async def on_submit(self: Self, interaction: discord.Interaction) -> None: @@ -551,9 +549,7 @@ def __init__(self: Self, bot: Bot) -> None: @commands.hybrid_command(name="username") # type: ignore [arg-type] async def get_username_command(self, ctx: commands.Context[Bot]) -> None: """Get the username of the currently logged in user to the PyPI Observation API.""" - async with ctx.bot.http_session.get( - DragonflyConfig.reporter_url + "/echo", - ) as res: + async with ctx.bot.http_session.get(DragonflyConfig.reporter_url + "/echo") as res: json = await res.json() username = json["username"] @@ -615,16 +611,12 @@ async def stop(self: Self, ctx: commands.Context, force: bool = False) -> None: @discord.app_commands.command(name="lookup", description="Scans a package") async def lookup(self: Self, interaction: discord.Interaction, name: str, version: str | None = None) -> None: # type: ignore[type-arg] """Pull the scan results for a package.""" - scan_results = await self.bot.dragonfly_services.get_scanned_packages( - name=name, version=version, - ) + scan_results = await self.bot.dragonfly_services.get_scanned_packages(name=name, version=version) if scan_results: embed = _build_package_scan_result_embed(scan_results[0]) await interaction.response.send_message(embed=embed) else: - await interaction.response.send_message( - "No entries were found with the specified filters.", - ) + await interaction.response.send_message("No entries were found with the specified filters.") @commands.group() async def threshold(self: Self, ctx: commands.Context) -> None: # type: ignore[type-arg] From 6cf3f1da84bc2338585bd7aa2e04c8903c8b621b Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Fri, 31 May 2024 17:07:39 +0100 Subject: [PATCH 30/37] Removed annotations Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 2801c4f..b8ce5d4 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -1,7 +1,5 @@ """Download the most recent packages from PyPI and use Dragonfly to check them for malware.""" -from __future__ import annotations - import logging from datetime import UTC, datetime, timedelta from logging import getLogger From 4de511862b8c8d349d891cb96e394c29ac1a9f2a Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Sun, 2 Jun 2024 23:29:51 +0100 Subject: [PATCH 31/37] Update src/bot/exts/dragonfly/dragonfly.py Co-authored-by: Robin <74519799+Robin5605@users.noreply.github.com> Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index b8ce5d4..10c088f 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -387,7 +387,7 @@ def get_timestamp( ) -> ( int ): - """Returns the current timestamp in seconds.""" + """Returns the current timestamp, formatted in Discord's relative style""" return f"" @discord.ui.button( From c21d60f9053bb597f0ca5f445f78310b28db4382 Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Sun, 2 Jun 2024 23:31:47 +0100 Subject: [PATCH 32/37] Update src/bot/exts/dragonfly/dragonfly.py Co-authored-by: Robin <74519799+Robin5605@users.noreply.github.com> Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 10c088f..0773542 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -398,7 +398,7 @@ async def report( self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], ) -> None: """Report package and update the embed.""" - await self.enable_button("Approve") + self.approve.disabled = False await self.add_event( f"Reported by {interaction.user.mention} • {self.get_timestamp()}", ) From ea8e5841623ee835d482617bc5811f307a69f39e Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Sun, 2 Jun 2024 23:33:33 +0100 Subject: [PATCH 33/37] Updated to remove enable button function Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 0773542..5b34314 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -329,11 +329,6 @@ async def on_error( ) raise error - @property - def interaction(self) -> discord.Interaction | None: - """Get the interaction that triggered the modal.""" - return self._interaction - class MalwareView(discord.ui.View): """View for the malware triage system.""" @@ -422,7 +417,7 @@ async def approve( self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], ) -> None: """Approve package and update the embed.""" - await self.enable_button("Report") + self.report.disabled = False await self.add_event( f"Approved by {interaction.user.mention} • {self.get_timestamp()}", ) From 4f2a63617eb6d5d43e1c213d5c9199b27cbb87cc Mon Sep 17 00:00:00 2001 From: Jayy001 Date: Sun, 2 Jun 2024 23:36:12 +0100 Subject: [PATCH 34/37] Added timestamp Signed-off-by: Jayy001 --- src/bot/exts/dragonfly/dragonfly.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 5b34314..9611927 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -11,6 +11,7 @@ from bot.constants import Channels, DragonflyConfig, Roles from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult from discord.ext import commands, tasks +from discord.utils import format_dt if TYPE_CHECKING: from bot.bot import Bot @@ -383,7 +384,7 @@ def get_timestamp( int ): """Returns the current timestamp, formatted in Discord's relative style""" - return f"" + return format_dt(datetime.now(UTC), style="R") @discord.ui.button( label="Report", From e5451d0c11ba270409aa8e42ecf120a04e864bd3 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 2 Jun 2024 17:52:14 -0500 Subject: [PATCH 35/37] Fix incorrect return type for method --- src/bot/exts/dragonfly/dragonfly.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 9611927..dd44016 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -378,11 +378,7 @@ async def update_status(self, status: str) -> None: """Update the status of the package in the embed.""" self.embed.set_footer(text=status) - def get_timestamp( - self, - ) -> ( - int - ): + def get_timestamp(self) -> str: """Returns the current timestamp, formatted in Discord's relative style""" return format_dt(datetime.now(UTC), style="R") From 285700f75b7f0fa59f1cbdbbb843dc5d0688af32 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Sun, 2 Jun 2024 18:57:04 -0500 Subject: [PATCH 36/37] Lint --- src/bot/exts/dragonfly/dragonfly.py | 49 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index dd44016..6921478 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -3,18 +3,17 @@ import logging from datetime import UTC, datetime, timedelta from logging import getLogger -from typing import TYPE_CHECKING, Self +from typing import Self import aiohttp import discord import sentry_sdk -from bot.constants import Channels, DragonflyConfig, Roles -from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult from discord.ext import commands, tasks from discord.utils import format_dt -if TYPE_CHECKING: - from bot.bot import Bot +from bot.bot import Bot +from bot.constants import Channels, DragonflyConfig, Roles +from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult log = getLogger(__name__) log.setLevel(logging.INFO) @@ -322,11 +321,14 @@ async def on_submit(self, interaction: discord.Interaction) -> None: await interaction.message.edit(embed=self.embed, view=self.view) async def on_error( - self, interaction: discord.Interaction, error: Exception, + self, + interaction: discord.Interaction, + error: Exception, ) -> None: """Handle errors that occur in the modal.""" await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True, + "An unexpected error occured.", + ephemeral=True, ) raise error @@ -337,7 +339,10 @@ class MalwareView(discord.ui.View): message: discord.Message | None = None def __init__( - self: Self, embed: discord.Embed, bot: Bot, payload: PackageScanResult, + self: Self, + embed: discord.Embed, + bot: Bot, + payload: PackageScanResult, ) -> None: self.embed = embed self.bot = bot @@ -346,12 +351,6 @@ def __init__( super().__init__() - async def enable_button(self, button_label: str) -> None: - """Enables a button by its label.""" - for button in self.children: - if button.label == button_label: - button.disabled = False - async def add_event(self, message: str) -> None: """Add an event to the event log.""" # Much like earlier, we need to check the fields of the embed to determine where to add the event log @@ -371,7 +370,9 @@ async def add_event(self, message: str) -> None: message, ) # For future reference, we save the event log in a variable self.embed.add_field( - name="Event log", value="\n".join(self.event_log), inline=False, + name="Event log", + value="\n".join(self.event_log), + inline=False, ) async def update_status(self, status: str) -> None: @@ -379,7 +380,7 @@ async def update_status(self, status: str) -> None: self.embed.set_footer(text=status) def get_timestamp(self) -> str: - """Returns the current timestamp, formatted in Discord's relative style""" + """Return the current timestamp, formatted in Discord's relative style.""" return format_dt(datetime.now(UTC), style="R") @discord.ui.button( @@ -387,7 +388,9 @@ def get_timestamp(self) -> str: style=discord.ButtonStyle.red, ) async def report( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], + self, + interaction: discord.Interaction, + button: discord.ui.Button, ) -> None: """Report package and update the embed.""" self.approve.disabled = False @@ -411,7 +414,9 @@ async def report( style=discord.ButtonStyle.green, ) async def approve( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], + self, + interaction: discord.Interaction, + button: discord.ui.Button, ) -> None: """Approve package and update the embed.""" self.report.disabled = False @@ -430,7 +435,9 @@ async def approve( style=discord.ButtonStyle.grey, ) async def add_note( - self, interaction: discord.Interaction, button: discord.ui.Button[MalwareView], + self, + interaction: discord.Interaction, + _button: discord.ui.Button, ) -> None: """Add note to the embed.""" await interaction.response.send_modal(NoteModal(embed=self.embed, view=self)) @@ -439,10 +446,12 @@ async def on_error( self, interaction: discord.Interaction[discord.Client], error: Exception, + _item: discord.ui.Item, ) -> None: """Handle errors that occur in the view.""" await interaction.response.send_message( - "An unexpected error occured.", ephemeral=True, + "An unexpected error occured.", + ephemeral=True, ) raise error From c16095720300e2fdae61aa6716b16e051bcbf008 Mon Sep 17 00:00:00 2001 From: Robin5605 Date: Tue, 4 Jun 2024 20:58:33 -0500 Subject: [PATCH 37/37] Conform to new API response model Upstream has changed their response model for the `GET /package` endpoint - this PR aims to be compliant with that new contract. --- src/bot/dragonfly_services.py | 57 +++++++++------------ src/bot/exts/audit.py | 6 +-- src/bot/exts/dragonfly/dragonfly.py | 18 +++---- src/bot/exts/dragonfly/threat_intel_feed.py | 4 +- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/src/bot/dragonfly_services.py b/src/bot/dragonfly_services.py index 5d2e6a4..259b80b 100644 --- a/src/bot/dragonfly_services.py +++ b/src/bot/dragonfly_services.py @@ -7,6 +7,7 @@ from typing import Any, Self from aiohttp import ClientSession +from pydantic import BaseModel class ScanStatus(Enum): @@ -18,41 +19,29 @@ class ScanStatus(Enum): FAILED = "failed" -@dataclass -class PackageScanResult: - """A package scan result.""" +class Package(BaseModel): + """Model representing a package queried from the database.""" - status: ScanStatus - inspector_url: str - queued_at: datetime + scan_id: str + name: str + version: str + status: ScanStatus | None + score: int | None + inspector_url: str | None + rules: list[str] = [] + download_urls: list[str] = [] + queued_at: datetime | None + queued_by: str | None + reported_at: datetime | None + reported_by: str | None pending_at: datetime | None + pending_by: str | None finished_at: datetime | None - reported_at: datetime | None - version: str - name: str - package_id: str - rules: list[str] - score: int - - @classmethod - def from_dict(cls: type[Self], data: dict) -> Self: # type: ignore[type-arg] - """Create a PackageScanResult from a dictionary.""" - return cls( - status=ScanStatus(data["status"]), - inspector_url=data["inspector_url"], - queued_at=datetime.fromisoformat(data["queued_at"]), - pending_at=datetime.fromisoformat(p) if (p := data["pending_at"]) else None, - finished_at=datetime.fromisoformat(p) if (p := data["finished_at"]) else None, - reported_at=datetime.fromisoformat(p) if (p := data["reported_at"]) else None, - version=data["version"], - name=data["name"], - package_id=data["scan_id"], - rules=[d["name"] for d in data["rules"]], - score=int(data["score"]), - ) - - def __str__(self: Self) -> str: - """Return a string representation of the package scan result.""" + finished_by: str | None + commit_hash: str | None + + def __str__(self) -> str: + """Return package name and version.""" return f"{self.name} {self.version}" @@ -146,7 +135,7 @@ async def get_scanned_packages( name: str | None = None, version: str | None = None, since: datetime | None = None, - ) -> list[PackageScanResult]: + ) -> list[Package]: """Get a list of scanned packages.""" params = {} if name: @@ -159,7 +148,7 @@ async def get_scanned_packages( params["since"] = int(since.timestamp()) # type: ignore[assignment] data = await self.make_request("GET", "/package", params=params) - return [PackageScanResult.from_dict(dct) for dct in data] + return list(map(Package.model_validate, data)) async def report_package( self: Self, diff --git a/src/bot/exts/audit.py b/src/bot/exts/audit.py index 39f83f9..a47b635 100644 --- a/src/bot/exts/audit.py +++ b/src/bot/exts/audit.py @@ -10,7 +10,7 @@ from discord.ext import commands from bot.bot import Bot -from bot.dragonfly_services import PackageScanResult +from bot.dragonfly_services import Package class PaginatorView(ui.View): @@ -20,7 +20,7 @@ def __init__( self: Self, *, member: discord.Member | discord.User, - packages: list[PackageScanResult], + packages: list[Package], per: int = 15, ) -> None: """Initialize the paginator view.""" @@ -70,7 +70,7 @@ async def interaction_check(self: Self, interaction: discord.Interaction) -> boo await interaction.response.send_message("This paginator is not for you!", ephemeral=True) return False - def _build_embed(self: Self, packages: list[PackageScanResult], page: int, total: int) -> discord.Embed: + def _build_embed(self: Self, packages: list[Package], page: int, total: int) -> discord.Embed: """Build an embed for the given packages.""" embed = discord.Embed( title="Package Audit", diff --git a/src/bot/exts/dragonfly/dragonfly.py b/src/bot/exts/dragonfly/dragonfly.py index 6921478..ad9152c 100644 --- a/src/bot/exts/dragonfly/dragonfly.py +++ b/src/bot/exts/dragonfly/dragonfly.py @@ -13,7 +13,7 @@ from bot.bot import Bot from bot.constants import Channels, DragonflyConfig, Roles -from bot.dragonfly_services import DragonflyServices, PackageReport, PackageScanResult +from bot.dragonfly_services import DragonflyServices, Package, PackageReport log = getLogger(__name__) log.setLevel(logging.INFO) @@ -108,7 +108,7 @@ class ConfirmEmailReportModal(discord.ui.Modal): style=discord.TextStyle.short, ) - def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: + def __init__(self: Self, *, package: Package, bot: Bot) -> None: """Initialize the modal.""" self.package = package self.bot = bot @@ -164,7 +164,7 @@ class ConfirmReportModal(discord.ui.Modal): style=discord.TextStyle.short, ) - def __init__(self: Self, *, package: PackageScanResult, bot: Bot) -> None: + def __init__(self: Self, *, package: Package, bot: Bot) -> None: """Initialize the modal.""" self.package = package self.bot = bot @@ -245,7 +245,7 @@ def disable_all(self: Self) -> None: class ReportView(discord.ui.View): """Report view.""" - def __init__(self: Self, bot: Bot, payload: PackageScanResult) -> None: + def __init__(self: Self, bot: Bot, payload: Package) -> None: self.bot = bot self.payload = payload super().__init__(timeout=None) @@ -342,7 +342,7 @@ def __init__( self: Self, embed: discord.Embed, bot: Bot, - payload: PackageScanResult, + payload: Package, ) -> None: self.embed = embed self.bot = bot @@ -456,9 +456,9 @@ async def on_error( raise error -def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord.Embed: +def _build_package_scan_result_embed(scan_result: Package) -> discord.Embed: """Build the embed that shows the results of a package scan.""" - condition = scan_result.score >= DragonflyConfig.threshold + condition = (scan_result.score or 0) >= DragonflyConfig.threshold title, color = ("Malicious", 0xF70606) if condition else ("Benign", 0x4CBB17) embed = discord.Embed( @@ -483,7 +483,7 @@ def _build_package_scan_result_embed(scan_result: PackageScanResult) -> discord. def _build_package_scan_result_triage_embed( - scan_result: PackageScanResult, + scan_result: Package, ) -> discord.Embed: """Build the embed for the malware triage system.""" embed = discord.Embed( @@ -503,7 +503,7 @@ def _build_package_scan_result_triage_embed( return embed -def _build_all_packages_scanned_embed(scan_results: list[PackageScanResult]) -> discord.Embed: +def _build_all_packages_scanned_embed(scan_results: list[Package]) -> discord.Embed: """Build the embed that shows a list of all packages scanned.""" if scan_results: description = "\n".join(map(str, scan_results)) diff --git a/src/bot/exts/dragonfly/threat_intel_feed.py b/src/bot/exts/dragonfly/threat_intel_feed.py index 4f90cea..9f44d7f 100644 --- a/src/bot/exts/dragonfly/threat_intel_feed.py +++ b/src/bot/exts/dragonfly/threat_intel_feed.py @@ -14,7 +14,7 @@ from bot import constants from bot.bot import Bot -from bot.dragonfly_services import PackageScanResult +from bot.dragonfly_services import Package log = getLogger(__name__) log.setLevel(logging.INFO) @@ -53,7 +53,7 @@ def search(d: dict, key: Any) -> Any | None: # noqa: ANN401 - we can't know the return None -def build_embed(package: PackageScanResult, path: str, inspector_url: str) -> discord.Embed: +def build_embed(package: Package, path: str, inspector_url: str) -> discord.Embed: """Return the embed to be sent in the threat intelligence feed.""" if package.reported_at: ts = discord.utils.format_dt(package.reported_at, style="F")