1
Fork 0
mirror of https://github.com/allthingslinux/tux.git synced 2024-10-02 16:43:12 +00:00

Compare commits

...

86 commits

Author SHA1 Message Date
Atmois
9fbb1c7fa6
Merge branch 'main' into levels 2024-09-30 21:44:29 +01:00
pre-commit-ci[bot]
b0f124dc37 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-30 20:42:31 +00:00
Atmois
739fa6ce66
Update levels.py 2024-09-30 21:42:23 +01:00
pre-commit-ci[bot]
c2b3fa9886 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-30 20:38:58 +00:00
Atmois
08289c21ba
fix indent error 2024-09-30 21:38:01 +01:00
Atmois
8de2801a40
Fix formatter for ruff 2024-09-30 21:35:52 +01:00
Atmois
e61858d8a9
fix xpset not updating level when xp is decreased 2024-09-30 21:29:23 +01:00
Atmois
374c932ff5
fix bug with setting xp 2024-09-30 21:00:42 +01:00
Atmois
4109b21757
Fix level assignment after xpset used 2024-09-30 20:04:23 +01:00
pre-commit-ci[bot]
aa57978d6c [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-30 17:35:23 +00:00
Atmois
c73c10d05c
Ensure that there is always a multiplier set 2024-09-30 18:35:05 +01:00
Atmois
ffc3f19719
Fix bug with times 2024-09-30 18:33:58 +01:00
electron271
6f1524de75
Merge pull request #581 from allthingslinux/gif_limiter
Fixed a critical bug in the GIF ratelimiter cog caused by the double …
2024-09-28 23:39:59 -05:00
electron271
d9e840b2d6
Merge branch 'main' into gif_limiter 2024-09-28 23:39:46 -05:00
rm-rf-tux
b03af85516 fix: Linting and formatting via Ruff 2024-09-27 16:35:04 +00:00
rm-rf-omega
d40280c975 Fixed a critical bug in the GIF ratelimiter cog caused by the double deletion of dictionary keys due to Python's default behavior of deleting keys if an empty value is assigned to them 2024-09-27 18:31:03 +02:00
electron271
e2b4fc835d
Merge pull request #567 from allthingslinux/565-add-poll-ban
565 add poll ban
2024-09-26 21:00:21 -05:00
electron271
0b11407fdc
Merge branch 'main' into 565-add-poll-ban 2024-09-26 21:00:00 -05:00
electron271
afb937783d
finish pollban 2024-09-26 20:59:50 -05:00
electron271
83b028b5b8
note(poll.py) address note (see commit desc)
this is just part of discord rate limiting, at the end of the day the reactions are still removed
2024-09-26 20:37:00 -05:00
electron271
887ad4710f
fix(poll.py) update poll banned message for consistency and professionalism 2024-09-26 20:33:23 -05:00
kzndotsh
3e085871b0
Merge pull request #569 from allthingslinux/renovate/aiocache-0.x-lockfile 2024-09-26 20:29:01 -04:00
kzndotsh
f654499f16
Merge pull request #576 from allthingslinux/renovate/ubuntu-24.x 2024-09-26 20:27:29 -04:00
kzndotsh
3c9055c239
Merge pull request #574 from allthingslinux/renovate/ruff-0.x-lockfile 2024-09-26 20:27:15 -04:00
renovate[bot]
5097f5594e
fix(deps): update dependency aiocache to v0.12.3 2024-09-27 00:27:03 +00:00
kzndotsh
343f21e1fd
Merge pull request #573 from allthingslinux/renovate/mkdocs-material-9.x-lockfile 2024-09-26 20:26:41 -04:00
Kasen Engel
fa21fb0275
Merge branch 'main' into 565-add-poll-ban 2024-09-26 16:51:30 -05:00
renovate[bot]
b24c6bec3c
chore(deps): update dependency ubuntu to v24 2024-09-26 19:09:18 +00:00
rm-rf-tux
6568a7a077
Merge pull request #564 from allthingslinux/gif_limiter
Added a configurable GIF ratelimiter cog
2024-09-26 19:53:57 +02:00
electron271
f9824132ed
Merge branch 'main' into gif_limiter 2024-09-26 11:50:46 -05:00
electron271
4205b0ccc1
fix(settings.yml.example) small consistency fix 2024-09-26 11:50:26 -05:00
renovate[bot]
93694f2921
fix(deps): update dependency ruff to v0.6.8 2024-09-26 14:27:43 +00:00
renovate[bot]
66da128c89
chore(deps): update dependency mkdocs-material to v9.5.38 2024-09-26 10:50:22 +00:00
pre-commit-ci[bot]
a17d10bd13 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-26 10:08:44 +00:00
rm-rf-omega
fd62c38845 - [READABILITY] Reverted example config changes, and added a GIF_LIMITER category
- [READABILITY] Separated the GIF limiter message handler into several functions
- [PERFORMANCE] Convert GIF limiter settings dictionaries in constants.py instead of in the cog itself
- [BUG FIX] Added locks to prevent race conditions between the message handler and routine cleanup function
2024-09-26 12:05:46 +02:00
Kasen Engel
ddb12e59c5
Merge branch 'main' into 565-add-poll-ban 2024-09-25 05:38:18 -05:00
d4faeeab4f
Merge pull request #561 from allthingslinux/renovate/mkdocs-material-9.x-lockfile
chore(deps): update dependency mkdocs-material to v9.5.37
2024-09-25 11:39:01 +02:00
7b5d7acaa1
Merge pull request #563 from allthingslinux/renovate/ruff-0.x-lockfile
fix(deps): update dependency ruff to v0.6.7
2024-09-25 11:38:49 +02:00
2a911fbda2
Merge pull request #566 from allthingslinux/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-09-25 11:37:58 +02:00
5a450104de
Merge branch 'main' into renovate/ruff-0.x-lockfile 2024-09-25 11:37:31 +02:00
4cc5ab51cf
Merge branch 'main' into renovate/mkdocs-material-9.x-lockfile 2024-09-25 11:37:03 +02:00
75eb997729
Merge pull request #568 from allthingslinux/renovate/pyright-1.x-lockfile
fix(deps): update dependency pyright to v1.1.382
2024-09-25 11:36:37 +02:00
renovate[bot]
573efb860d
fix(deps): update dependency pyright to v1.1.382 2024-09-25 09:08:18 +00:00
renovate[bot]
f1cfa59c99
chore(deps): update dependency mkdocs-material to v9.5.37 2024-09-25 09:08:05 +00:00
Kasen Engel
6241e2199f add assertation (another) to poll.py 2024-09-23 16:46:18 -05:00
Kasen Engel
6022a1d33d add assertation to poll.py 2024-09-23 16:43:17 -05:00
Kasen Engel
7e1bb4f934 make unbanning people possible. 2024-09-23 16:38:19 -05:00
Kasen Engel
ce6515782b Finish up Pollban code and fix some bugs 2024-09-23 16:25:01 -05:00
pre-commit-ci[bot]
ee39d7bfca
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.6 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.6...v0.6.7)
2024-09-23 20:34:38 +00:00
Kasen Engel
7fe7b85f18 integrate poll banning into poll code, gonna see if this code is working 2024-09-23 17:58:27 +00:00
Kasen Engel
65466b5b25 Added the code, heavily derived off of the snippet ban code. gonna integrate with polls next. 2024-09-23 17:39:32 +00:00
Kasen Engel
1b5b8c5f48 update schema.prisma casetype 2024-09-23 17:31:11 +00:00
Kasen Engel
06dfab4304 update flags.py to include pollban/unban 2024-09-23 17:28:19 +00:00
Kasen Engel
356176560f add pollban command cog 2024-09-23 16:45:12 +00:00
electron271
5d807114f0
fix(fact.py) remove duplicate 2024-09-23 00:26:31 -05:00
rm-rf-tux
11bd345c44 fix: Linting and formatting via Ruff 2024-09-22 09:56:06 +00:00
rm-rf-omega
5f5ed7d28c Fought with Pyright for a while but the code should now not raise 30 errors 2024-09-22 11:54:30 +02:00
rm-rf-omega
538d9577f6 Pyright wants *exact* types - no inference
This causes issue when discord.py allows a channel to have 10 types
Maybe it is a skill issue, *maybe*, but I have the feeling it could be a bit easier...
2024-09-22 01:04:08 +02:00
rm-rf-omega
1ef9ee6637 Fixed GH somehow adding commit information in a source code file (WTF?) 2024-09-22 00:43:47 +02:00
rm-rf-omega
19f51c2685 Make Pyright a bit happier about my code 2024-09-22 00:39:52 +02:00
rm-rf-tux
68b7f4de7f fix: Linting and formatting via Ruff 2024-09-21 22:17:27 +00:00
rm-rf-omega
c07c086f3b Added a GIF ratelimiter cog.
Modified config example accordingly
Modified constants accordingly
2024-09-22 00:14:32 +02:00
renovate[bot]
58614ddadb
fix(deps): update dependency ruff to v0.6.7 2024-09-21 19:16:26 +00:00
kzndotsh
edb0b5b29b refactor: convert methods to static methods in various classes for improved clarity and usability 2024-09-21 01:14:32 -04:00
kzndotsh
cb135e4468 chore(.pre-commit-config.yaml): upgrade ruff-pre-commit from v0.6.5 to v0.6.6 to include latest updates and bug fixes 2024-09-21 00:40:54 -04:00
kzndotsh
7b07a2ef4a
Merge pull request #541 from allthingslinux/tess-remindme-rewrite 2024-09-21 00:39:54 -04:00
kzndotsh
6e201bafd5 fix(remindme.py): wrap reminder sending logic in try-except block to handle exceptions and prevent app crash
feat(remindme.py): add logging of errors when sending reminders fails to improve error tracking and debugging
2024-09-21 00:39:08 -04:00
kzndotsh
5b2dcc2cab
Merge branch 'main' into tess-remindme-rewrite 2024-09-21 00:18:58 -04:00
kzndotsh
c3a6bd3654
Merge pull request #547 from allthingslinux/renovate/ruff-0.x-lockfile 2024-09-21 00:18:27 -04:00
kzndotsh
9676b5a4db
Merge pull request #544 from allthingslinux/renovate/pyright-1.x-lockfile 2024-09-21 00:18:15 -04:00
kzndotsh
ea6bf27727
Merge pull request #554 from allthingslinux/renovate/mkdocs-material-9.x-lockfile 2024-09-21 00:17:41 -04:00
kzndotsh
60585bdf2b
Merge pull request #552 from allthingslinux/renovate/pydantic-2.x-lockfile 2024-09-21 00:17:31 -04:00
kzndotsh
c20d44a23b
Merge pull request #549 from allthingslinux/pre-commit-ci-update-config 2024-09-21 00:17:16 -04:00
kzndotsh
d9fb3ebb67
Merge pull request #548 from allthingslinux/renovate/githubkit-0.x-lockfile 2024-09-21 00:17:08 -04:00
renovate[bot]
b21e2f641c
fix(deps): update dependency ruff to v0.6.6 2024-09-20 07:47:55 +00:00
renovate[bot]
7ec3993429
fix(deps): update dependency pyright to v1.1.381 2024-09-18 09:35:11 +00:00
renovate[bot]
09c79b9eae
chore(deps): update dependency mkdocs-material to v9.5.35 2024-09-18 09:35:01 +00:00
renovate[bot]
f7c5d7f4c2
fix(deps): update dependency pydantic to v2.9.2 2024-09-17 16:36:36 +00:00
pre-commit-ci[bot]
c2d6a6db5c
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/gitleaks/gitleaks: v8.18.4 → v8.19.2](https://github.com/gitleaks/gitleaks/compare/v8.18.4...v8.19.2)
- [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.5)
2024-09-16 20:31:38 +00:00
renovate[bot]
5578504255
fix(deps): update dependency githubkit to v0.11.10 2024-09-16 05:00:37 +00:00
8f7c7e76a5 Update error logging for failed reminders 2024-09-10 09:52:59 -04:00
44cc360da8 Make the database fetch as light as possible, allowing a remindme every 120 seconds 2024-09-10 09:35:20 -04:00
d8300957a4 feat(database): Add more information in the command embed 2024-09-10 09:13:27 -04:00
49c71d7ba7 chore(database): Add method to retrieve unsent reminders 2024-09-10 09:03:30 -04:00
7625999cb1 core(remindme): Update the database controller 2024-09-10 09:00:28 -04:00
4218656483 Add reminder_sent attribute to the database 2024-09-10 09:00:09 -04:00
32 changed files with 748 additions and 389 deletions

View file

@ -9,7 +9,7 @@ permissions:
jobs: jobs:
Linting: Linting:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v4 uses: actions/checkout@v4

View file

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/gitleaks/gitleaks - repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4 rev: v8.19.2
hooks: hooks:
- id: gitleaks - id: gitleaks
@ -12,7 +12,7 @@ repos:
- id: check-toml - id: check-toml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4 rev: v0.6.7
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View file

@ -64,3 +64,14 @@ EMBED_ICONS:
KICK: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true" KICK: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true"
TIMEOUT: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true" TIMEOUT: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true"
WARN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true" WARN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true"
GIF_LIMITER:
RECENT_GIF_AGE: 60
GIF_LIMIT_EXCLUDE:
- 123456789012345
GIF_LIMITS_USER:
"123456789012345": 2
GIF_LIMITS_CHANNEL:
"123456789012345": 3

306
poetry.lock generated
View file

@ -2,13 +2,13 @@
[[package]] [[package]]
name = "aiocache" name = "aiocache"
version = "0.12.2" version = "0.12.3"
description = "multi backend asyncio cache" description = "multi backend asyncio cache"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"}, {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"}, {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
] ]
[package.extras] [package.extras]
@ -190,13 +190,13 @@ files = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.4.0" version = "4.5.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations" description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, {file = "anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, {file = "anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9"},
] ]
[package.dependencies] [package.dependencies]
@ -204,9 +204,9 @@ idna = ">=2.8"
sniffio = ">=1.1" sniffio = ">=1.1"
[package.extras] [package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"]
trio = ["trio (>=0.23)"] trio = ["trio (>=0.26.1)"]
[[package]] [[package]]
name = "astunparse" name = "astunparse"
@ -692,18 +692,18 @@ files = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.16.0" version = "3.16.1"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
{file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
typing = ["typing-extensions (>=4.12.2)"] typing = ["typing-extensions (>=4.12.2)"]
[[package]] [[package]]
@ -811,13 +811,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
[[package]] [[package]]
name = "githubkit" name = "githubkit"
version = "0.11.9" version = "0.11.10"
description = "GitHub SDK for Python" description = "GitHub SDK for Python"
optional = false optional = false
python-versions = "<4.0,>=3.8" python-versions = "<4.0,>=3.8"
files = [ files = [
{file = "githubkit-0.11.9-py3-none-any.whl", hash = "sha256:68933ff9867467418c90176e2ea77c7623fa7ab8fbf64723c2ca56c09b14a0f6"}, {file = "githubkit-0.11.10-py3-none-any.whl", hash = "sha256:e95074916af4a5f357d47005022a555984e16729d9e6204161ef67bc29a78789"},
{file = "githubkit-0.11.9.tar.gz", hash = "sha256:d25da1a667cce32f3d1ab0f33cf52fa328a55e85a6916e19da2765b0602c472a"}, {file = "githubkit-0.11.10.tar.gz", hash = "sha256:9729783ba44935ca47b3bebaad76971ccc3d3b548b8af54e06d16a489b0ece90"},
] ]
[package.dependencies] [package.dependencies]
@ -914,13 +914,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.0" version = "2.6.1"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
] ]
[package.extras] [package.extras]
@ -928,13 +928,13 @@ license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.9" version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "idna-3.9-py3-none-any.whl", hash = "sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e"}, {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.9.tar.gz", hash = "sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
] ]
[package.extras] [package.extras]
@ -1160,13 +1160,13 @@ pyyaml = ">=5.1"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "9.5.34" version = "9.5.38"
description = "Documentation that simply works" description = "Documentation that simply works"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mkdocs_material-9.5.34-py3-none-any.whl", hash = "sha256:54caa8be708de2b75167fd4d3b9f3d949579294f49cb242515d4653dbee9227e"}, {file = "mkdocs_material-9.5.38-py3-none-any.whl", hash = "sha256:d4779051d52ba9f1e7e344b34de95449c7c366c212b388e4a2db9a3db043c228"},
{file = "mkdocs_material-9.5.34.tar.gz", hash = "sha256:1e60ddf716cfb5679dfd65900b8a25d277064ed82d9a53cd5190e3f894df7840"}, {file = "mkdocs_material-9.5.38.tar.gz", hash = "sha256:1843c5171ad6b489550aeaf7358e5b7128cc03ddcf0fb4d91d19aa1e691a63b8"},
] ]
[package.dependencies] [package.dependencies]
@ -1446,13 +1446,13 @@ xmp = ["defusedxml"]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.3" version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"}, {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
] ]
[package.extras] [package.extras]
@ -1556,18 +1556,18 @@ files = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.9.1" version = "2.9.2"
description = "Data validation using Python type hints" description = "Data validation using Python type hints"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
{file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
] ]
[package.dependencies] [package.dependencies]
annotated-types = ">=0.6.0" annotated-types = ">=0.6.0"
pydantic-core = "2.23.3" pydantic-core = "2.23.4"
typing-extensions = [ typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""},
@ -1579,100 +1579,100 @@ timezone = ["tzdata"]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.23.3" version = "2.23.4"
description = "Core functionality for Pydantic validation and serialization" description = "Core functionality for Pydantic validation and serialization"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
{file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
{file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
{file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
{file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
{file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
{file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
{file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
{file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
{file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
{file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
{file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
{file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
{file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
{file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
{file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
{file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
{file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
{file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
{file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
{file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
{file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
{file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
{file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
{file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"}, {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
{file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"}, {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"}, {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
{file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"}, {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
{file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"}, {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
{file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"}, {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
{file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"}, {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
{file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"}, {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
{file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"}, {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"}, {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
{file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"}, {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
{file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"}, {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
{file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"}, {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
{file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"}, {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"}, {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
{file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
] ]
[package.dependencies] [package.dependencies]
@ -1758,21 +1758,23 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.380" version = "1.1.382"
description = "Command line wrapper for pyright" description = "Command line wrapper for pyright"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyright-1.1.380-py3-none-any.whl", hash = "sha256:a6404392053d8848bacc7aebcbd9d318bb46baf1a1a000359305481920f43879"}, {file = "pyright-1.1.382-py3-none-any.whl", hash = "sha256:b6658802b2eca1107a6d63b6816903da4c7a53dffebc27cdbc02ebd904b7a18e"},
{file = "pyright-1.1.380.tar.gz", hash = "sha256:e6ceb1a5f7e9f03106e0aa1d6fbb4d97735a5e7ffb59f3de6b2db590baf935b2"}, {file = "pyright-1.1.382.tar.gz", hash = "sha256:0c953837aa9f1e1d8d46772ee7ebae845104db657e9216834dbdde567a11f177"},
] ]
[package.dependencies] [package.dependencies]
nodeenv = ">=1.6.0" nodeenv = ">=1.6.0"
typing-extensions = ">=4.1"
[package.extras] [package.extras]
all = ["twine (>=3.4.1)"] all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"]
dev = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"]
nodejs = ["nodejs-wheel-binaries"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
@ -2043,29 +2045,29 @@ pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.6.5" version = "0.6.8"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
{file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
{file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
{file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
{file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
{file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
{file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
{file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
{file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
{file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
{file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
{file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
] ]
[[package]] [[package]]
@ -2218,13 +2220,13 @@ files = [
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.20240808" version = "6.0.12.20240917"
description = "Typing stubs for PyYAML" description = "Typing stubs for PyYAML"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "types-PyYAML-6.0.12.20240808.tar.gz", hash = "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af"}, {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"},
{file = "types_PyYAML-6.0.12.20240808-py3-none-any.whl", hash = "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35"}, {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"},
] ]
[[package]] [[package]]
@ -2285,13 +2287,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.26.4" version = "20.26.5"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"},
{file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"},
] ]
[package.dependencies] [package.dependencies]

View file

@ -111,6 +111,7 @@ model Reminder {
reminder_expires_at DateTime reminder_expires_at DateTime
reminder_channel_id BigInt reminder_channel_id BigInt
reminder_user_id BigInt reminder_user_id BigInt
reminder_sent Boolean @default(false)
guild_id BigInt guild_id BigInt
guild Guild @relation(fields: [guild_id], references: [guild_id]) guild Guild @relation(fields: [guild_id], references: [guild_id])
@ -182,4 +183,6 @@ enum CaseType {
UNJAIL UNJAIL
SNIPPETUNBAN SNIPPETUNBAN
UNTEMPBAN UNTEMPBAN
POLLBAN
POLLUNBAN
} }

View file

@ -99,7 +99,8 @@ class Mail(commands.Cog):
delete_after=30, delete_after=30,
) )
def _generate_password(self) -> str: @staticmethod
def _generate_password() -> str:
password = "changeme" + "".join(str(random.randint(0, 9)) for _ in range(6)) password = "changeme" + "".join(str(random.randint(0, 9)) for _ in range(6))
password += "".join(random.choice("!@#$%^&*") for _ in range(4)) password += "".join(random.choice("!@#$%^&*") for _ in range(4))
return password return password
@ -161,7 +162,8 @@ class Mail(commands.Cog):
delete_after=30, delete_after=30,
) )
def _extract_mailbox_info(self, result: list[dict[str, str | None]]) -> str | None: @staticmethod
def _extract_mailbox_info(result: list[dict[str, str | None]]) -> str | None:
for item in result: for item in result:
if "msg" in item: if "msg" in item:
msg = item["msg"] msg = item["msg"]
@ -173,8 +175,8 @@ class Mail(commands.Cog):
return None return None
@staticmethod
async def _send_dm( async def _send_dm(
self,
interaction: discord.Interaction, interaction: discord.Interaction,
member: discord.Member, member: discord.Member,
mailbox_info: str, mailbox_info: str,

View file

@ -17,7 +17,6 @@ class Fact(commands.Cog):
"Linus Torvalds was around 22 years old when he started work on the Linux Kernel in 1991. In the same year, he also released prototypes of the kernel publicly.", "Linus Torvalds was around 22 years old when he started work on the Linux Kernel in 1991. In the same year, he also released prototypes of the kernel publicly.",
"Linux's 1.0 release was in March 1994.", "Linux's 1.0 release was in March 1994.",
"Less than 1% of the latest kernel release includes code written by Linus Torvalds.", "Less than 1% of the latest kernel release includes code written by Linus Torvalds.",
"Linux is used by every major space programme in the world.",
"Approximately 13.3% of the latest Linux kernel is made up of blank lines.", "Approximately 13.3% of the latest Linux kernel is made up of blank lines.",
"Vim has various easter eggs. A notable one is found by typing :help 42 into the command bar.", "Vim has various easter eggs. A notable one is found by typing :help 42 into the command bar.",
"Slackware is the oldest active linux distribution being released on the 17th July 1993.", "Slackware is the oldest active linux distribution being released on the 17th July 1993.",

View file

@ -14,33 +14,57 @@ from tux.ui.embeds import EmbedCreator
class ImgEffect(commands.Cog): class ImgEffect(commands.Cog):
def __init__(self, bot: Tux) -> None: def __init__(self, bot: Tux) -> None:
self.bot = bot self.bot = bot
self.allowed_mimetypes = [ self.allowed_mimetypes = ["image/jpeg", "image/png"]
"image/jpeg",
"image/png",
]
imgeffect = app_commands.Group(name="imgeffect", description="Image effects") imgeffect = app_commands.Group(name="imgeffect", description="Image effects")
@imgeffect.command( @imgeffect.command(name="deepfry", description="Deepfry an image")
name="deepfry",
description="Deepfry an image",
)
async def deepfry(self, interaction: discord.Interaction, image: discord.Attachment) -> None: async def deepfry(self, interaction: discord.Interaction, image: discord.Attachment) -> None:
""" if not self.is_valid_image(image):
Deepfry an image. await self.send_invalid_image_response(interaction)
return
Parameters await interaction.response.defer(ephemeral=True)
----------
interaction : discord.Interaction
The interaction object for the command.
image : discord.File
The image to deepfry.
"""
# check if the image is a image pil_image = await self.fetch_image(image.url)
if pil_image:
deepfried_image = self.deepfry_image(pil_image)
await self.send_deepfried_image(interaction, deepfried_image)
else:
await self.send_error_response(interaction)
def is_valid_image(self, image: discord.Attachment) -> bool:
logger.info(f"Content type: {image.content_type}, Filename: {image.filename}, URL: {image.url}") logger.info(f"Content type: {image.content_type}, Filename: {image.filename}, URL: {image.url}")
if image.content_type not in self.allowed_mimetypes: return image.content_type in self.allowed_mimetypes
@staticmethod
async def fetch_image(url: str) -> Image.Image:
logger.info("Fetching image from URL with HTTPX...")
async with httpx.AsyncClient() as client:
response = await client.get(url)
return Image.open(io.BytesIO(response.content)).convert("RGB")
@staticmethod
def deepfry_image(pil_image: Image.Image) -> Image.Image:
pil_image = pil_image.resize((int(pil_image.width * 0.25), int(pil_image.height * 0.25)))
pil_image = ImageEnhance.Sharpness(pil_image).enhance(100.0)
r = pil_image.split()[0]
r = ImageEnhance.Contrast(r).enhance(2.0)
r = ImageEnhance.Brightness(r).enhance(1.5)
colours = ((254, 0, 2), (255, 255, 15))
r = ImageOps.colorize(r, colours[0], colours[1])
pil_image = Image.blend(pil_image, r, 0.75)
return pil_image.resize((int(pil_image.width * 4), int(pil_image.height * 4)))
async def send_invalid_image_response(self, interaction: discord.Interaction) -> None:
logger.error("The file is not a permitted image.") logger.error("The file is not a permitted image.")
embed = EmbedCreator.create_embed( embed = EmbedCreator.create_embed(
@ -53,47 +77,27 @@ class ImgEffect(commands.Cog):
) )
await interaction.response.send_message(embed=embed, ephemeral=True) await interaction.response.send_message(embed=embed, ephemeral=True)
return
# say that the image is being processed async def send_error_response(self, interaction: discord.Interaction) -> None:
logger.info("Processing image...") logger.error("Error processing the image.")
await interaction.response.defer(ephemeral=True)
# open url with PIL embed = EmbedCreator.create_embed(
logger.info("Opening image with PIL and HTTPX...") bot=self.bot,
async with httpx.AsyncClient() as client: embed_type=EmbedCreator.ERROR,
response = await client.get(image.url) user_name=interaction.user.name,
user_display_avatar=interaction.user.display_avatar.url,
title="Error",
description="An error occurred while processing the image.",
)
pil_image = Image.open(io.BytesIO(response.content)) await interaction.response.send_message(embed=embed, ephemeral=True)
pil_image = pil_image.convert("RGB")
logger.info("Image opened with PIL.")
# resize image to 25% then back to original size @staticmethod
logger.info("Resizing image...") async def send_deepfried_image(interaction: discord.Interaction, deepfried_image: Image.Image) -> None:
pil_image = pil_image.resize((int(pil_image.width * 0.25), int(pil_image.height * 0.25)))
logger.info("Image resized.")
# increase sharpness
logger.info("Increasing sharpness...")
pil_image = ImageEnhance.Sharpness(pil_image).enhance(100.0)
logger.info("Sharpness increased.")
logger.info("Adjusting color...")
r = pil_image.split()[0]
r = ImageEnhance.Contrast(r).enhance(2.0)
r = ImageEnhance.Brightness(r).enhance(1.5)
colours = ((254, 0, 2), (255, 255, 15))
r = ImageOps.colorize(r, colours[0], colours[1])
pil_image = Image.blend(pil_image, r, 0.75)
logger.info("Color adjustment complete.")
# send image
logger.info("Sending image...")
pil_image = pil_image.resize((int(pil_image.width * 4), int(pil_image.height * 4)))
arr = io.BytesIO() arr = io.BytesIO()
pil_image.save(arr, format="JPEG", quality=1) deepfried_image.save(arr, format="JPEG", quality=1)
arr.seek(0) arr.seek(0)
file = discord.File(arr, filename="deepfried.jpg") file = discord.File(arr, filename="deepfried.jpg")
await interaction.followup.send(file=file, ephemeral=True) await interaction.followup.send(file=file, ephemeral=True)

View file

@ -57,7 +57,13 @@ class Random(commands.Cog):
aliases=["eightball", "8b"], aliases=["eightball", "8b"],
) )
@commands.guild_only() @commands.guild_only()
async def eight_ball(self, ctx: commands.Context[Tux], *, question: str, cow: bool = False) -> None: async def eight_ball(
self,
ctx: commands.Context[Tux],
*,
question: str,
cow: bool = False,
) -> None:
""" """
Ask the magic 8ball a question. Ask the magic 8ball a question.

View file

@ -65,7 +65,7 @@ class Info(commands.Cog):
custom_color=discord.Color.blurple(), custom_color=discord.Color.blurple(),
custom_author_text="Server Information", custom_author_text="Server Information",
custom_author_icon_url=guild.icon.url, custom_author_icon_url=guild.icon.url,
custom_footer_text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}", custom_footer_text=f"ID: {guild.id} | Created: {guild.created_at.strftime("%B %d, %Y")}",
) )
.add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown") .add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown")
.add_field(name="Vanity URL", value=guild.vanity_url_code or "None") .add_field(name="Vanity URL", value=guild.vanity_url_code or "None")
@ -73,7 +73,7 @@ class Info(commands.Cog):
.add_field(name="Text Channels", value=len(guild.text_channels)) .add_field(name="Text Channels", value=len(guild.text_channels))
.add_field(name="Voice Channels", value=len(guild.voice_channels)) .add_field(name="Voice Channels", value=len(guild.voice_channels))
.add_field(name="Forum Channels", value=len(guild.forums)) .add_field(name="Forum Channels", value=len(guild.forums))
.add_field(name="Emojis", value=f"{len(guild.emojis)}/{2*guild.emoji_limit}") .add_field(name="Emojis", value=f"{len(guild.emojis)}/{2 * guild.emoji_limit}")
.add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}") .add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}")
.add_field(name="Roles", value=len(guild.roles)) .add_field(name="Roles", value=len(guild.roles))
.add_field(name="Humans", value=sum(not member.bot for member in guild.members)) .add_field(name="Humans", value=sum(not member.bot for member in guild.members))
@ -213,7 +213,7 @@ class Info(commands.Cog):
menu: ViewMenu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) menu: ViewMenu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
for chunk in chunks: for chunk in chunks:
page_embed: discord.Embed = embed.copy() page_embed: discord.Embed = embed.copy()
page_embed.description = f"{list_type.capitalize()} list for {guild_name}:\n{' '.join(chunk)}" page_embed.description = f"{list_type.capitalize()} list for {guild_name}:\n{" ".join(chunk)}"
menu.add_page(page_embed) menu.add_page(page_embed)
buttons = [ buttons = [
@ -229,7 +229,8 @@ class Info(commands.Cog):
await menu.start() await menu.start()
def _chunks(self, it: Iterator[str], size: int) -> Generator[list[str], None, None]: @staticmethod
def _chunks(it: Iterator[str], size: int) -> Generator[list[str], None, None]:
""" """
Split an iterator into chunks of a specified size. Split an iterator into chunks of a specified size.

View file

@ -100,8 +100,8 @@ class ModerationCogBase(commands.Cog):
if isinstance(log_channel, discord.TextChannel): if isinstance(log_channel, discord.TextChannel):
await log_channel.send(embed=embed) await log_channel.send(embed=embed)
@staticmethod
async def send_dm( async def send_dm(
self,
ctx: commands.Context[Tux], ctx: commands.Context[Tux],
silent: bool, silent: bool,
user: discord.Member, user: discord.Member,
@ -250,3 +250,31 @@ class ModerationCogBase(commands.Cog):
await self.send_embed(ctx, embed, log_type="mod") await self.send_embed(ctx, embed, log_type="mod")
await ctx.send(embed=embed, delete_after=30, ephemeral=True) await ctx.send(embed=embed, delete_after=30, ephemeral=True)
async def is_pollbanned(self, guild_id: int, user_id: int) -> bool:
"""
Check if a user is poll banned.
Parameters
----------
guild_id : int
The ID of the guild to check in.
user_id : int
The ID of the user to check.
Returns
-------
bool
True if the user is poll banned, False otherwise.
"""
# ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.POLLBAN)
# unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.POLLUNBAN)
ban_cases = await self.db.case.get_all_cases_by_type(guild_id, CaseType.POLLBAN)
unban_cases = await self.db.case.get_all_cases_by_type(guild_id, CaseType.POLLUNBAN)
ban_count = sum(case.case_user_id == user_id for case in ban_cases)
unban_count = sum(case.case_user_id == user_id for case in unban_cases)
return ban_count > unban_count

View file

@ -319,8 +319,8 @@ class Cases(ModerationCogBase):
await menu.start() await menu.start()
@staticmethod
def _create_case_fields( def _create_case_fields(
self,
moderator: discord.Member, moderator: discord.Member,
user: discord.Member | discord.User, user: discord.Member | discord.User,
reason: str, reason: str,
@ -361,7 +361,8 @@ class Cases(ModerationCogBase):
return embed return embed
def _format_emoji(self, emoji: discord.Emoji | None) -> str: @staticmethod
def _format_emoji(emoji: discord.Emoji | None) -> str:
return f"<:{emoji.name}:{emoji.id}>" if emoji else "" return f"<:{emoji.name}:{emoji.id}>" if emoji else ""
def _get_case_status_emoji(self, case_status: bool | None) -> discord.Emoji | None: def _get_case_status_emoji(self, case_status: bool | None) -> discord.Emoji | None:
@ -392,16 +393,16 @@ class Cases(ModerationCogBase):
def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None: def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None:
action = None action = None
if case_type in [ if case_type in {
CaseType.BAN, CaseType.BAN,
CaseType.KICK, CaseType.KICK,
CaseType.TIMEOUT, CaseType.TIMEOUT,
CaseType.WARN, CaseType.WARN,
CaseType.JAIL, CaseType.JAIL,
CaseType.SNIPPETBAN, CaseType.SNIPPETBAN,
]: }:
action = "added" action = "added"
elif case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN]: elif case_type in {CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN}:
action = "removed" action = "removed"
if action is not None: if action is not None:
@ -410,8 +411,8 @@ class Cases(ModerationCogBase):
return self.bot.get_emoji(emoji_id) return self.bot.get_emoji(emoji_id)
return None return None
@staticmethod
def _get_case_description( def _get_case_description(
self,
case: Case, case: Case,
case_status_emoji: str, case_status_emoji: str,
case_type_emoji: str, case_type_emoji: str,

View file

@ -105,8 +105,8 @@ class Jail(ModerationCogBase):
dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "jailed") dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "jailed")
await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent) await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent)
@staticmethod
def _get_manageable_roles( def _get_manageable_roles(
self,
member: discord.Member, member: discord.Member,
jail_role: discord.Role, jail_role: discord.Role,
) -> list[discord.Role]: ) -> list[discord.Role]:

View file

@ -0,0 +1,71 @@
import discord
from discord.ext import commands
from loguru import logger
from prisma.enums import CaseType
from tux.bot import Tux
from tux.database.controllers.case import CaseController
from tux.utils import checks
from tux.utils.flags import PollBanFlags, generate_usage
from . import ModerationCogBase
class PollBan(ModerationCogBase):
def __init__(self, bot: Tux) -> None:
super().__init__(bot)
self.case_controller = CaseController()
self.poll_ban.usage = generate_usage(self.poll_ban, PollBanFlags)
@commands.hybrid_command(
name="pollban",
aliases=["pb"],
)
@commands.guild_only()
@checks.has_pl(3)
async def poll_ban(
self,
ctx: commands.Context[Tux],
member: discord.Member,
*,
flags: PollBanFlags,
) -> None:
"""
Ban a user from creating polls using tux.
Parameters
----------
ctx : commands.Context[Tux]
The context object.
member : discord.Member
The member to poll ban.
flags : PollBanFlags
The flags for the command. (reason: str, silent: bool)
"""
assert ctx.guild
if await self.is_pollbanned(ctx.guild.id, member.id):
await ctx.send("User is already poll banned.", delete_after=30, ephemeral=True)
return
try:
case = await self.db.case.insert_case(
case_user_id=member.id,
case_moderator_id=ctx.author.id,
case_type=CaseType.POLLBAN,
case_reason=flags.reason,
guild_id=ctx.guild.id,
)
except Exception as e:
logger.error(f"Failed to ban {member}. {e}")
await ctx.send(f"Failed to ban {member}. {e}", delete_after=30)
return
dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "poll banned")
await self.handle_case_response(ctx, CaseType.POLLBAN, case.case_number, flags.reason, member, dm_sent)
async def setup(bot: Tux) -> None:
await bot.add_cog(PollBan(bot))

View file

@ -0,0 +1,71 @@
import discord
from discord.ext import commands
from loguru import logger
from prisma.enums import CaseType
from tux.bot import Tux
from tux.database.controllers.case import CaseController
from tux.utils import checks
from tux.utils.flags import PollUnbanFlags, generate_usage
from . import ModerationCogBase
class PollUnban(ModerationCogBase):
def __init__(self, bot: Tux) -> None:
super().__init__(bot)
self.case_controller = CaseController()
self.poll_unban.usage = generate_usage(self.poll_unban, PollUnbanFlags)
@commands.hybrid_command(
name="pollunban",
aliases=["pub"],
)
@commands.guild_only()
@checks.has_pl(3)
async def poll_unban(
self,
ctx: commands.Context[Tux],
member: discord.Member,
*,
flags: PollUnbanFlags,
):
"""
Unban a user from creating snippets.
Parameters
----------
ctx : commands.Context[Tux]
The context object.
member : discord.Member
The member to snippet unban.
flags : PollUnbanFlags
The flags for the command. (reason: str, silent: bool)
"""
assert ctx.guild
if not await self.is_pollbanned(ctx.guild.id, member.id):
await ctx.send("User is not poll banned.", delete_after=30, ephemeral=True)
return
try:
case = await self.db.case.insert_case(
case_user_id=member.id,
case_moderator_id=ctx.author.id,
case_type=CaseType.POLLUNBAN,
case_reason=flags.reason,
guild_id=ctx.guild.id,
)
except Exception as e:
logger.error(f"Failed to poll unban {member}. {e}")
await ctx.send(f"Failed to poll unban {member}. {e}", delete_after=30, ephemeral=True)
return
dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "poll unbanned")
await self.handle_case_response(ctx, CaseType.POLLUNBAN, case.case_number, flags.reason, member, dm_sent)
async def setup(bot: Tux) -> None:
await bot.add_cog(PollUnban(bot))

View file

@ -15,7 +15,8 @@ class Slowmode(commands.Cog):
@commands.hybrid_command( @commands.hybrid_command(
name="slowmode", name="slowmode",
aliases=["sm"], aliases=["sm"],
usage="slowmode <delay|get> [channel]\nor slowmode [channel] <delay|get>", # only place where generate_usage shouldn't be used # only place where generate_usage shouldn't be used:
usage="slowmode <delay|get> [channel]\nor slowmode [channel] <delay|get>",
) )
@commands.guild_only() @commands.guild_only()
@checks.has_pl(2) @checks.has_pl(2)
@ -83,11 +84,12 @@ class Slowmode(commands.Cog):
return action, channel return action, channel
def _get_channel(self, ctx: commands.Context[Tux]) -> discord.TextChannel | discord.Thread | None: @staticmethod
def _get_channel(ctx: commands.Context[Tux]) -> discord.TextChannel | discord.Thread | None:
return ctx.channel if isinstance(ctx.channel, discord.TextChannel | discord.Thread) else None return ctx.channel if isinstance(ctx.channel, discord.TextChannel | discord.Thread) else None
@staticmethod
async def _get_slowmode( async def _get_slowmode(
self,
ctx: commands.Context[Tux], ctx: commands.Context[Tux],
channel: discord.TextChannel | discord.Thread, channel: discord.TextChannel | discord.Thread,
) -> None: ) -> None:
@ -135,7 +137,8 @@ class Slowmode(commands.Cog):
await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True) await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True)
logger.error(f"Failed to set slowmode. Error: {error}") logger.error(f"Failed to set slowmode. Error: {error}")
def _parse_delay(self, delay: str) -> int | None: @staticmethod
def _parse_delay(delay: str) -> int | None:
try: try:
if delay.endswith("s"): if delay.endswith("s"):
delay = delay[:-1] delay = delay[:-1]

View file

@ -52,7 +52,7 @@ class Bookmarks(commands.Cog):
message: discord.Message, message: discord.Message,
) -> discord.Embed: ) -> discord.Embed:
if len(message.content) > CONST.EMBED_MAX_DESC_LENGTH: if len(message.content) > CONST.EMBED_MAX_DESC_LENGTH:
message.content = f"{message.content[:CONST.EMBED_MAX_DESC_LENGTH - 3]}..." message.content = f"{message.content[: CONST.EMBED_MAX_DESC_LENGTH - 3]}..."
embed = EmbedCreator.create_embed( embed = EmbedCreator.create_embed(
bot=self.bot, bot=self.bot,
@ -71,8 +71,8 @@ class Bookmarks(commands.Cog):
return embed return embed
@staticmethod
async def _send_bookmark( async def _send_bookmark(
self,
user: discord.User, user: discord.User,
message: discord.Message, message: discord.Message,
embed: discord.Embed, embed: discord.Embed,

View file

@ -0,0 +1,108 @@
import asyncio
from collections import defaultdict
from time import time
import discord
from discord.ext import commands, tasks
from tux.bot import Tux
from tux.utils.constants import CONST
class GifLimiter(commands.Cog):
"""
This class is a handler for GIF ratelimiting.
It keeps a list of GIF send times and routinely removes old times.
It will prevent people from posting GIFs if the quotas are exceeded.
"""
def __init__(self, bot: Tux) -> None:
self.bot = bot
# Max age for a GIF to be considered a recent post
self.recent_gif_age: int = CONST.RECENT_GIF_AGE
# Max number of GIFs sent recently in a channel
self.channelwide_gif_limits: dict[int, int] = CONST.GIF_LIMITS_CHANNEL
# Max number of GIFs sent recently by a user to be able to post one in specified channels
self.user_gif_limits: dict[int, int] = CONST.GIF_LIMITS
# list of channels in which not to count GIFs
self.gif_limit_exclude: list[int] = CONST.GIF_LIMIT_EXCLUDE
# Timestamps for recently-sent GIFs for the server, and channels
# UID, list of timestamps
self.recent_gifs_by_user: defaultdict[int, list[int]] = defaultdict(list)
# Channel ID, list of timestamps
self.recent_gifs_by_channel: defaultdict[int, list[int]] = defaultdict(list)
# Lock to prevent race conditions
self.gif_lock = asyncio.Lock()
self.old_gif_remover.start()
async def _should_process_message(self, message: discord.Message) -> bool:
"""Checks if a message contains a GIF and was not sent in a blacklisted channel"""
return not (
len(message.embeds) == 0
or "gif" not in message.content.lower()
or message.channel.id in self.gif_limit_exclude
)
async def _handle_gif_message(self, message: discord.Message) -> None:
"""Checks for ratelimit infringements"""
async with self.gif_lock:
channel: int = message.channel.id
user: int = message.author.id
if (
channel in self.channelwide_gif_limits
and len(self.recent_gifs_by_channel[channel]) >= self.channelwide_gif_limits[channel]
):
await self._delete_message(message, "for channel")
return
if channel in self.user_gif_limits and len(self.recent_gifs_by_user[user]) >= self.user_gif_limits[channel]:
await self._delete_message(message, "for user")
return
# Add message to recent GIFs if it doesn't infringe on ratelimits
current_time: int = int(time())
self.recent_gifs_by_channel[channel].append(current_time)
self.recent_gifs_by_user[user].append(current_time)
async def _delete_message(self, message: discord.Message, epilogue: str) -> None:
"""
Deletes the message passed as an argument, and sends a self-deleting message with the reason
"""
await message.delete()
await message.channel.send(f"-# GIF ratelimit exceeded {epilogue}", delete_after=3)
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
"""Checks for GIFs in every sent message"""
if await self._should_process_message(message):
await self._handle_gif_message(message)
@tasks.loop(seconds=20)
async def old_gif_remover(self) -> None:
"""Regularly cleans old GIF timestamps"""
current_time: int = int(time())
async with self.gif_lock:
for channel_id, timestamps in list(self.recent_gifs_by_channel.items()):
self.recent_gifs_by_channel[channel_id] = [
t for t in timestamps if current_time - t < self.recent_gif_age
]
for user_id, timestamps in list(self.recent_gifs_by_user.items()):
filtered_timestamps = [t for t in timestamps if current_time - t < self.recent_gif_age]
if filtered_timestamps:
self.recent_gifs_by_user[user_id] = filtered_timestamps
else:
del self.recent_gifs_by_user[user_id]
async def setup(bot: Tux) -> None:
await bot.add_cog(GifLimiter(bot))

View file

@ -3,7 +3,9 @@ from discord import app_commands
from discord.ext import commands from discord.ext import commands
from loguru import logger from loguru import logger
from prisma.enums import CaseType
from tux.bot import Tux from tux.bot import Tux
from tux.database.controllers import CaseController
from tux.ui.embeds import EmbedCreator from tux.ui.embeds import EmbedCreator
# TODO: Create option inputs for the poll command instead of using a comma separated string # TODO: Create option inputs for the poll command instead of using a comma separated string
@ -12,6 +14,35 @@ from tux.ui.embeds import EmbedCreator
class Poll(commands.Cog): class Poll(commands.Cog):
def __init__(self, bot: Tux) -> None: def __init__(self, bot: Tux) -> None:
self.bot = bot self.bot = bot
self.case_controller = CaseController()
# TODO: for the moment this is duplicated code from ModerationCogBase in a attempt to get the code out sooner
async def is_pollbanned(self, guild_id: int, user_id: int) -> bool:
"""
Check if a user is poll banned.
Parameters
----------
guild_id : int
The ID of the guild to check in.
user_id : int
The ID of the user to check.
Returns
-------
bool
True if the user is poll banned, False otherwise.
"""
ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.POLLBAN)
unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.POLLUNBAN)
ban_count = sum(case.case_user_id == user_id for case in ban_cases)
unban_count = sum(case.case_user_id == user_id for case in unban_cases)
return (
ban_count > unban_count
) # TODO: this implementation is flawed, if someone bans and unbans the same user multiple times, this will not work as expected
@commands.Cog.listener() # listen for messages @commands.Cog.listener() # listen for messages
async def on_message(self, message: discord.Message) -> None: async def on_message(self, message: discord.Message) -> None:
@ -40,7 +71,6 @@ class Poll(commands.Cog):
@commands.Cog.listener() @commands.Cog.listener()
async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User) -> None: async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User) -> None:
# Block any reactions that are not numbers for the poll # Block any reactions that are not numbers for the poll
if reaction.message.embeds: if reaction.message.embeds:
embed = reaction.message.embeds[0] embed = reaction.message.embeds[0]
if ( if (
@ -64,7 +94,12 @@ class Poll(commands.Cog):
The title of the poll. The title of the poll.
options : str options : str
The options for the poll, separated by commas. The options for the poll, separated by commas.
""" """
if interaction.guild_id is None:
await interaction.response.send_message("This command can only be used in a server.", ephemeral=True)
return
# Split the options by comma # Split the options by comma
options_list = options.split(",") options_list = options.split(",")
@ -72,6 +107,17 @@ class Poll(commands.Cog):
# Remove any leading or trailing whitespaces from the options # Remove any leading or trailing whitespaces from the options
options_list = [option.strip() for option in options_list] options_list = [option.strip() for option in options_list]
if await self.is_pollbanned(interaction.guild_id, interaction.user.id):
embed = EmbedCreator.create_embed(
bot=self.bot,
embed_type=EmbedCreator.ERROR,
user_name=interaction.user.name,
user_display_avatar=interaction.user.display_avatar.url,
title="Poll Banned",
description="You are poll banned and cannot create a poll.",
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Check if the options count is between 2-9 # Check if the options count is between 2-9
if len(options_list) < 2 or len(options_list) > 9: if len(options_list) < 2 or len(options_list) > 9:
embed = EmbedCreator.create_embed( embed = EmbedCreator.create_embed(

View file

@ -1,10 +1,9 @@
import asyncio
import contextlib import contextlib
import datetime import datetime
import discord import discord
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands, tasks
from loguru import logger from loguru import logger
from prisma.models import Reminder from prisma.models import Reminder
@ -14,40 +13,26 @@ from tux.ui.embeds import EmbedCreator
from tux.utils.functions import convert_to_seconds from tux.utils.functions import convert_to_seconds
def get_closest_reminder(reminders: list[Reminder]) -> Reminder | None:
"""
Check if there are any reminders and return the closest one.
Parameters
----------
reminders : list[Reminder]
A list of reminders to check.
Returns
-------
Reminder | None
The closest reminder or None if there are no reminders.
"""
return min(reminders, key=lambda x: x.reminder_expires_at) if reminders else None
class RemindMe(commands.Cog): class RemindMe(commands.Cog):
def __init__(self, bot: Tux) -> None: def __init__(self, bot: Tux) -> None:
self.bot = bot self.bot = bot
self.db = DatabaseController().reminder self.db = DatabaseController().reminder
self.bot.loop.create_task(self.update()) self.check_reminders.start()
async def send_reminders(self, reminder: Reminder) -> None: @tasks.loop(seconds=120)
""" async def check_reminders(self):
Send the reminder to the user. reminders = await self.db.get_unsent_reminders()
Parameters try:
---------- for reminder in reminders:
reminder : Reminder await self.send_reminder(reminder)
The reminder object. await self.db.update_reminder_status(reminder.reminder_id, sent=True)
""" logger.debug(f'Status of reminder {reminder.reminder_id} updated to "sent".')
except Exception as e:
logger.error(f"Error sending reminders: {e}")
async def send_reminder(self, reminder: Reminder) -> None:
user = self.bot.get_user(reminder.reminder_user_id) user = self.bot.get_user(reminder.reminder_user_id)
if user is not None: if user is not None:
@ -64,103 +49,37 @@ class RemindMe(commands.Cog):
await user.send(embed=embed) await user.send(embed=embed)
except discord.Forbidden: except discord.Forbidden:
# Send a message in the channel if the user has DMs closed channel = self.bot.get_channel(reminder.reminder_channel_id)
channel: discord.abc.GuildChannel | discord.Thread | discord.abc.PrivateChannel | None = (
self.bot.get_channel(reminder.reminder_channel_id)
)
if channel is not None and isinstance( if isinstance(channel, discord.TextChannel | discord.Thread | discord.VoiceChannel):
channel,
discord.TextChannel | discord.Thread | discord.VoiceChannel,
):
with contextlib.suppress(discord.Forbidden): with contextlib.suppress(discord.Forbidden):
await channel.send( await channel.send(
content=f"{user.mention} Failed to DM you, sending in channel", content=f"{user.mention} Failed to DM you, sending in channel",
embed=embed, embed=embed,
) )
return
else: else:
logger.error( logger.error(
f"Failed to send reminder to {user.id}, DMs closed and channel not found.", f"Failed to send reminder {reminder.reminder_id}, DMs closed and channel not found.",
) )
else: else:
logger.error(f"Failed to send reminder to {reminder.reminder_user_id}, user not found.") logger.error(
f"Failed to send reminder {reminder.reminder_id}, user with ID {reminder.reminder_user_id} not found.",
)
# Delete the reminder after sending @check_reminders.before_loop
await self.db.delete_reminder_by_id(reminder.reminder_id) async def before_check_reminders(self):
await self.bot.wait_until_ready()
# wait for a second so that the reminder is deleted before checking for more reminders
# who knows if this works, it seems to
await asyncio.sleep(1)
# Run update again to check if there are any more reminders
await self.update()
async def end_timer(self, reminder: Reminder) -> None:
"""
End the timer for the reminder.
Parameters
----------
reminder : Reminder
The reminder object.
"""
# Wait until the reminder expires
await discord.utils.sleep_until(reminder.reminder_expires_at)
await self.send_reminders(reminder)
async def update(self) -> None:
"""
Update the reminders
Check if there are any reminders and send the closest one.
"""
try:
# Get all reminders
reminders = await self.db.get_all_reminders()
# Get the closest reminder
closest_reminder = get_closest_reminder(reminders)
except Exception as e:
logger.error(f"Error getting reminders: {e}")
return
# If there are no reminders, return
if closest_reminder is None:
return
# Check if it's expired
if closest_reminder.reminder_expires_at < datetime.datetime.now(datetime.UTC):
await self.send_reminders(closest_reminder)
return
# Create a task to wait until the reminder expires
self.bot.loop.create_task(self.end_timer(closest_reminder))
@app_commands.command( @app_commands.command(
name="remindme", name="remindme",
description="Reminds you after a certain amount of time.", description="Reminds you after a certain amount of time.",
) )
async def remindme(self, interaction: discord.Interaction, time: str, *, reminder: str) -> None: async def remindme(self, interaction: discord.Interaction, time: str, *, reminder: str) -> None:
"""
Set a reminder for a certain amount of time.
Parameters
----------
interaction : discord.Interaction
The discord interaction object.
time : str
Time in the format `[number][M/w/d/h/m/s]`.
reminder : str
Reminder content.
"""
seconds = convert_to_seconds(time) seconds = convert_to_seconds(time)
# Check if the time is valid (this is set to 0 if the time is invalid via convert_to_seconds)
if seconds == 0: if seconds == 0:
await interaction.response.send_message( await interaction.response.send_message(
"Invalid time format. Please use the format `[number][M/w/d/h/m/s]`.", "Invalid time format. Please use the format `[number][M/w/d/h/m/s]`.",
@ -169,13 +88,13 @@ class RemindMe(commands.Cog):
) )
return return
seconds = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds) expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)
try: try:
await self.db.insert_reminder( await self.db.insert_reminder(
reminder_user_id=interaction.user.id, reminder_user_id=interaction.user.id,
reminder_content=reminder, reminder_content=reminder,
reminder_expires_at=seconds, reminder_expires_at=expires_at,
reminder_channel_id=interaction.channel_id or 0, reminder_channel_id=interaction.channel_id or 0,
guild_id=interaction.guild_id or 0, guild_id=interaction.guild_id or 0,
) )
@ -186,12 +105,13 @@ class RemindMe(commands.Cog):
user_name=interaction.user.name, user_name=interaction.user.name,
user_display_avatar=interaction.user.display_avatar.url, user_display_avatar=interaction.user.display_avatar.url,
title="Reminder Set", title="Reminder Set",
description=f"Reminder set for <t:{int(seconds.timestamp())}:f>.", description=f"Reminder set for <t:{int(expires_at.timestamp())}:f>.",
) )
embed.add_field( embed.add_field(
name="Note", name="Note",
value="If you have DMs closed, the reminder may not reach you. We will attempt to send it in this channel instead, however it is not guaranteed.", value="- If you have DMs closed, we will attempt to send it in this channel instead.\n"
"- The reminder may be delayed by up to 120 seconds due to the way Tux works.",
) )
except Exception as e: except Exception as e:
@ -207,9 +127,6 @@ class RemindMe(commands.Cog):
await interaction.response.send_message(embed=embed, ephemeral=True) await interaction.response.send_message(embed=embed, ephemeral=True)
# Run update again to check if this reminder is the closest
await self.update()
async def setup(bot: Tux) -> None: async def setup(bot: Tux) -> None:
await bot.add_cog(RemindMe(bot)) await bot.add_cog(RemindMe(bot))

View file

@ -46,7 +46,8 @@ class Run(commands.Cog):
self.run.usage = generate_usage(self.run) self.run.usage = generate_usage(self.run)
self.languages.usage = generate_usage(self.languages) self.languages.usage = generate_usage(self.languages)
def remove_ansi(self, ansi: str) -> str: @staticmethod
def remove_ansi(ansi: str) -> str:
""" """
Converts ANSI encoded text into non-ANSI. Converts ANSI encoded text into non-ANSI.
@ -63,7 +64,8 @@ class Run(commands.Cog):
return ansi_re.sub("", ansi) return ansi_re.sub("", ansi)
def remove_backticks(self, st: str) -> str: @staticmethod
def remove_backticks(st: str) -> str:
""" """
Removes backticks from the provided string. Removes backticks from the provided string.
@ -277,7 +279,7 @@ class Run(commands.Cog):
code, code,
) )
await msg.delete() await msg.delete()
if filtered_output == "" and gen_one == "" and normalized_lang == "": if not filtered_output and not gen_one and not normalized_lang:
return return
await self.send_embedded_reply( await self.send_embedded_reply(
ctx, ctx,
@ -340,7 +342,7 @@ class Run(commands.Cog):
user_name=ctx.author.name, user_name=ctx.author.name,
user_display_avatar=ctx.author.display_avatar.url, user_display_avatar=ctx.author.display_avatar.url,
title="Supported Languages", title="Supported Languages",
description=f"```{', '.join(compiler_map.keys())}```", description=f"```{", ".join(compiler_map.keys())}```",
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)

View file

@ -152,7 +152,7 @@ class Snippets(commands.Cog):
text = "```\n" text = "```\n"
for i, snippet in enumerate(snippets[:10]): for i, snippet in enumerate(snippets[:10]):
text += f"{i+1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n" text += f"{i + 1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n"
text += "```" text += "```"
# only show top 10, no pagination # only show top 10, no pagination
@ -321,7 +321,7 @@ class Snippets(commands.Cog):
embed.add_field(name="Name", value=snippet.snippet_name, inline=False) embed.add_field(name="Name", value=snippet.snippet_name, inline=False)
embed.add_field( embed.add_field(
name="Author", name="Author",
value=f"{author.mention if author else f'<@!{snippet.snippet_user_id}>'}", value=f"{author.mention if author else f"<@!{snippet.snippet_user_id}>"}",
inline=False, inline=False,
) )
embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False) embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False)
@ -495,7 +495,7 @@ class Snippets(commands.Cog):
if author := self.bot.get_user(snippet.snippet_user_id): if author := self.bot.get_user(snippet.snippet_user_id):
with contextlib.suppress(discord.Forbidden): with contextlib.suppress(discord.Forbidden):
await author.send( await author.send(
f"""Your snippet `{snippet.snippet_name}` has been {'locked' if status.locked else 'unlocked'}. f"""Your snippet `{snippet.snippet_name}` has been {"locked" if status.locked else "unlocked"}.
**What does this mean?** **What does this mean?**
If a snippet is locked, it cannot be edited by anyone other than moderators. This means that you can no longer edit this snippet. If a snippet is locked, it cannot be edited by anyone other than moderators. This means that you can no longer edit this snippet.

View file

@ -135,7 +135,8 @@ class Tldr(commands.Cog):
return self._run_subprocess(["tldr", "--list"], "No TLDR pages found.").split("\n") return self._run_subprocess(["tldr", "--list"], "No TLDR pages found.").split("\n")
def _run_subprocess(self, command_list: list[str], default_response: str) -> str: @staticmethod
def _run_subprocess(command_list: list[str], default_response: str) -> str:
""" """
Helper method to run subprocesses for CLI interactions. Helper method to run subprocesses for CLI interactions.

View file

@ -120,7 +120,8 @@ class LevelsController:
return False return False
last_message_naive = last_message_time.last_message.replace(tzinfo=None) last_message_naive = last_message_time.last_message.replace(tzinfo=None)
time_between_messages = datetime.datetime.fromtimestamp(time.time(), tz=datetime.UTC) - last_message_naive last_message_aware = last_message_naive.replace(tzinfo=datetime.UTC)
time_between_messages = datetime.datetime.fromtimestamp(time.time(), tz=datetime.UTC) - last_message_aware
cooldown_period = datetime.timedelta(seconds=self.xp_cooldown) cooldown_period = datetime.timedelta(seconds=self.xp_cooldown)
@ -190,12 +191,12 @@ class LevelsController:
if await self.is_blacklisted(user_id, guild_id): if await self.is_blacklisted(user_id, guild_id):
return return
multiplier = 1.0 multiplier = 1
for role in member.roles: for role in member.roles:
if role.id in self.xp_multipliers: multiplier = max(multiplier, self.xp_multipliers[role.id]) if role.id in self.xp_multipliers else 1
multiplier = max(multiplier, self.xp_multipliers[role.id])
xp_increment = 1 * multiplier xp_increment = 1 * multiplier
await db.levels.update( await db.levels.update(
where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}}, where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}},
data={ data={
@ -233,10 +234,9 @@ class LevelsController:
The guild where the member is located. The guild where the member is located.
""" """
try: try:
level = await self.calculate_level(user_id, guild_id, member, guild)
await db.levels.update( await db.levels.update(
where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}}, where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}},
data={"xp": xp_amount, "level": level}, data={"xp": xp_amount},
) )
except Exception as e: except Exception as e:
logger.error(f"Error setting XP for user_id: {user_id}, guild_id: {guild_id}: {e}") logger.error(f"Error setting XP for user_id: {user_id}, guild_id: {guild_id}: {e}")
@ -301,18 +301,16 @@ class LevelsController:
user_xp = await self.get_xp(user_id, guild_id) user_xp = await self.get_xp(user_id, guild_id)
current_user_level = await self.get_level(user_id, guild_id) current_user_level = await self.get_level(user_id, guild_id)
required_xp = math.ceil(500 * ((current_user_level + 1) / 5) ** self.levels_exponent) new_user_level = int((user_xp / 500) ** (1 / self.levels_exponent) * 5)
if user_xp < required_xp:
return current_user_level if new_user_level != current_user_level:
if user_xp >= required_xp:
new_user_level = current_user_level + 1
await db.levels.update( await db.levels.update(
where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}}, where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}},
data={"level": new_user_level}, data={"level": new_user_level},
) )
await self.update_roles(member, guild, new_user_level) await self.update_roles(member, guild, new_user_level)
return new_user_level return new_user_level
return 0
async def update_roles(self, member: discord.Member, guild: discord.Guild, new_user_level: int) -> None: async def update_roles(self, member: discord.Member, guild: discord.Guild, new_user_level: int) -> None:
""" """

View file

@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from prisma.models import Guild, Reminder from prisma.models import Guild, Reminder
from tux.database.client import db from tux.database.client import db
@ -21,6 +21,10 @@ class ReminderController:
async def get_reminder_by_id(self, reminder_id: int) -> Reminder | None: async def get_reminder_by_id(self, reminder_id: int) -> Reminder | None:
return await self.table.find_first(where={"reminder_id": reminder_id}) return await self.table.find_first(where={"reminder_id": reminder_id})
async def get_unsent_reminders(self) -> list[Reminder]:
now = datetime.now(UTC)
return await self.table.find_many(where={"reminder_sent": False, "reminder_expires_at": {"lte": now}})
async def insert_reminder( async def insert_reminder(
self, self,
reminder_user_id: int, reminder_user_id: int,
@ -38,6 +42,7 @@ class ReminderController:
"reminder_expires_at": reminder_expires_at, "reminder_expires_at": reminder_expires_at,
"reminder_channel_id": reminder_channel_id, "reminder_channel_id": reminder_channel_id,
"guild_id": guild_id, "guild_id": guild_id,
"reminder_sent": False,
}, },
) )
@ -53,3 +58,19 @@ class ReminderController:
where={"reminder_id": reminder_id}, where={"reminder_id": reminder_id},
data={"reminder_content": reminder_content}, data={"reminder_content": reminder_content},
) )
async def update_reminder_status(self, reminder_id: int, sent: bool = True) -> None:
"""
Update the status of a reminder. This sets the value "reminder_sent" to True by default.
Parameters
----------
reminder_id : int
The ID of the reminder to update.
sent : bool
The new status of the reminder.
"""
await self.table.update(
where={"reminder_id": reminder_id},
data={"reminder_sent": sent},
)

View file

@ -13,7 +13,8 @@ class ActivityHandler(commands.Cog):
self.delay = delay self.delay = delay
self.activities = self.build_activity_list() self.activities = self.build_activity_list()
def build_activity_list(self) -> list[discord.Activity | discord.Streaming]: @staticmethod
def build_activity_list() -> list[discord.Activity | discord.Streaming]:
activity_data = [ activity_data = [
{"type": discord.ActivityType.watching, "name": "{member_count} members"}, {"type": discord.ActivityType.watching, "name": "{member_count} members"},
{"type": discord.ActivityType.watching, "name": "All Things Linux"}, {"type": discord.ActivityType.watching, "name": "All Things Linux"},

View file

@ -273,7 +273,8 @@ class ErrorHandler(commands.Cog):
return error_map.get(type(error), self.error_message).format(error=error) return error_map.get(type(error), self.error_message).format(error=error)
def log_error_traceback(self, error: Exception) -> None: @staticmethod
def log_error_traceback(error: Exception) -> None:
""" """
Log the error traceback. Log the error traceback.

View file

@ -20,7 +20,8 @@ class EventHandler(commands.Cog):
async def on_guild_remove(self, guild: discord.Guild) -> None: async def on_guild_remove(self, guild: discord.Guild) -> None:
await self.db.guild.delete_guild_by_id(guild.id) await self.db.guild.delete_guild_by_id(guild.id)
async def handle_harmful_message(self, message: discord.Message) -> None: @staticmethod
async def handle_harmful_message(message: discord.Message) -> None:
if message.author.bot: if message.author.bot:
return return

View file

@ -38,7 +38,8 @@ class TuxHelp(commands.HelpCommand):
return self.context.clean_prefix or CONST.DEFAULT_PREFIX return self.context.clean_prefix or CONST.DEFAULT_PREFIX
def _embed_base(self, title: str, description: str | None = None) -> discord.Embed: @staticmethod
def _embed_base(title: str, description: str | None = None) -> discord.Embed:
""" """
Creates a base embed with uniform styling. Creates a base embed with uniform styling.
@ -61,8 +62,8 @@ class TuxHelp(commands.HelpCommand):
color=CONST.EMBED_COLORS["DEFAULT"], color=CONST.EMBED_COLORS["DEFAULT"],
) )
@staticmethod
def _add_command_field( def _add_command_field(
self,
embed: discord.Embed, embed: discord.Embed,
command: commands.Command[Any, Any, Any], command: commands.Command[Any, Any, Any],
prefix: str, prefix: str,
@ -84,11 +85,12 @@ class TuxHelp(commands.HelpCommand):
embed.add_field( embed.add_field(
name=f"{prefix}{command.qualified_name} ({command_aliases})", name=f"{prefix}{command.qualified_name} ({command_aliases})",
value=f"> {command.short_doc or 'No documentation summary.'}", value=f"> {command.short_doc or "No documentation summary."}",
inline=False, inline=False,
) )
def _get_flag_type(self, flag_annotation: Any) -> str: @staticmethod
def _get_flag_type(flag_annotation: Any) -> str:
""" """
Determines the type of a flag based on its annotation. Determines the type of a flag based on its annotation.
@ -111,7 +113,8 @@ class TuxHelp(commands.HelpCommand):
case _: case _:
return str(flag_annotation) return str(flag_annotation)
def _format_flag_name(self, flag: commands.Flag) -> str: @staticmethod
def _format_flag_name(flag: commands.Flag) -> str:
""" """
Formats the flag name based on whether it is required. Formats the flag name based on whether it is required.
@ -158,11 +161,11 @@ class TuxHelp(commands.HelpCommand):
flag_str = self._format_flag_name(flag) flag_str = self._format_flag_name(flag)
if flag.aliases: if flag.aliases:
flag_str += f" ({', '.join(flag.aliases)})" flag_str += f" ({", ".join(flag.aliases)})"
# else: # else:
# flag_str += f" : {flag_type}" # flag_str += f" : {flag_type}"
flag_str += f"\n\t{flag.description or 'No description provided.'}" flag_str += f"\n\t{flag.description or "No description provided."}"
if flag.default is not discord.utils.MISSING: if flag.default is not discord.utils.MISSING:
flag_str += f"\n\tDefault: {flag.default}" flag_str += f"\n\tDefault: {flag.default}"
@ -289,12 +292,13 @@ class TuxHelp(commands.HelpCommand):
for command in mapping_commands: for command in mapping_commands:
cmd_name_and_aliases = f"`{command.name}`" cmd_name_and_aliases = f"`{command.name}`"
if command.aliases: if command.aliases:
cmd_name_and_aliases += f" ({', '.join(f'`{alias}`' for alias in command.aliases)})" cmd_name_and_aliases += f" ({", ".join(f"`{alias}`" for alias in command.aliases)})"
command_categories[cog_group][command.name] = cmd_name_and_aliases command_categories[cog_group][command.name] = cmd_name_and_aliases
return command_categories return command_categories
def _get_cog_groups(self) -> list[str]: @staticmethod
def _get_cog_groups() -> list[str]:
""" """
Retrieves a list of cog groups from the 'cogs' folder. Retrieves a list of cog groups from the 'cogs' folder.
@ -349,8 +353,8 @@ class TuxHelp(commands.HelpCommand):
return select_options return select_options
@staticmethod
def _add_navigation_and_selection( def _add_navigation_and_selection(
self,
menu: ViewMenu, menu: ViewMenu,
select_options: dict[discord.SelectOption, list[Page]], select_options: dict[discord.SelectOption, list[Page]],
) -> None: ) -> None:
@ -368,7 +372,8 @@ class TuxHelp(commands.HelpCommand):
menu.add_select(ViewSelect(title="Command Categories", options=select_options)) menu.add_select(ViewSelect(title="Command Categories", options=select_options))
menu.add_button(ViewButton.end_session()) menu.add_button(ViewButton.end_session())
def _extract_cog_group(self, cog: commands.Cog) -> str | None: @staticmethod
def _extract_cog_group(cog: commands.Cog) -> str | None:
""" """
Extracts the cog group from a cog's string representation. Extracts the cog group from a cog's string representation.
@ -424,7 +429,7 @@ class TuxHelp(commands.HelpCommand):
embed = self._embed_base( embed = self._embed_base(
title=f"{prefix}{command.qualified_name}", title=f"{prefix}{command.qualified_name}",
description=f"> {command.help or 'No documentation available.'}", description=f"> {command.help or "No documentation available."}",
) )
await self._add_command_help_fields(embed, command) await self._add_command_help_fields(embed, command)
@ -450,12 +455,12 @@ class TuxHelp(commands.HelpCommand):
embed.add_field( embed.add_field(
name="Usage", name="Usage",
value=f"`{prefix}{command.usage or 'No usage.'}`", value=f"`{prefix}{command.usage or "No usage."}`",
inline=False, inline=False,
) )
embed.add_field( embed.add_field(
name="Aliases", name="Aliases",
value=(f"`{', '.join(command.aliases)}`" if command.aliases else "No aliases."), value=(f"`{", ".join(command.aliases)}`" if command.aliases else "No aliases."),
inline=False, inline=False,
) )
@ -471,7 +476,7 @@ class TuxHelp(commands.HelpCommand):
prefix = await self._get_prefix() prefix = await self._get_prefix()
embed = self._embed_base(f"{group.name}", f"> {group.help or 'No documentation available.'}") embed = self._embed_base(f"{group.name}", f"> {group.help or "No documentation available."}")
await self._add_command_help_fields(embed, group) await self._add_command_help_fields(embed, group)
for command in group.commands: for command in group.commands:

View file

@ -6,6 +6,8 @@ from typing import Final
import yaml import yaml
from dotenv import load_dotenv, set_key from dotenv import load_dotenv, set_key
from tux.utils.functions import convert_dict_str_to_int
load_dotenv(verbose=True) load_dotenv(verbose=True)
config_file = Path("config/settings.yml") config_file = Path("config/settings.yml")
@ -77,6 +79,13 @@ class Constants:
# Icon constants # Icon constants
EMBED_ICONS: Final[dict[str, str]] = config["EMBED_ICONS"] EMBED_ICONS: Final[dict[str, str]] = config["EMBED_ICONS"]
# GIF ratelimit constants
RECENT_GIF_AGE: Final[int] = config["GIF_LIMITER"]["RECENT_GIF_AGE"]
GIF_LIMIT_EXCLUDE: Final[list[int]] = config["GIF_LIMITER"]["GIF_LIMIT_EXCLUDE"]
GIF_LIMITS: Final[dict[int, int]] = convert_dict_str_to_int(config["GIF_LIMITER"]["GIF_LIMITS_USER"])
GIF_LIMITS_CHANNEL: Final[dict[int, int]] = convert_dict_str_to_int(config["GIF_LIMITER"]["GIF_LIMITS_CHANNEL"])
# Embed limit constants # Embed limit constants
EMBED_MAX_NAME_LENGTH = 256 EMBED_MAX_NAME_LENGTH = 256
EMBED_MAX_DESC_LENGTH = 4096 EMBED_MAX_DESC_LENGTH = 4096

View file

@ -37,7 +37,7 @@ def generate_usage(
flags: dict[str, commands.Flag] = flag_converter.get_flags() if flag_converter else {} flags: dict[str, commands.Flag] = flag_converter.get_flags() if flag_converter else {}
for param_name, param in parameters.items(): for param_name, param in parameters.items():
if param_name in ["ctx", "flags"]: if param_name in {"ctx", "flags"}:
continue continue
is_required = param.default == inspect.Parameter.empty is_required = param.default == inspect.Parameter.empty
matching_string = get_matching_string(param_name) matching_string = get_matching_string(param_name)
@ -60,7 +60,7 @@ def generate_usage(
usage += f" {flag}" usage += f" {flag}"
if optional_flags: if optional_flags:
usage += f" [{' | '.join(optional_flags)}]" usage += f" [{" | ".join(optional_flags)}]"
return usage return usage
@ -309,3 +309,33 @@ class SnippetUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter
aliases=["s", "quiet"], aliases=["s", "quiet"],
default=False, default=False,
) )
class PollBanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
reason: str = commands.flag(
name="reason",
description="Reason for the poll ban.",
aliases=["r"],
default=MISSING,
)
silent: bool = commands.flag(
name="silent",
description="Do not send a DM to the target.",
aliases=["s", "quiet"],
default=False,
)
class PollUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
reason: str = commands.flag(
name="reason",
description="Reason for the poll unban",
aliases=["r"],
default=MISSING,
)
silent: bool = commands.flag(
name="silent",
description="Do not send a DM to the target.",
aliases=["s", "quiet"],
default=False,
)

View file

@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import discord import discord
from loguru import logger
harmful_command_pattern = r"(?:sudo\s+|doas\s+|run0\s+)?rm\s+(-[frR]*|--force|--recursive|--no-preserve-root|\s+)*([/\~]\s*|\*|/bin|/boot|/etc|/lib|/proc|/root|/sbin|/sys|/tmp|/usr|/var|/var/log|/network.|/system)(\s+--no-preserve-root|\s+\*)*|:\(\)\{ :|:& \};:" # noqa: RUF001 harmful_command_pattern = r"(?:sudo\s+|doas\s+|run0\s+)?rm\s+(-[frR]*|--force|--recursive|--no-preserve-root|\s+)*([/\~]\s*|\*|/bin|/boot|/etc|/lib|/proc|/root|/sbin|/sys|/tmp|/usr|/var|/var/log|/network.|/system)(\s+--no-preserve-root|\s+\*)*|:\(\)\{ :|:& \};:" # noqa: RUF001
@ -300,3 +301,19 @@ def extract_member_attrs(member: discord.Member) -> dict[str, Any]:
"status": member.status, "status": member.status,
"activity": member.activity, "activity": member.activity,
} }
def convert_dict_str_to_int(original_dict: dict[str, int]) -> dict[int, int]:
"""Helper function used for GIF Limiter constants.
Required as YAML keys are str. Channel and user IDs are int."""
converted_dict: dict[int, int] = {}
for key, value in original_dict.items():
try:
int_key: int = int(key)
converted_dict[int_key] = value
except ValueError:
logger.exception(f"An error occurred when loading the GIF ratelimiter configuration at key {key}")
return converted_dict