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:
Linting:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4

View file

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

View file

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

View file

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

View file

@ -99,7 +99,8 @@ class Mail(commands.Cog):
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 += "".join(random.choice("!@#$%^&*") for _ in range(4))
return password
@ -161,7 +162,8 @@ class Mail(commands.Cog):
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:
if "msg" in item:
msg = item["msg"]
@ -173,8 +175,8 @@ class Mail(commands.Cog):
return None
@staticmethod
async def _send_dm(
self,
interaction: discord.Interaction,
member: discord.Member,
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.",
"Linux's 1.0 release was in March 1994.",
"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.",
"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.",

View file

@ -14,33 +14,57 @@ from tux.ui.embeds import EmbedCreator
class ImgEffect(commands.Cog):
def __init__(self, bot: Tux) -> None:
self.bot = bot
self.allowed_mimetypes = [
"image/jpeg",
"image/png",
]
self.allowed_mimetypes = ["image/jpeg", "image/png"]
imgeffect = app_commands.Group(name="imgeffect", description="Image effects")
@imgeffect.command(
name="deepfry",
description="Deepfry an image",
)
@imgeffect.command(name="deepfry", description="Deepfry an image")
async def deepfry(self, interaction: discord.Interaction, image: discord.Attachment) -> None:
"""
Deepfry an image.
if not self.is_valid_image(image):
await self.send_invalid_image_response(interaction)
return
Parameters
----------
interaction : discord.Interaction
The interaction object for the command.
image : discord.File
The image to deepfry.
"""
await interaction.response.defer(ephemeral=True)
# 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}")
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.")
embed = EmbedCreator.create_embed(
@ -53,47 +77,27 @@ class ImgEffect(commands.Cog):
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# say that the image is being processed
logger.info("Processing image...")
await interaction.response.defer(ephemeral=True)
async def send_error_response(self, interaction: discord.Interaction) -> None:
logger.error("Error processing the image.")
# open url with PIL
logger.info("Opening image with PIL and HTTPX...")
async with httpx.AsyncClient() as client:
response = await client.get(image.url)
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="Error",
description="An error occurred while processing the image.",
)
pil_image = Image.open(io.BytesIO(response.content))
pil_image = pil_image.convert("RGB")
logger.info("Image opened with PIL.")
await interaction.response.send_message(embed=embed, ephemeral=True)
# resize image to 25% then back to original size
logger.info("Resizing image...")
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)))
@staticmethod
async def send_deepfried_image(interaction: discord.Interaction, deepfried_image: Image.Image) -> None:
arr = io.BytesIO()
pil_image.save(arr, format="JPEG", quality=1)
deepfried_image.save(arr, format="JPEG", quality=1)
arr.seek(0)
file = discord.File(arr, filename="deepfried.jpg")
await interaction.followup.send(file=file, ephemeral=True)

View file

@ -57,7 +57,13 @@ class Random(commands.Cog):
aliases=["eightball", "8b"],
)
@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.

View file

@ -65,7 +65,7 @@ class Info(commands.Cog):
custom_color=discord.Color.blurple(),
custom_author_text="Server Information",
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="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="Voice Channels", value=len(guild.voice_channels))
.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="Roles", value=len(guild.roles))
.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)
for chunk in chunks:
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)
buttons = [
@ -229,7 +229,8 @@ class Info(commands.Cog):
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.

View file

@ -100,8 +100,8 @@ class ModerationCogBase(commands.Cog):
if isinstance(log_channel, discord.TextChannel):
await log_channel.send(embed=embed)
@staticmethod
async def send_dm(
self,
ctx: commands.Context[Tux],
silent: bool,
user: discord.Member,
@ -250,3 +250,31 @@ class ModerationCogBase(commands.Cog):
await self.send_embed(ctx, embed, log_type="mod")
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()
@staticmethod
def _create_case_fields(
self,
moderator: discord.Member,
user: discord.Member | discord.User,
reason: str,
@ -361,7 +361,8 @@ class Cases(ModerationCogBase):
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 ""
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:
action = None
if case_type in [
if case_type in {
CaseType.BAN,
CaseType.KICK,
CaseType.TIMEOUT,
CaseType.WARN,
CaseType.JAIL,
CaseType.SNIPPETBAN,
]:
}:
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"
if action is not None:
@ -410,8 +411,8 @@ class Cases(ModerationCogBase):
return self.bot.get_emoji(emoji_id)
return None
@staticmethod
def _get_case_description(
self,
case: Case,
case_status_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")
await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent)
@staticmethod
def _get_manageable_roles(
self,
member: discord.Member,
jail_role: 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(
name="slowmode",
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()
@checks.has_pl(2)
@ -83,11 +84,12 @@ class Slowmode(commands.Cog):
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
@staticmethod
async def _get_slowmode(
self,
ctx: commands.Context[Tux],
channel: discord.TextChannel | discord.Thread,
) -> None:
@ -135,7 +137,8 @@ class Slowmode(commands.Cog):
await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True)
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:
if delay.endswith("s"):
delay = delay[:-1]

View file

@ -52,7 +52,7 @@ class Bookmarks(commands.Cog):
message: discord.Message,
) -> discord.Embed:
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(
bot=self.bot,
@ -71,8 +71,8 @@ class Bookmarks(commands.Cog):
return embed
@staticmethod
async def _send_bookmark(
self,
user: discord.User,
message: discord.Message,
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 loguru import logger
from prisma.enums import CaseType
from tux.bot import Tux
from tux.database.controllers import CaseController
from tux.ui.embeds import EmbedCreator
# 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):
def __init__(self, bot: Tux) -> None:
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
async def on_message(self, message: discord.Message) -> None:
@ -40,7 +71,6 @@ class Poll(commands.Cog):
@commands.Cog.listener()
async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User) -> None:
# Block any reactions that are not numbers for the poll
if reaction.message.embeds:
embed = reaction.message.embeds[0]
if (
@ -64,7 +94,12 @@ class Poll(commands.Cog):
The title of the poll.
options : str
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
options_list = options.split(",")
@ -72,6 +107,17 @@ class Poll(commands.Cog):
# Remove any leading or trailing whitespaces from the options
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
if len(options_list) < 2 or len(options_list) > 9:
embed = EmbedCreator.create_embed(

View file

@ -1,10 +1,9 @@
import asyncio
import contextlib
import datetime
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext import commands, tasks
from loguru import logger
from prisma.models import Reminder
@ -14,40 +13,26 @@ from tux.ui.embeds import EmbedCreator
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):
def __init__(self, bot: Tux) -> None:
self.bot = bot
self.db = DatabaseController().reminder
self.bot.loop.create_task(self.update())
self.check_reminders.start()
async def send_reminders(self, reminder: Reminder) -> None:
"""
Send the reminder to the user.
@tasks.loop(seconds=120)
async def check_reminders(self):
reminders = await self.db.get_unsent_reminders()
Parameters
----------
reminder : Reminder
The reminder object.
"""
try:
for reminder in reminders:
await self.send_reminder(reminder)
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)
if user is not None:
@ -64,103 +49,37 @@ class RemindMe(commands.Cog):
await user.send(embed=embed)
except discord.Forbidden:
# Send a message in the channel if the user has DMs closed
channel: discord.abc.GuildChannel | discord.Thread | discord.abc.PrivateChannel | None = (
self.bot.get_channel(reminder.reminder_channel_id)
)
channel = self.bot.get_channel(reminder.reminder_channel_id)
if channel is not None and isinstance(
channel,
discord.TextChannel | discord.Thread | discord.VoiceChannel,
):
if isinstance(channel, discord.TextChannel | discord.Thread | discord.VoiceChannel):
with contextlib.suppress(discord.Forbidden):
await channel.send(
content=f"{user.mention} Failed to DM you, sending in channel",
embed=embed,
)
return
else:
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:
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
await self.db.delete_reminder_by_id(reminder.reminder_id)
# 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))
@check_reminders.before_loop
async def before_check_reminders(self):
await self.bot.wait_until_ready()
@app_commands.command(
name="remindme",
description="Reminds you after a certain amount of time.",
)
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)
# Check if the time is valid (this is set to 0 if the time is invalid via convert_to_seconds)
if seconds == 0:
await interaction.response.send_message(
"Invalid time format. Please use the format `[number][M/w/d/h/m/s]`.",
@ -169,13 +88,13 @@ class RemindMe(commands.Cog):
)
return
seconds = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)
expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=seconds)
try:
await self.db.insert_reminder(
reminder_user_id=interaction.user.id,
reminder_content=reminder,
reminder_expires_at=seconds,
reminder_expires_at=expires_at,
reminder_channel_id=interaction.channel_id or 0,
guild_id=interaction.guild_id or 0,
)
@ -186,12 +105,13 @@ class RemindMe(commands.Cog):
user_name=interaction.user.name,
user_display_avatar=interaction.user.display_avatar.url,
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(
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:
@ -207,9 +127,6 @@ class RemindMe(commands.Cog):
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:
await bot.add_cog(RemindMe(bot))

View file

@ -46,7 +46,8 @@ class Run(commands.Cog):
self.run.usage = generate_usage(self.run)
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.
@ -63,7 +64,8 @@ class Run(commands.Cog):
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.
@ -277,7 +279,7 @@ class Run(commands.Cog):
code,
)
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
await self.send_embedded_reply(
ctx,
@ -340,7 +342,7 @@ class Run(commands.Cog):
user_name=ctx.author.name,
user_display_avatar=ctx.author.display_avatar.url,
title="Supported Languages",
description=f"```{', '.join(compiler_map.keys())}```",
description=f"```{", ".join(compiler_map.keys())}```",
)
await ctx.send(embed=embed)

View file

@ -152,7 +152,7 @@ class Snippets(commands.Cog):
text = "```\n"
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 += "```"
# 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="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,
)
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):
with contextlib.suppress(discord.Forbidden):
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?**
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")
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.

View file

@ -120,7 +120,8 @@ class LevelsController:
return False
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)
@ -190,12 +191,12 @@ class LevelsController:
if await self.is_blacklisted(user_id, guild_id):
return
multiplier = 1.0
multiplier = 1
for role in member.roles:
if role.id in self.xp_multipliers:
multiplier = max(multiplier, self.xp_multipliers[role.id])
multiplier = max(multiplier, self.xp_multipliers[role.id]) if role.id in self.xp_multipliers else 1
xp_increment = 1 * multiplier
await db.levels.update(
where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}},
data={
@ -233,10 +234,9 @@ class LevelsController:
The guild where the member is located.
"""
try:
level = await self.calculate_level(user_id, guild_id, member, guild)
await db.levels.update(
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:
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)
current_user_level = await self.get_level(user_id, guild_id)
required_xp = math.ceil(500 * ((current_user_level + 1) / 5) ** self.levels_exponent)
if user_xp < required_xp:
return current_user_level
if user_xp >= required_xp:
new_user_level = current_user_level + 1
new_user_level = int((user_xp / 500) ** (1 / self.levels_exponent) * 5)
if new_user_level != current_user_level:
await db.levels.update(
where={"user_id_guild_id": {"user_id": user_id, "guild_id": guild_id}},
data={"level": new_user_level},
)
await self.update_roles(member, guild, 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:
"""

View file

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import UTC, datetime
from prisma.models import Guild, Reminder
from tux.database.client import db
@ -21,6 +21,10 @@ class ReminderController:
async def get_reminder_by_id(self, reminder_id: int) -> Reminder | None:
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(
self,
reminder_user_id: int,
@ -38,6 +42,7 @@ class ReminderController:
"reminder_expires_at": reminder_expires_at,
"reminder_channel_id": reminder_channel_id,
"guild_id": guild_id,
"reminder_sent": False,
},
)
@ -53,3 +58,19 @@ class ReminderController:
where={"reminder_id": reminder_id},
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.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 = [
{"type": discord.ActivityType.watching, "name": "{member_count} members"},
{"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)
def log_error_traceback(self, error: Exception) -> None:
@staticmethod
def log_error_traceback(error: Exception) -> None:
"""
Log the error traceback.

View file

@ -20,7 +20,8 @@ class EventHandler(commands.Cog):
async def on_guild_remove(self, guild: discord.Guild) -> None:
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:
return

View file

@ -38,7 +38,8 @@ class TuxHelp(commands.HelpCommand):
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.
@ -61,8 +62,8 @@ class TuxHelp(commands.HelpCommand):
color=CONST.EMBED_COLORS["DEFAULT"],
)
@staticmethod
def _add_command_field(
self,
embed: discord.Embed,
command: commands.Command[Any, Any, Any],
prefix: str,
@ -84,11 +85,12 @@ class TuxHelp(commands.HelpCommand):
embed.add_field(
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,
)
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.
@ -111,7 +113,8 @@ class TuxHelp(commands.HelpCommand):
case _:
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.
@ -158,11 +161,11 @@ class TuxHelp(commands.HelpCommand):
flag_str = self._format_flag_name(flag)
if flag.aliases:
flag_str += f" ({', '.join(flag.aliases)})"
flag_str += f" ({", ".join(flag.aliases)})"
# else:
# 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:
flag_str += f"\n\tDefault: {flag.default}"
@ -289,12 +292,13 @@ class TuxHelp(commands.HelpCommand):
for command in mapping_commands:
cmd_name_and_aliases = f"`{command.name}`"
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
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.
@ -349,8 +353,8 @@ class TuxHelp(commands.HelpCommand):
return select_options
@staticmethod
def _add_navigation_and_selection(
self,
menu: ViewMenu,
select_options: dict[discord.SelectOption, list[Page]],
) -> None:
@ -368,7 +372,8 @@ class TuxHelp(commands.HelpCommand):
menu.add_select(ViewSelect(title="Command Categories", options=select_options))
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.
@ -424,7 +429,7 @@ class TuxHelp(commands.HelpCommand):
embed = self._embed_base(
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)
@ -450,12 +455,12 @@ class TuxHelp(commands.HelpCommand):
embed.add_field(
name="Usage",
value=f"`{prefix}{command.usage or 'No usage.'}`",
value=f"`{prefix}{command.usage or "No usage."}`",
inline=False,
)
embed.add_field(
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,
)
@ -471,7 +476,7 @@ class TuxHelp(commands.HelpCommand):
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)
for command in group.commands:

View file

@ -6,6 +6,8 @@ from typing import Final
import yaml
from dotenv import load_dotenv, set_key
from tux.utils.functions import convert_dict_str_to_int
load_dotenv(verbose=True)
config_file = Path("config/settings.yml")
@ -77,6 +79,13 @@ class Constants:
# Icon constants
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_MAX_NAME_LENGTH = 256
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 {}
for param_name, param in parameters.items():
if param_name in ["ctx", "flags"]:
if param_name in {"ctx", "flags"}:
continue
is_required = param.default == inspect.Parameter.empty
matching_string = get_matching_string(param_name)
@ -60,7 +60,7 @@ def generate_usage(
usage += f" {flag}"
if optional_flags:
usage += f" [{' | '.join(optional_flags)}]"
usage += f" [{" | ".join(optional_flags)}]"
return usage
@ -309,3 +309,33 @@ class SnippetUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter
aliases=["s", "quiet"],
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
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
@ -300,3 +301,19 @@ def extract_member_attrs(member: discord.Member) -> dict[str, Any]:
"status": member.status,
"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