Compare commits

..

382 commits

Author SHA1 Message Date
374a083528
fix(aurora): fix some ruff errors
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 43s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2025-01-25 19:45:04 +00:00
3d4c438f37
chore(vscode): change formatOnSave
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 46s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2025-01-24 23:35:53 +00:00
062b788ef0
chore(vscode): set -vvv in the launch and debug options
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 45s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2025-01-24 23:31:53 +00:00
fb9a208dd4
chore(repo): add vscode run and debug configuration
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 49s
2025-01-24 17:20:47 -06:00
f5b591c31e
chore(deps): update www.coastalcommits.com/cswimr/actions:docs docker digest to e405cd6 2025-01-24 17:20:47 -06:00
335882befd
chore(devcontainer): change volume name 2025-01-24 17:20:47 -06:00
8aaa5bd33e
chore(repo): update red-discordbot and add pip 2025-01-24 17:20:47 -06:00
afc6735e90
chore(deps): pin dependencies 2025-01-24 17:20:47 -06:00
aa34b36608
switch to devcontainers 2025-01-24 17:20:47 -06:00
65c35a422c
chore(deps): update www.coastalcommits.com/cswimr/actions:uv docker digest to 211aaf7 2025-01-24 17:20:47 -06:00
9637f5209f
style(repo): format nix flake 2025-01-24 17:20:47 -06:00
39d286b559
chore(deps): update www.coastalcommits.com/cswimr/actions:uv docker digest to 95493fb 2025-01-24 17:20:47 -06:00
ca78a197b9
chore(repo): update renovate config 2025-01-24 17:20:47 -06:00
719e040f2e
chore(deps): pin dependencies 2025-01-24 17:20:47 -06:00
6e0b13bb20
Update renovate.json 2025-01-24 17:20:47 -06:00
1a78475bc6
chore(deps): update actions/checkout action to v4 2025-01-24 17:20:47 -06:00
470b4d86d7
Add renovate.json 2025-01-24 17:20:47 -06:00
4d79320b39
chore(repo): flake formatting 2025-01-24 17:20:47 -06:00
ec08130f99
chore(repo): use nixpkgs-python to get python version instead of setting languages.python.package 2025-01-24 17:20:47 -06:00
523a3a1e89
chore(repo): add nix flake dev-shell for development environment 2025-01-24 17:20:47 -06:00
90e04cb70a
chore(repo): update issue templates 2025-01-24 17:20:47 -06:00
9fb796d7f2
remove poetry file and update workflow file to use correct meli secret 2025-01-24 17:20:47 -06:00
2ecc2cd318
chore(repo): add uv.lock file 2025-01-24 17:20:47 -06:00
d2c4b19610
feat(workflow): move to uv 2025-01-24 17:20:47 -06:00
6060c78056
(repo): switch to uv
Signed-off-by: cswimr <seaswimmerthefsh@gmail.com>
2025-01-24 17:20:47 -06:00
25a70b925e
docs(pterodactyl): link to the pelican-eggs repository instead of some random fork
Signed-off-by: cswimr <seaswimmerthefsh@gmail.com>
2025-01-24 17:20:31 -06:00
03c43710e0
chore(poetry): update deps 2025-01-24 17:20:31 -06:00
14fe976a0d
fix(repo): update all instances of SeaswimmerTheFsh to cswimr 2025-01-24 23:18:06 +00:00
f3f99209da
fix(backup): update for red 3.5.13 2025-01-24 23:17:21 +00:00
2c7b0f5441
feat(repo): updated all cogs (except aurora) to use the help formatting present in the indev version of aurora 2025-01-24 23:17:21 +00:00
eb331faf55
fix(pterodactyl): fixed a missing argument in one of the websocket events 2025-01-24 23:17:21 +00:00
3e4efa9220
fix(actions): updated container tags 2025-01-24 23:17:21 +00:00
b80dedbda2
fix(pterodactyl): fixed join and leave listeners throwing errors 2025-01-24 23:17:21 +00:00
872e1aabff
fix(pterodactyl): don't depend on a website to host images when i can bundle image files in the cog itself 2025-01-24 23:17:21 +00:00
bab80d6344
chore(pterodactyl): clean up some dirty code 2025-01-24 23:17:21 +00:00
73d3449894
fix(aurora): fixed two pydantic validation errors
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 1m3s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 1m11s
Signed-off-by: cswimr <seaswimmerthefsh@gmail.com>
2024-09-25 22:57:57 -04:00
24005800fa
fix(aurora): actually fixed the thing i just tried to fix
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 1m2s
Signed-off-by: cswimr <seaswimmerthefsh@gmail.com>
2024-09-25 22:55:33 -04:00
d92a0d27da
fix(aurora): fix /case breaking if a case is resolved but the resolving user doesn't exist anymore
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 3s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 55s
Signed-off-by: cswimr <seaswimmerthefsh@gmail.com>
2024-09-25 22:51:45 -04:00
97406e7bac
feat(aurora): updated phx-class-registry to ^5.0.0 and fixed some typos
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 54s
2024-09-20 14:49:28 -04:00
405729f37d
force downgrade of phx-class-registry
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m6s
Signed-off-by: seaswimmer <seaswimmerthefsh@gmail.com>
2024-09-20 10:38:49 -04:00
1c941462df
feat(aurora): log how many errors occured in the handle_expiry task
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 39s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 48s
2024-09-08 12:47:01 -04:00
b91946abeb
fix(aurora): catch exceptions thrown inside of the expiry handler instead of just stopping the task
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-09-08 12:40:38 -04:00
d4fe97f247
fix(aurora): fixed retrieving a User object instead of a Member object
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-09-08 12:33:16 -04:00
9e21879a49
fix(aurora): fixed note's handler using cls.string where it should have been using cls.verb
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 50s
2024-08-24 19:21:49 -04:00
d600a10729
fix(aurora): don't try and use the attribute of a Member object when the object is a User object
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 51s
2024-08-24 19:13:01 -04:00
5934506c8a
fix(aurora): make silent an optional argument in Aurora.moderate()
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 55s
2024-08-23 14:54:06 -04:00
5459392d7e
fix(aurora): update the autologger method to use the new type registry
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 54s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
2024-08-23 14:43:43 -04:00
6560f98aef
feat(aurora): add [p]aurora info command
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 50s
2024-08-22 18:47:17 -04:00
fa8036291c
fix(aurora): bump version and change author key
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-08-22 17:54:36 -04:00
fadb3e1a9d
fix(aurora): make the moderation_id key in the moderation tables not nullable (NOT NULL)
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-08-22 17:52:50 -04:00
bc3ea67e1e
fix(aurora): pylint fixes
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 53s
2024-08-22 16:04:37 -04:00
43e82c9eb5
feat(aurora): minify export json to reduce file sizes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 52s
2024-08-22 15:59:23 -04:00
320db1b692
fix(aurora): make sure that bans with durations are imported as tempbans
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 51s
2024-08-22 15:44:22 -04:00
fa27d12de5
feat(aurora): make all models compatible with repr
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 51s
2024-08-22 15:33:36 -04:00
794ce56040
misc(aurora): changed a docstring
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 51s
2024-08-22 12:33:30 -04:00
064784c9d7
fix(aurora): fixed the Note moderation type handler using cls.string instead of cls.verb
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 54s
2024-08-22 12:26:42 -04:00
101d364241
misc(aurora): remove a useless logging call
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 52s
2024-08-21 17:21:54 -04:00
cd3d3c7733
fix(aurora): make sure the bot key does not exist in the Change.from_dict() data dictionary
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 54s
2024-08-21 15:16:40 -04:00
4d2004ed93
fix(aurora): fix changes not being imported 2024-08-21 15:16:10 -04:00
a3ad38f338
misc(aurora): improved Change.from_dict() 2024-08-21 15:15:50 -04:00
64758686bb
misc(aurora): simplified AuroraGuildModel a bit
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 52s
2024-08-21 14:49:47 -04:00
fce4001152
fix(aurora): hopefully fixed changes not being imported 2024-08-21 14:48:56 -04:00
90be42769c
Merge branch 'main' into aurora-pydantic
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 52s
2024-08-21 14:37:16 -04:00
8479dcdd48
fix(aurora): fixed a bug in the Aurora importer that prevented new imports from being imported
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 4s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
2024-08-21 14:15:25 -04:00
797793b970
feat(aurora): add _obj to the partial models
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:57:26 -04:00
5d22e67864
fix(aurora): don't mutate a list while iterating through it
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:53:15 -04:00
7c86d862cf
fix(aurora): fixed a TypeError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:52:18 -04:00
edbc950741
fix(aurora): remove empty lines in changes_factory and fix timestamp formatting
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:50:52 -04:00
709042f057
fix(aurora): fixed an AttributeError in changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-19 17:46:05 -04:00
1c3b9377b5
fix(aurora): catch a ValueError in changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:45:02 -04:00
891b36ccaa
fix(aurora): fixed another syntax error in changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:42:25 -04:00
9be187e4aa
fix(aurora): fixed a SyntaxError in changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-19 17:41:41 -04:00
ec082b58ad
fix(aurora): make changes_factory support changes properly
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-19 17:40:52 -04:00
b0509d748c
fix(aurora): fixed a 403 forbidden error in some moderation type handlers when moderating someone with the administrator permission
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-08-19 17:23:53 -04:00
29f393fa89
fix(aurora): fix an attributeerror when editing a case type that doesn't use durations
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:21:33 -04:00
0c2cde1a78
fix(aurora): changed how moderation changes are added. only log the information that is actually changed, and not everything
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 17:19:01 -04:00
dc407c1125
fix(aurora): fixed the jsonencoder converting timedeltas to strings using str()
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 14:49:04 -04:00
3d3036f9b6
fix(aurora): convert timedeltas to strings before creating changes from them
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-08-19 14:46:08 -04:00
3e484e1ae5
fix(aurora): fixed /edit command not setting the moderation's duration properly
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-08-19 14:37:47 -04:00
5cd0ef61cb
fix(aurora): fixed an error in the /edit command
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-19 14:33:11 -04:00
46c1cf53bf
fix(aurora): fixed most of the moderation handlers having improper error handling for incorrect durations
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-08-19 14:24:25 -04:00
7b3608e264
fix(aurora): fixed a keyerror in the message_factory function and cleaned up some other problems
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 56s
2024-08-19 03:55:08 -04:00
ac7d950aaa
fix(aurora): added temporary debug logging
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-14 16:05:20 -04:00
8cbe9d94cf
fix(aurora): pass interaction instead of interaction.client to the duration_edit_handler
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-08-14 15:59:09 -04:00
ec508a92e3
fix(aurora): fixed a typeerror in /edit
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-08-14 15:57:53 -04:00
4a4f24bb8f
fix(aurora): don't continue banning someone if they're already banned
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-08-14 15:46:27 -04:00
9ae7607015
fix(aurora): fixed the Moderation.update() method again
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-14 15:26:55 -04:00
b0c9656758
fix(aurora): awaited a coroutine
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-14 15:23:40 -04:00
7854af8ec0
fix(aurora): fixed the /edit command failing again
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-14 15:23:05 -04:00
de4c789fea
fix(aurora): don't use deepcopy
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-14 15:21:07 -04:00
51f165c480
fix(aurora): fixed /edit command not properly storing the data from the old moderation
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-08-14 15:14:22 -04:00
5fc6d0eb6a
fix(aurora): fixed Moderation.update() not converting the duration timedelta to the correct kind of string
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-14 15:08:37 -04:00
48d2f8b416
fix(aurora): fixed debug logging
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-08-14 14:37:32 -04:00
c5472d25c1
Merge branch 'main' into aurora-pydantic
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-08-14 01:06:13 -04:00
001fccfe34
fix(aurora): pylint fixes
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 51s
2024-08-14 00:06:48 -04:00
fb92cdc08c
fix(aurora): fixed a bug in removerole
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-08-14 00:04:25 -04:00
123cd188dc
fix(aurora): fixed addrole and removerole *again*
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-08-14 00:02:48 -04:00
871cabfb71
fix(aurora): fixed addrole and removerole again
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-08-14 00:01:13 -04:00
6aaef7d3b2
fix(aurora): fixed removerole and addrole being broken if used without a duration set
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-08-13 23:59:36 -04:00
86c0cadd82
fix(aurora): don't pass key
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-13 16:39:36 -04:00
6c7cdd4e5e
fix(aurora): fixed all type
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-13 16:38:20 -04:00
1275b8e99a
fix(aurora): revert a stupid change
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-08-13 16:36:15 -04:00
71840cc148
misc(aurora): added a temporary logging call
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-13 16:31:00 -04:00
f5aa9f2b20
fix(aurora): fixed a programmingError when using all
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-13 16:26:14 -04:00
1a20ae30de
feat(aurora): add autocomplete entry for all
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-13 16:23:35 -04:00
3d8050d3b9
fix(aurora): fixed a minor bug with autocomplete
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-08-13 16:08:16 -04:00
a5e3a11479
fix(aurora): set a default argument for the resolve command's string parameter
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-13 15:11:39 -04:00
48259df18f
misc(aurora): move moderate() into the Aurora cog class
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-13 15:09:49 -04:00
233e6ac908
feat(aurora): added expired argument to the /history command
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-08-13 14:25:03 -04:00
448ac6792e
fix(aurora): fixed some expiry_handler() methods fetching the user object but not putting it into a variable
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-08-13 14:18:25 -04:00
009b406ff6
fix(aurora): fixed some broken expiry handlers
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-08-13 01:30:47 -04:00
a6124dd9ca
fix(aurora): fixed a TypeError whenever an expiry_handler function is called
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-08-13 01:26:50 -04:00
ad063062fb
feat(aurora): add autocomplete to /history's types argument
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-08-13 01:22:18 -04:00
830237938a
fix(aurora): pylint fix
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 47s
2024-08-13 00:41:25 -04:00
e559db9f10
feat(aurora): moved the converter commands to their own convert command group
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-08-12 20:46:40 -04:00
9e2bcf9b00
feat(aurora): added datetime command 2024-08-12 20:44:12 -04:00
4ec065e438
fix(aurora): fixed a NotFound error being thrown to the console 2024-08-12 20:40:10 -04:00
7c8aaba309
misc(aurora): version bump
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-08-12 20:33:47 -04:00
c82aa6a0f5
fix(aurora): catch an attribute error 2024-08-12 20:33:37 -04:00
10cfeeefcd
fix(aurora): awaited a coroutine 2024-08-12 20:32:14 -04:00
818ff810ea
misc(aurora): removed an old kwarg from the moderate function docstring
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-08-12 19:52:35 -04:00
85dbb6cc88
fix(aurora): export all moderation types
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-12 19:45:14 -04:00
8bb9223054
misc(aurora): updated my own name
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-08-12 18:57:48 -04:00
1bc6412b07
misc(aurora): version bump to 3.0.0-indev1
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-12 18:56:57 -04:00
cbd82f8572
fix(aurora): minor changes to the aurora importer and also fixed a bug in the Moderation.get_latest method
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-12 18:50:07 -04:00
8822d1714e
misc(aurora): make show_in_history default to True
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-12 18:18:34 -04:00
d546fb3371
misc(aurora): minor formatting fix
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
2024-08-12 18:15:15 -04:00
4d408ff616
feat(aurora): implemented the per-type configuration options so they actually do something
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-08-12 18:13:04 -04:00
08278c2de4
fix(aurora): fixed some formatting
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-12 17:49:51 -04:00
bf36e99224
fix(aurora): fixed a registry error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-12 17:48:01 -04:00
7c69a89442
feat(aurora): added reset button to the types menu
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 45s
2024-08-12 17:44:48 -04:00
290ac5947f
fix(aurora): actually fixed the previous TypeError 2024-08-12 17:44:39 -04:00
c134a3ed18
fix(aurora): maybe fixed a TypeError?
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-12 17:42:46 -04:00
9726b1423c
fix(aurora): fixed a ModuleNotFoundError being caused by a typo
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-12 17:40:17 -04:00
9c345ed96b
feat(aurora): add per-type configuration options and a menu to configure them
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
none of the options do anything yet, this is just creating the configuration keys and the menu to modify them
2024-08-12 17:39:13 -04:00
8aaa918b6e
Merge branch 'main' into aurora-pydantic
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 50s
2024-08-12 03:19:05 -04:00
de06490050
fix(aurora): typo
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-10 13:51:17 -04:00
2e3640b738
fix(aurora): make on more accurate
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-10 13:50:47 -04:00
cb424ed722
fix(aurora): fixed another OperationalError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-08-10 13:44:18 -04:00
f3246366ff
fix(aurora): fixed Moderation.get_latest() breaking when using only before and after, and no other kwargs
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-08-10 13:42:15 -04:00
14750787b2
fix(aurora): cast timestamps to integers
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-08-10 13:34:15 -04:00
14dc256919
fix(aurora); log when Modereation.execute() fails with an operationalerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-08-10 13:32:37 -04:00
49cf7c9005
feat(aurora): switch from dateparser to dateutil and add on parameter to history command
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-10 13:27:49 -04:00
1a3af342df
feat(aurora): allow for querying moderation history by date (before/after)
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-08-10 13:17:16 -04:00
7f0bd8c1a8
fix(aurora): fixed the Moderation.update() method being broken
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-07-12 16:33:00 -04:00
6edda87baa
fix(aurora): add a resolve_handler to AddRole (oops!)
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 44s
2024-07-12 16:30:34 -04:00
b379584251
fix(aurora): fixed a TypeError in the note and warn handlers
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-07-12 16:05:05 -04:00
f1e763673b
fix(aurora): legitimately the final pylint fixes for now
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-07-12 16:02:19 -04:00
c4c5b323a3
fix(aurora): ACTUALLY final pylint fixes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-07-12 15:58:15 -04:00
ea12d362df
fix(aurora): final pylint fixes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-07-12 15:55:16 -04:00
8d03022453
fix(aurora): more pylint fixes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-07-12 15:52:27 -04:00
63e6f4b552
fix(aurora): pylint fixes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-07-12 15:49:54 -04:00
47c8116ee0
misc(aurora): removed useless whitespace from the top of a file
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-07-12 15:42:58 -04:00
ad0d981888
fix(aurora): hopefully fixed a pydantic schema error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-07-12 15:28:39 -04:00
e2b0fc999a
feat(aurora): finishing up moderation handlers
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2024-07-12 15:22:24 -04:00
9d0f2e3887
misc(aurora): send evidenceformat to dms if send_evidenceformat is called outside of an Interaction context, as epheremal messages would be disabled in this case 2024-07-06 18:58:04 -04:00
461fbf83ee
fix(aurora): use phx-class-registry instead of class-registry 2024-07-06 14:23:56 -04:00
9faf1b027c
misc(aurora): typehints
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-07-06 13:39:01 -04:00
ec32e19b8b
fix(aurora): pass a Type instead of a string
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-07-06 13:24:44 -04:00
9f747a77ca
fix(aurora): fixed a history call to Moderation.type that was causing a TypeError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-07-06 13:16:53 -04:00
6d0d79c6c7
fix(aurora): properly skip over case 0
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 37s
2024-07-06 13:15:06 -04:00
13874dd4f0
fix(aurora): add a debug logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 25s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 38s
2024-07-06 13:12:47 -04:00
aae4370868
misc(aurora): fixed an incorrect string
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 25s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-07-06 13:10:47 -04:00
37bae2eeb3
fix(aurora): add a softban type
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-07-06 13:10:09 -04:00
8f0425456c
feat(aurora): boilerplate for all currently added moderation types
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-07-06 13:03:59 -04:00
a05e957dde
fix(aurora): fixed a json encoder issue that was causing an attributeerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-07-06 12:29:43 -04:00
0c628cf2a2
fix(aurora): fixed a TypeError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-07-06 12:27:59 -04:00
bf945ec9f1
fix(aurora): fixed some stuff up
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-07-06 12:23:06 -04:00
54ac77ceb9
fix(aurora): fix `__str__() method of the Type` class
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-07-06 12:16:15 -04:00
b85932c338
feat(aurora): use resolve handlers 2024-07-06 12:15:39 -04:00
08512d0dad
feat(aurora): use the type_registry in the Moderation object
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-07-06 12:03:49 -04:00
134e787c42
fix(aurora): fixed the Moderation.get_target method trying to get a channel object from a user id
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-07-06 11:58:46 -04:00
127dd564f5
fix(aurora): use the verb for message embed titles
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 24s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-07-06 11:55:05 -04:00
88cc7b4a3f
fix(aurora): fixed another attributeerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 37s
2024-07-06 11:53:39 -04:00
dd89bfaf34
fix(aurora): checking against the wrong bool value for silent
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-07-06 11:52:19 -04:00
ee49d5b10c
misc(aurora): make target_type lowercase 2024-07-06 11:50:59 -04:00
fac00feeef
fix(aurora): fixed another AttributeError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-07-06 11:47:56 -04:00
f2a88cbf94
fix(aurora): fixed an AttributeError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-07-06 11:46:13 -04:00
aeec616be5
fix(aurora): assume that ctx is a commands.Context object before trying to use it in moderate
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 25s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 38s
2024-07-06 11:32:33 -04:00
9f068bba6f
feat(aurora): starting on the updated moderation type system
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-07-06 11:30:37 -04:00
fcecfe8e88
fix(aurora): fixed an issue in the galacticbot importer
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-07-05 19:14:03 -04:00
2aadf5134d
fix(aurora): use f-strings
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
2024-07-05 19:03:44 -04:00
3b102debac
fix(aurora): fixed a PermissionError 2024-06-30 05:04:21 -04:00
c28b089edb
fix(aurora): fixed an issue with the /case command preventing the cog from loading 2024-06-30 05:02:32 -04:00
987fc0dbf8
feat(aurora): add scoped export functionality to aurora's /history command 2024-06-30 04:56:42 -04:00
87942213a5
fix(aurora): typehints 2024-06-11 03:17:38 -04:00
60d4afc6f3
fix(aurora): fixed AuroraBaseModel.to_json() not using AuroraBaseModel.dump() 2024-06-11 03:11:45 -04:00
0bcbcd6c0c
feat(aurora): bunch of changes 2024-06-08 20:12:22 -04:00
f6a42b97d9
misc(aurora): various model changes 2024-06-05 23:13:23 -04:00
5cb61ecd65
fix(aurora): awaited a coroutine 2024-06-05 01:39:34 -04:00
9622e037c9
fix(aurora): fixed another sql syntax error 2024-06-05 01:38:27 -04:00
e9a64e5a39
fix(aurora): fixed an sql syntax error 2024-06-05 01:37:41 -04:00
afac274978
fix(aurora): removed a useless try/except block 2024-06-05 01:36:45 -04:00
e988917319
feat(aurora): added a return_obj parameter to Moderation.execute() 2024-06-05 01:31:40 -04:00
42f7f9f69b
feat(aurora): migrated Aurora.handle_expiry() to use Moderation.execute() instead of opening its own connections 2024-06-05 01:29:47 -04:00
d07e5ed804
misc(aurora): changing around some logging levels 2024-06-05 01:12:49 -04:00
df465e5ba6
fix(aurora): awaited another coroutine 2024-06-05 01:11:18 -04:00
76572e2281
fix(aurora): awaited a coroutine 2024-06-05 01:05:11 -04:00
d629f1a5a2
fix(aurora): awaited two coroutines 2024-06-05 01:02:09 -04:00
ca4510d3a5
fix(aurora): why was I CLOSING THE CONNECTION THERE 😭 2024-06-05 01:01:17 -04:00
3247e6fb82
fix(aurora): lmao i'm dumb 2024-06-05 00:59:33 -04:00
67e3abf5ce
fix(aurora): use interaction.channel.send instead 2024-06-05 00:54:05 -04:00
d1b5346396
fix(aurora): fixed failed_cases in the aurora importer 2024-06-05 00:51:13 -04:00
fe5823b637
fix(aurora): awaited a coroutine 2024-06-05 00:41:41 -04:00
3383e84221
fix(aurora): fixed some issues with aiosqlite 2024-06-05 00:39:56 -04:00
56a2f96a2d
feat(aurora): made database.connect() into an async context manager 2024-06-05 00:36:12 -04:00
eebddd6e89
fix(aurora): override aiosqlite's logging level to warning 2024-06-05 00:25:19 -04:00
5cbf4e7e47
feat(aurora): migrated to aiosqlite 2024-06-05 00:14:43 -04:00
027144f35d
fix(aurora): pylint fixes
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-06-04 23:55:55 -04:00
0d64e3f652
misc(aurora): convert Change.from_dict() to use utils.timedelta_from_string()
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-06-04 23:52:35 -04:00
8591649465
fix(aurora): fixed aurora.utilities.utils.timedelta_from_string not including days in its calculations
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2024-06-04 23:43:53 -04:00
78630dc317
feat(aurora): added timedelta_from_string() function
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 50s
2024-06-04 23:31:52 -04:00
74d122a2e7
fix(aurora): catch importer errors instead of letting the entire import process die 2024-06-04 23:31:36 -04:00
ce48c1e889
fix(aurora): fixed a valueerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-06-04 16:55:29 -04:00
ff34310113
fix(aurora): fixed a valueerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-06-04 16:52:19 -04:00
3d2dabae08
fix(aurora): fixed a typeerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-06-04 16:50:33 -04:00
3fdc54b7cb
fix(aurora): get_next_case_number now only retrieves the first entry in the sql database (using LIMIT 1)
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-06-04 16:37:28 -04:00
b252343dc0
misc(aurora): allow Moderation.log() to skip returning the class object
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-06-04 16:35:18 -04:00
720e100a20
fix(aurora): removed some logging statements
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-06-04 15:52:23 -04:00
ff66006b8a
misc(aurora): add more logging
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 50s
2024-06-04 15:43:15 -04:00
fdb96539c3
fix(aurora): removed a useless debug statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-06-04 15:39:16 -04:00
7a664ce9c3
fix(aurora): make Change.reason optional
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-06-04 15:37:58 -04:00
3168c42787
fix(aurora): fixed a pydantic ValidationError
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-06-04 15:36:38 -04:00
5d53eec2f1
fix(aurora): fixed a valueerror in the Change.from_dict() method
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-06-04 15:32:59 -04:00
166421b6ba
fix(aurora): removed a character causing a valueerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-06-04 15:23:45 -04:00
0a207b66e4
fix(aurora): fixed an error with timedelta formatting
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-06-04 15:22:50 -04:00
5151f65317
fix(aurora): added more to the debugging statement
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-06-04 14:59:57 -04:00
0089625ef3
fix(aurora): add a debug statement
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-06-04 14:56:51 -04:00
8ac735dafe
misc(aurora): change the JSONEncoder subclass to use match/case instead of if statements
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 40s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m4s
2024-06-04 14:20:01 -04:00
21fa3d9eb0
feat(aurora): added an __int__ dunder method to the moderation model
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-06-04 12:44:58 -04:00
38180f5ccd
fix(aurora): fixed an sql operation error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-06-04 12:29:24 -04:00
19e50c25e3
Merge branch 'aurora-pydantic' of https://www.coastalcommits.com/Seaswimmer/SeaCogs into aurora-pydantic
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 49s
2024-06-04 12:23:13 -04:00
a10af37f14
feat(aurora): add support for OFFSET in Moderation.get_latest() 2024-06-04 12:23:12 -04:00
a5b344a323
Merge branch 'main' into aurora-pydantic
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-06-04 16:16:26 +00:00
cb420d2fc4
fix(aurora): pylint/ruff fix
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 54s
2024-06-04 11:52:34 -04:00
04223c3c55
Merge branch 'main' into aurora-pydantic
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2024-06-04 11:48:15 -04:00
1c6d2456ed
misc(aurora): changed aurora.utilities.utils.get_bool_emoji to use match/case instead of if/else
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
2024-06-04 00:04:46 -04:00
9b0f977016
feat(aurora): moved get_next_case_number() into the Moderation class, along with removing from_sql and get_all_cases. also added some other classmethods to replace those.
also modified message_factory and its calls to add a new kwarg
2024-06-04 00:04:11 -04:00
460d5a31fc
feat(aurora): allow the Moderation.execute() classmethod to accept a cursor, to prevent opening a new database connection on every call 2024-06-03 23:48:21 -04:00
bbe8b281d1
misc(aurora): changed two typehints in aurora.utilities.utils and added other typehints 2024-06-03 23:46:22 -04:00
be253b668b
feat(aurora): converted /history to use the new Moderation sql queries
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 45s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m9s
2024-06-03 01:07:00 -04:00
22f9ce52d1
fix(aurora): fixed a typeerror
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 42s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m11s
2024-06-03 00:55:56 -04:00
499cfbe8a9
misc(aurora): use tuples instead of lists
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m14s
2024-06-03 00:55:09 -04:00
99d95afe07
fix(aurora): fixed another TypeError
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 45s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m10s
2024-06-03 00:51:51 -04:00
028e22ebec
fix(aurora): happy now?
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 1m9s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 43s
2024-06-03 00:50:17 -04:00
4c28453173
fix(aurora): fixed a TypeError
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 1m9s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 42s
2024-06-03 00:49:08 -04:00
d646754466
feat(aurora): added a bunch of functionality to the Moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 44s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 1m11s
2024-06-03 00:47:56 -04:00
76f176d4cc
feat(aurora): change history to use Moderation.from_sql_all()
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 45s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m10s
2024-06-03 00:22:32 -04:00
c35580c576
misc(aurora): change to aurora importer log arguments
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 47s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m15s
2024-06-03 00:17:33 -04:00
7fc6235abe
fix(aurora): remove bot keys from the change import when importing from aurora
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 46s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m12s
2024-06-03 00:15:17 -04:00
21e51dc320
misc(aurora): minor syntax change
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 46s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m15s
2024-06-03 00:09:39 -04:00
db477c4744
fix(aurora): fixed an issue with json encoding
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 47s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m10s
2024-06-03 00:07:52 -04:00
1990b97518
fix(aurora): ignore moderation cases where the moderation_id is 0
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 49s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m13s
2024-06-02 23:59:17 -04:00
641f45d126
feat(aurora): add the from_sql_all classmethod to the Moderation model
All checks were successful
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 1m9s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 47s
2024-06-02 23:53:19 -04:00
73c9104882
fix(aurora): cast to string before checking length to avoid typeerrors when reason is None
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-24 04:26:58 -04:00
5b64ee9578
feat(aurora): add metadata to evidenceformat
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-24 04:22:42 -04:00
bfb4d8768d
fix(aurora): add moderation metadata to the log factory
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 38s
2024-05-24 04:18:17 -04:00
7f71ca3d6d
fix(aurora): change interval metadata
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-05-24 04:16:48 -04:00
3dcc637920
fix(aurora): avoid keyerrors
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 04:15:27 -04:00
9a4f19f4a1
fix(aurora): show metadata key/value pairs in /case
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-05-24 04:12:28 -04:00
7a9c9846de
fix(aurora): fixed a broken from_id method
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-24 04:09:27 -04:00
0cc7d6079d
fix(aurora): removed useless debug statement
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-24 04:06:05 -04:00
ed923f1d9b
fix(aurora): finally actually maybe fixed the pydantic validation error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 25s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 38s
2024-05-24 04:04:21 -04:00
39cb5feb50
misc(aurora): adding temporary debug logging
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 44s
2024-05-24 04:02:15 -04:00
c90796f6b3
fix(aurora): hopefully actually fixed the pydantic validation error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 38s
2024-05-24 04:01:15 -04:00
0b1d1d29e6
fix(aurora): hopefully fixed a pydantic validation error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 03:57:54 -04:00
599ab8c51d
fix(aurora): fixed a non-async function being awaited
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 03:56:32 -04:00
797fd561c9
fix(aurora): fixed another typeerror
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 03:53:38 -04:00
51d3245703
fix(aurora): fixed a typeerror in Moderation.log()
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 38s
2024-05-24 03:51:43 -04:00
0411e3dab7
fix(aurora): fixed a typeerror in the check_moddable function
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 03:49:55 -04:00
67b33a2eb8
feat(aurora): added a slowmode command
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 39s
2024-05-24 03:46:20 -04:00
dc51aa7bdc
fix(aurora): import fixes
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 45s
2024-05-09 21:27:26 -04:00
904fd1c914
fix(aurora): fixed more import errors
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-06 21:46:01 -04:00
92d221ff70
fix(aurora): forgot a file!
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-05-06 21:43:08 -04:00
d91a4f49f9
fix(aurora): fixed import errors
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-05-06 21:39:43 -04:00
ab878739c4
misc(aurora): pep 604 compliance
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-05-06 21:04:08 -04:00
946e14ee3c
fix(aurora): something idk
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-05-06 20:45:22 -04:00
bcc4aa384f
fix(aurora): hopefully fixed the module not found issue?
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-05-06 17:41:14 -04:00
260dd3ef4c
fix(aurora): fixing a ModuleNotFound error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-06 17:35:43 -04:00
c69b3cd032
misc(aurora): import change
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s
2024-05-06 17:33:28 -04:00
2b79e3b6a8
fix(aurora): added an __init__.py to a directory that was missing one
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 40s
2024-05-06 17:29:57 -04:00
7dfe94869c
misc(aurora): codebase cleanup
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 41s
2024-05-06 17:23:59 -04:00
ac8cefd779
fix(aurora): pylint fixes
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 43s
2024-05-06 16:47:21 -04:00
09471a4027
misc(aurora): change the logging severity of Change.from_dict()'s logs to trace
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 16:39:13 -04:00
15ccd5530a
fix(aurora): fixed a few unboundlocalerrors
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 16:37:52 -04:00
bb5ab8e61b
fix(aurora): make Moderration.from_result() return a Moderation object instead of a dictionary
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-06 16:36:27 -04:00
c2e017339e
fix(aurora): fixed a broken classmethod
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-05-06 16:35:06 -04:00
d7ca5cab46
feat(aurora): cleaned up the codebase and fixed a whole bunch of bugs
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-06 16:34:08 -04:00
53b67e1c95
fix(aurora): convert results into a dictionary before making a Moderation instance out of them
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-05-06 16:07:35 -04:00
d7a8fbe367
fix(aurora): ignore moderations with the id 0 in the history command
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 16:02:00 -04:00
f8968e8e9e
feat(aurora): updated /history command
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-06 15:59:43 -04:00
834d116b20
fix(aurora): fixed a syntax error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 15:32:01 -04:00
d375716fbf
feat(aurora): added a resolve function to the Moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 15:29:48 -04:00
dcda128f11
fix(aurora): edit command will now edit reasons as intended
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 15:15:54 -04:00
16af26f93e
fix(aurora): fixed a logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-05-06 15:11:11 -04:00
5be5a39c54
fix(aurora): check type of dictionary in debug log
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 15:06:10 -04:00
e14845ef85
fix(aurora): make sure the user_id in changes is always an integer
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-06 15:03:41 -04:00
8d1a57165d
fix(aurora): added debug logging
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 15:00:54 -04:00
a4e11fd828
fix(aurora): fixing some model stuff
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 14:55:36 -04:00
9c7e0b0b89
fix(aurora): fixed a missing parameter
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 14:48:16 -04:00
1e865643a0
fix(aurora): fixed duration being a timedelta in case_safe
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-06 14:45:04 -04:00
b7b6dc2f2e
fix(aurora): don't convert end_timestamp to an integer
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
2024-05-06 14:44:06 -04:00
84235d6504
fix(aurora): call from_dict properly
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-06 14:41:16 -04:00
70c00a59d6
fix(aurora): fixed the broken logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 14:40:14 -04:00
0386cb346e
fix(aurora): finally fixed the debug logging statement (maybe)
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
2024-05-06 14:35:02 -04:00
335d1a2a4c
fix(aurora): actually actually ACTUALLY fixed the broken logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 14:29:57 -04:00
1b322dfe50
fix(aurora): actually actually fixed the broken logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 14:29:09 -04:00
5b6b04dfe0
fix(aurora): actually fixed a broken logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 14:25:42 -04:00
158e7560f8
fix(aurora): fixed a broken logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-06 14:22:40 -04:00
52fcdcc96a
fix(aurora): fixed a logging issue
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 38s
2024-05-06 14:21:57 -04:00
65bb9af7a6
fix(aurora): fixed an issue with dictionary values not being converted to a tuple
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 14:20:49 -04:00
e591b2c4a5
fix(aurora): convert floats to ints
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-06 14:18:54 -04:00
a86348fae3
fix(aurora): fixed a programming error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 50s
2024-05-06 14:16:51 -04:00
37e471fbaa
fix(aurora): fixing a whole bunch of stuff
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-06 14:15:05 -04:00
0553856aa9
fix(aurora): fixed utils.generate_dict() using literal_eval still even when it's unnecessary
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-06 13:51:23 -04:00
6745f0a486
fix(aurora): fixed an issue with importing changes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2024-05-06 13:44:48 -04:00
85a935f9b3
fix(aurora): fixed the json issue from yesterday
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-06 13:27:09 -04:00
4c8cd7bd16
Revert "fix(aurora): trying ast"
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
This reverts commit 278bd98349.
2024-05-04 23:02:57 -04:00
278bd98349
fix(aurora): trying ast
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-04 23:01:39 -04:00
cad24d852c
fix(aurora): ok fine bro 😭
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-05-04 22:58:29 -04:00
de90f6a8b7
fix(aurora): troubleshooting this annoying json issue
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-05-04 22:57:34 -04:00
293f77c228
fix(aurora): changed the logging statement again
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 54s
2024-05-04 22:55:50 -04:00
a6371fd367
fix(aurora): changed the logging statement slightly
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-05-04 22:55:17 -04:00
8433c946fd
fix(aurora): added a debug logging statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 47s
2024-05-04 22:52:54 -04:00
300d26dc7e
fix(aurora): fixed a bunch of json issues
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 30s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-05-04 22:50:30 -04:00
e8d210df2a
fix(aurora): removed a %s
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-05-04 22:19:29 -04:00
acf3b0c68f
fix(aurora): added logging for the Moderation.update() method
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-05-04 22:17:52 -04:00
04d10d2cf8
fix(aurora): strip and replace the json dumps before inserting them into the db
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-04 22:12:00 -04:00
0b697f9f50
fix(aurora): change how Moderation.update() works
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-04 22:07:51 -04:00
557ac45fcc
fix(aurora): fixed a circular import
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-04 22:05:14 -04:00
3f8cdf2012
fix(aurora): fixed an sqlite error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s
2024-05-04 22:04:29 -04:00
b03ce04245
fix(aurora): fixed an unboundlocalerror in Change.from_dict()
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 25s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-04 22:02:31 -04:00
dc44e8c6de
feat(aurora): migrated /edit to the new model system
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 27s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
2024-05-04 22:01:32 -04:00
fc15b434c7
feat(aurora): finished the factory migrations
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 23s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 32s
2024-05-04 21:24:37 -04:00
e89db3de5a
misc(aurora): added some markdown formatting
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 21:20:05 -04:00
a8414a7918
fix(aurora): fixed some subscripting errors in changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 21:19:06 -04:00
6eeab9ed96
fix(aurora): fixed a keyerror
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 21:17:03 -04:00
94f6d6c3b5
fix(aurora): fixed the changes_factory
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 22s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 21:15:39 -04:00
ee331b7544
fix(aurora): fixed an incorrect conditional statement resulting in unintended behavior
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 32s
2024-05-04 21:07:37 -04:00
50c2db80d9
fix(aurora): convert datetimes to unix timestamps
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 21:06:29 -04:00
23eba46948
fix(aurora): fix a few classmethods returning None instead of their constructed classes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 30s
2024-05-04 21:02:18 -04:00
0b31e70089
fix(aurora): fixed a conditional statement and a pydantic issue
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 24s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 33s
2024-05-04 20:59:23 -04:00
3f6aec0a82
fix(aurora): don't JSON serialize the Red class
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 22s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 20:55:11 -04:00
a22de1d2c2
fix(aurora): fixed another pydantic error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 22s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:28:03 -04:00
6a7758e8f9
fix(aurora): reverted previous change, instead using pydantic's ConfigDicts to resolve the error I was encountering previously
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 20s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 30s
2024-05-04 18:27:12 -04:00
a7d8f452d1
fix(aurora): updated utilities.json.JSONEncoder to match the model change I just made
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:23:24 -04:00
6520f4f2b9
fix(aurora): don't inherit from AuroraBaseModel
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 22s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:22:42 -04:00
ea280c2d62
fix(aurora): fixed a syntax error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 30s
2024-05-04 18:18:57 -04:00
d70f2bf5f1
fix(aurora): fixed guild ids appearing in changes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:17:21 -04:00
356d58f9d7
fix(aurora): changed the jsonencoder
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:09:06 -04:00
74a0983419
fix(aurora): fixed a missing kwarg in a model initialization function 2024-05-04 18:08:27 -04:00
1313834ea5
fix(aurora): fixed a pydantic error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 18:07:29 -04:00
b752181781
fix(aurora): oops lol
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 20s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 30s
2024-05-04 18:06:23 -04:00
39479eb216
fix(aurora): fixing up some model stuff
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 30s
2024-05-04 18:05:20 -04:00
e46612dc08
fix(aurora): fixed a circular import
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 22s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 17:49:01 -04:00
e7e8d60f3b
feat(aurora): added a new object for Changes
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 21s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 31s
2024-05-04 17:48:08 -04:00
eea66607d9
feat(aurora): finished migrating case_factory to the new model 2024-05-04 17:31:16 -04:00
2c336ff70c
fix(aurora): fixed a faulty expiration check
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
2024-05-04 17:15:21 -04:00
a3a208b38e
fix(aurora): fixed an incorrect function call in Moderation.from_sql()
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-04 17:12:35 -04:00
26bf5920c6
fix(aurora): mark the bot as a classvar in the moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-04 17:10:26 -04:00
d13ad88f16
fix(aurora): fixed a whole bunch of pydantic errors
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 42s
2024-05-04 17:09:22 -04:00
6147c8c6d5
feat(aurora): whole bunch of changes to the models and various other things
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 39s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 58s
2024-05-04 16:54:12 -04:00
2dfc9d9824
fix(aurora): use AuroraBaseModels for the JSONEncoder class instead of just pydantic ones to prevent issues with other data types
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-05-04 15:37:07 -04:00
ca7c0d8d7c
fix(aurora): use pydantic basemodels instead of the Moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 51s
2024-05-04 15:33:49 -04:00
98f3e5943b
fix(aurora): fixed incorrect kwarg name
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 48s
2024-05-04 15:29:01 -04:00
7dfd9c607a
misc(aurora): moved some stuff around
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 50s
2024-05-04 15:25:05 -04:00
df87612055
feat(aurora): added imported_timestamp to metadata on new imports
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 36s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 51s
2024-05-04 15:13:31 -04:00
58303b8e9c
feat(aurora): added a to_json method to the moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-05-04 15:08:08 -04:00
ca1722fee3
feat(aurora): migrated to a custom json encoder
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 51s
2024-05-04 15:05:50 -04:00
69805b276f
fix(aurora): fixed an issue with json decoding
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
2024-05-04 14:58:24 -04:00
25d7101cb5
fix(aurora): testing a potential bugfix
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-05-04 14:55:17 -04:00
92f9619cea
fix(aurora): added two conditional statements to fix an error
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-05-04 14:54:14 -04:00
1d825625f3
fix(aurora): fixed some broken conditional statements
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 29s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 44s
2024-05-04 14:53:04 -04:00
ea65816c32
fix(aurora): fixed an incorrect if statement
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 46s
2024-05-04 14:51:22 -04:00
afed1d6a37
feat(aurora): changed a lot of stuff. THIS IS A BREAKING CHANGE! VERY BREAKING! TAKE DATABASE BACKUPS BEFORE UPDATING TO THIS
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
2024-05-04 14:49:07 -04:00
e8ca0aeb1c
fix(aurora): convert float timestamps to integers
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 48s
2024-05-04 14:19:48 -04:00
2da76eb51a
feat(aurora): subclassed jsonencoder to allow for custom behavior
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-05-04 14:18:19 -04:00
e5cdd3893f
fix(aurora): cleaned up the Moderation model
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-05-04 14:08:53 -04:00
14a04cff59
fix(aurora): fixed an error
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 47s
2024-05-04 13:48:57 -04:00
f3d6244a17
fix(aurora): optimizing the from_sql method
All checks were successful
Actions / Build Documentation (MkDocs) (pull_request) Successful in 32s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 46s
2024-05-04 13:47:07 -04:00
b6d1510698
fix(aurora): fixed a broken import
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 31s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 45s
2024-05-04 13:42:58 -04:00
c0969ea947
feat(aurora): added a Moderation model
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 33s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 49s
2024-05-04 13:41:11 -04:00
68 changed files with 4470 additions and 3532 deletions

View file

@ -1,6 +1,5 @@
FROM ghcr.io/astral-sh/uv:0.5.30@sha256:bb74263127d6451222fe7f71b330edfb189ab1c98d7898df2401fbf4f272d9b9 AS uv
FROM ghcr.io/astral-sh/uv:0.5.24@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 AS uv
FROM python:3.11-slim@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS python
FROM code.forgejo.org/forgejo/runner:6.2.1@sha256:fecc96a111a15811a6887ce488e75718089f24599e613e93db8e54fe70b706e8 AS forgejo-runner
FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm@sha256:6155a486f236fd5127b76af33086029d64f64cf49dd504accb6e5f949098eb7e
LABEL repository="www.coastalcommits.com/cswimr/SeaCogs"
@ -30,9 +29,6 @@ RUN apt-get update; \
COPY --from=uv --chown=vscode: /uv /uvx /bin/
COPY --from=python --chown=vscode: /usr/local /usr/local
COPY --from=forgejo-runner --chown=vscode: /bin/forgejo-runner /bin/forgejo-runner
COPY --chown=vscode: .devcontainer/home/* /home/vscode/
RUN ln -s /usr/local/bin/python3.11 /usr/local/bin/python; \
python --version; \
python -m ensurepip
python --version

View file

@ -4,21 +4,8 @@
"context": "..",
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"settings": {
"python.terminal.activateEnvInCurrentTerminal": true,
"python.terminal.activateEnvironment": true,
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/zsh"
}
}
},
"extensions": [
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
@ -42,11 +29,7 @@
"UV_PYTHON_DOWNLOADS": "never",
"PROJECT_DIR": "/workspaces/SeaCogs"
},
"mounts": [
"source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"
],
"postCreateCommand": {
"Setup Virtual Environment": "uv sync --frozen && sudo chown -R vscode:vscode /workspaces/SeaCogs/.data && uv run redbot-setup --no-prompt --instance-name=local --data-path=/workspaces/SeaCogs/.data --backend=json"
},
"mounts": ["source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"],
"postCreateCommand": "uv sync --frozen",
"remoteUser": "vscode"
}

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
alias runactions="forgejo-runner exec --default-actions-url=https://www.coastalcommits.com --gitea-instance=https://www.coastalcommits.com"

View file

@ -12,5 +12,5 @@ Aurora is a fully-featured moderation system. It is heavily inspired by Galactic
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs aurora
[p]load aurora
[p]cog load aurora
```

View file

@ -7,7 +7,7 @@ Backup allows you to export a JSON list of all of your installed repositories an
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs backup
[p]load backup
[p]cog load backup
```
## Version Compatibility

View file

@ -8,7 +8,7 @@ This cog does require an api key to work.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs bible
[p]load bible
[p]cog load bible
```
## Setup
@ -21,9 +21,8 @@ Then, you can use `[p]set api` to set the API key. Make sure your formatting mat
## Commands
### bible passage
- Usage: `[p]bible passage <book> <passage>`
- Aliases: `verse`
- Usage: `[p]bible passage <book> <passage>`
- Aliases: `verse`
Get a Bible passage.
@ -32,7 +31,6 @@ Example usage:
`[p]bible passage John 3:16-3:17`
### bible random
- Usage: `[p]bible random`
- Usage: `[p]bible random`
Get a random Bible verse.

View file

@ -7,7 +7,7 @@ EmojiInfo allows you to retrieve information about an emoji.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs emojiinfo
[p]load emojiinfo
[p]cog load emojiinfo
```
## Commands

View file

@ -1,26 +0,0 @@
# HotReload
HotReload automatically reloads cogs in local cog paths on file change.
This is useful for development, as it allows you to make changes to your cogs and see the changes reflected in Discord immediately, without having to manually `[p]reload` the cog.
## Installation
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs hotreload
[p]load hotreload
```
## Commands
### hotreload compile
Determines if the cog should try to compile a modified Python file before reloading the associated cog. Useful for catching syntax errors. Disabled by default.
### hotreload notifychannel
Set the channel where hotreload will send notifications when a cog is reloaded.
### hotreload list
Debugging command that shows the list of currently active observers. May be expanded in the future to show watched file paths.

View file

@ -7,7 +7,7 @@ Nerdify allows you to nerdify other people's text.
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs nerdify
[p]load nerdify
[p]cog load nerdify
```
## Commands

View file

@ -12,5 +12,5 @@ Pterodactyl allows for connecting to a Pterodactyl server through websockets. It
```bash
[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs
[p]cog install seacogs pterodactyl
[p]load pterodactyl
[p]cog load aurora
```

View file

@ -1,12 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

1
.envrc
View file

@ -1 +0,0 @@
use flake

View file

@ -2,5 +2,5 @@
<!-- Create a new issue, if it doesn't exist yet -->
- [ ] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com/cswimr) to license my work under
- [ ] By submitting this pull request, I permit cswimr to license my work under
the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

View file

@ -12,7 +12,6 @@
too-many-locals,
too-many-public-methods,
too-many-statements,
too-many-positional-arguments,
arguments-differ,
too-many-return-statements,
import-outside-toplevel,

View file

@ -1,8 +1,6 @@
name: Actions
on:
push:
branches:
- main
pull_request:
jobs:
@ -29,6 +27,7 @@ jobs:
docs:
name: Build Documentation (MkDocs)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: docker
container: www.coastalcommits.com/cswimr/actions:docs@sha256:e405cd6b9b1182a570ddee32ed8dd1b2f899edc625d006c8b4b2f18c100e724f
steps:

3
.gitignore vendored
View file

@ -3,6 +3,3 @@ site
.venv
.data
__pycache__
.mypy_cache/
.ruff_cache/
.direnv/

7
.vscode/launch.json vendored
View file

@ -1,12 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Red-DiscordBot",
"name": "Red-DiscordBot",
"type": "debugpy",
"request": "launch",
"module": "redbot",
"args": ["local", "--dev", "-vvv", "--load-cogs=hotreload"]
"args": ["local", "-vvv"]
}
]
}

25
.vscode/settings.json vendored
View file

@ -1,32 +1,13 @@
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"files.exclude": {
"**/.git": true,
"**/__pycache__": true,
"**/.ruff_cache": true,
"**/.mypy_cache": true
},
"python.analysis.diagnosticSeverityOverrides": {
"reportAttributeAccessIssue": false, // disabled because `commands.group.command` is listed as Any / Unknown for some reason
"reportCallIssue": "information"
},
"python.analysis.diagnosticMode": "workspace",
"python.analysis.supportDocstringTemplate": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.typeEvaluation.enableReachabilityAnalysis": true,
"python.analysis.typeEvaluation.strictDictionaryInference": true,
"python.analysis.typeEvaluation.strictListInference": true,
"python.analysis.typeEvaluation.strictSetInference": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.json-language-features"
}
}

View file

@ -17,7 +17,7 @@ class AntiPolls(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.3"
__version__ = "1.0.1"
__documentation__ = "https://seacogs.coastalcommits.com/antipolls/"
def __init__(self, bot: Red):
@ -49,7 +49,7 @@ class AntiPolls(commands.Cog):
"""Nothing to delete."""
return
@commands.Cog.listener("on_message")
@commands.Cog.listener('on_message')
async def polls_listener(self, message: discord.Message) -> None:
if message.guild is None:
return self.logger.verbose("Message in direct messages ignored")
@ -62,13 +62,13 @@ class AntiPolls(commands.Cog):
guild_config = await self.config.guild(message.guild).all()
if guild_config["manage_messages"] is True and message.author.guild_permissions.manage_messages:
if guild_config['manage_messages'] is True and message.author.guild_permissions.manage_messages:
return self.logger.verbose("Message from user with Manage Messages permission ignored")
if message.channel.id in guild_config["channel_whitelist"]:
if message.channel.id in guild_config['channel_whitelist']:
return self.logger.verbose("Message in whitelisted channel %s ignored", message.channel.id)
if any(role.id in guild_config["role_whitelist"] for role in message.author.roles):
if any(role.id in guild_config['role_whitelist'] for role in message.author.roles):
return self.logger.verbose("Message from whitelisted role %s ignored", message.author.roles)
if not message.content and not message.embeds and not message.attachments and not message.stickers:
@ -80,9 +80,9 @@ class AntiPolls(commands.Cog):
return self.logger.error("Failed to delete message: %s", e)
return self.logger.trace("Deleted poll message %s", message.id)
return self.logger.verbose("Message %s is not a poll, ignoring", message.id)
self.logger.verbose("Message %s is not a poll, ignoring", message.id)
@commands.group(name="antipolls", aliases=["ap"]) # type: ignore
@commands.group(name="antipolls", aliases=["ap"])
@commands.guild_only()
@commands.admin_or_permissions(manage_guild=True)
async def antipolls(self, ctx: commands.Context) -> None:
@ -95,8 +95,6 @@ class AntiPolls(commands.Cog):
@antipolls_roles.command(name="add")
async def antipolls_roles_add(self, ctx: commands.Context, *roles: discord.Role) -> None:
"""Add roles to the whitelist."""
assert ctx.guild is not None # using `assert` here and in the rest of this file to satisfy typecheckers
# this is safe because the commands are part of a guild-only command group
async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist:
role_whitelist: list
failed: list[discord.Role] = []
@ -112,7 +110,6 @@ class AntiPolls(commands.Cog):
@antipolls_roles.command(name="remove")
async def antipolls_roles_remove(self, ctx: commands.Context, *roles: discord.Role) -> None:
"""Remove roles from the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist:
role_whitelist: list
failed: list[discord.Role] = []
@ -126,14 +123,13 @@ class AntiPolls(commands.Cog):
await ctx.send(f"The following roles were not in the whitelist: {humanize_list([role.mention for role in failed])}", delete_after=10)
@antipolls_roles.command(name="list")
async def antipolls_roles_list(self, ctx: commands.Context) -> discord.Message:
async def antipolls_roles_list(self, ctx: commands.Context) -> None:
"""List roles in the whitelist."""
assert ctx.guild is not None
role_whitelist = await self.config.guild(ctx.guild).role_whitelist()
if not role_whitelist:
return await ctx.send("No roles in the whitelist.")
roles = [role for role in (ctx.guild.get_role(role) for role in role_whitelist) if role is not None]
return await ctx.send(humanize_list([role.mention for role in roles]))
roles = [ctx.guild.get_role(role) for role in role_whitelist]
await ctx.send(humanize_list([role.mention for role in roles]))
@antipolls.group(name="channels")
async def antipolls_channels(self, ctx: commands.Context) -> None:
@ -142,7 +138,6 @@ class AntiPolls(commands.Cog):
@antipolls_channels.command(name="add")
async def antipolls_channels_add(self, ctx: commands.Context, *channels: discord.TextChannel) -> None:
"""Add channels to the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist:
channel_whitelist: list
failed: list[discord.TextChannel] = []
@ -158,7 +153,6 @@ class AntiPolls(commands.Cog):
@antipolls_channels.command(name="remove")
async def antipolls_channels_remove(self, ctx: commands.Context, *channels: discord.TextChannel) -> None:
"""Remove channels from the whitelist."""
assert ctx.guild is not None
async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist:
channel_whitelist: list
failed: list[discord.TextChannel] = []
@ -172,21 +166,16 @@ class AntiPolls(commands.Cog):
await ctx.send(f"The following channels were not in the whitelist: {humanize_list([channel.mention for channel in failed])}", delete_after=10)
@antipolls_channels.command(name="list")
async def antipolls_channels_list(self, ctx: commands.Context) -> discord.Message:
async def antipolls_channels_list(self, ctx: commands.Context) -> None:
"""List channels in the whitelist."""
assert ctx.guild is not None
channel_whitelist = await self.config.guild(ctx.guild).channel_whitelist()
if not channel_whitelist:
return await ctx.send("No channels in the whitelist.")
channels = [channel for channel in (ctx.guild.get_channel(channel) for channel in channel_whitelist) if channel is not None]
for c in channels:
if not c:
channels.remove(c)
return await ctx.send(humanize_list([channel.mention for channel in channels]))
channels = [ctx.guild.get_channel(channel) for channel in channel_whitelist]
await ctx.send(humanize_list([channel.mention for channel in channels]))
@antipolls.command(name="managemessages")
async def antipolls_managemessages(self, ctx: commands.Context, enabled: bool) -> None:
"""Toggle Manage Messages permission check."""
assert ctx.guild is not None
await self.config.guild(ctx.guild).manage_messages.set(enabled)
await ctx.tick()

View file

@ -1,14 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "AntiPolls",
"short": "AntiPolls deletes messages that contain polls.",
"description": "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).",
"end_user_data_statement": "This cog does not store any user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "AntiPolls",
"short" : "AntiPolls deletes messages that contain polls.",
"description" : "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).",
"end_user_data_statement" : "This cog does not store any user data.",
"hidden": true,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"tags": ["automod", "automoderation", "polls"]
"tags": [
"automod",
"automoderation",
"polls"
]
}

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,25 @@
# pylint: disable=duplicate-code
import json
from datetime import timedelta
from typing import Dict
import os
from time import time
from typing import Dict, List
from discord import ButtonStyle, Interaction, Message, ui
from redbot.core import commands
from redbot.core.utils.chat_formatting import box, warning
from discord import ButtonStyle, File, Interaction, Message, ui
from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import warning
from ..utilities.database import connect, create_guild_table, mysql_log
from ..models.moderation import Moderation
from ..models.type import Type, type_registry
from ..utilities.json import dump
from ..utilities.utils import create_guild_table, timedelta_from_string
class ImportAuroraView(ui.View):
def __init__(self, timeout, ctx, message):
def __init__(self, timeout, ctx, message, data: List[Dict[str, any]]):
super().__init__()
self.ctx: commands.Context = ctx
self.message: Message = message
self.data: List[Dict[str, any]] = data
@ui.button(label="Yes", style=ButtonStyle.success)
async def import_button_y(
@ -25,14 +30,8 @@ class ImportAuroraView(ui.View):
"Deleting original table...", ephemeral=True
)
database = connect()
cursor = database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query)
cursor.close()
database.commit()
await Moderation.execute(query=query, return_obj=False)
await interaction.edit_original_response(content="Creating new table...")
@ -40,73 +39,102 @@ class ImportAuroraView(ui.View):
await interaction.edit_original_response(content="Importing moderations...")
file = await self.ctx.message.attachments[0].read()
data: list[dict] = sorted(json.loads(file), key=lambda x: x["moderation_id"])
user_mod_types = ["NOTE", "WARN", "ADDROLE", "REMOVEROLE", "MUTE", "UNMUTE", "KICK", "TEMPBAN", "BAN", "UNBAN"]
channel_mod_types = ["SLOWMODE", "LOCKDOWN"]
failed_cases = []
for case in data:
for case in self.data:
if case["moderation_id"] == 0:
continue
moderation_type: Type = type_registry[case["moderation_type"].lower()]
if "target_type" not in case or not case["target_type"]:
if case["moderation_type"] in user_mod_types:
case["target_type"] = "USER"
elif case["moderation_type"] in channel_mod_types:
case["target_type"] = "CHANNEL"
if moderation_type.channel:
case["target_type"] = "channel"
else:
case["target_type"] = "USER"
case["target_type"] = "user"
if "role_id" not in case or not case["role_id"]:
case["role_id"] = 0
case["role_id"] = None
else:
case["role_id"] = int(case["role_id"])
if "changes" not in case or not case["changes"]:
case["changes"] = []
case["target_id"] = int(case["target_id"])
case["moderator_id"] = int(case["moderator_id"])
changes = case.get("changes", None)
if not changes:
changes = []
else:
if not isinstance(changes, list):
changes = json.loads(changes)
if isinstance(changes, str):
changes: list[dict] = json.loads(changes)
for change in changes:
if "bot" in change:
del change["bot"]
if "metadata" not in case:
metadata = {}
else:
if isinstance(case["metadata"], str):
metadata: Dict[str, any] = json.loads(case["metadata"])
else:
metadata = case["metadata"]
if not metadata.get("imported_from"):
metadata.update({"imported_from": "Aurora"})
metadata.update({"imported_timestamp": int(time())})
if case["duration"] != "NULL":
hours, minutes, seconds = map(int, case["duration"].split(":"))
duration = timedelta(hours=hours, minutes=minutes, seconds=seconds)
if case["duration"] != "NULL" and case["duration"] is not None:
duration = timedelta_from_string(case["duration"])
if moderation_type.key == "ban":
moderation_type = type_registry["tempban"]
else:
duration = "NULL"
duration = None
await mysql_log(
self.ctx.guild.id,
case["moderator_id"],
case["moderation_type"],
case["target_type"],
case["target_id"],
case["role_id"],
duration,
case["reason"],
try:
await Moderation.log(
bot=interaction.client,
guild_id=self.ctx.guild.id,
moderator_id=case["moderator_id"],
moderation_type=moderation_type,
target_type=case["target_type"].lower(),
target_id=case["target_id"],
role_id=case["role_id"],
duration=duration,
reason=case["reason"],
timestamp=case["timestamp"],
resolved=case["resolved"],
resolved_by=case["resolved_by"],
resolved_reason=case["resolve_reason"],
expired=case["expired"],
changes=case["changes"],
changes=changes,
metadata=metadata,
database=database,
return_obj=False
)
except Exception as e: # pylint: disable=broad-exception-caught
failed_cases.append(str(case["moderation_id"]) + f": {e}")
await interaction.edit_original_response(content="Import complete.")
if failed_cases:
await interaction.edit_original_response(
content="Import complete.\n"
+ warning("Failed to import the following cases:\n")
+ box(failed_cases)
filename = (
str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep)
+ f"failed_cases_{interaction.guild.id}.json"
)
with open(filename, "w", encoding="utf-8") as f:
dump(obj=failed_cases, fp=f, indent=2)
await interaction.channel.send(
content="Import complete.\n"
+ warning("Failed to import the following cases:\n"),
file=File(
filename, f"failed_cases_{interaction.guild.id}.json"
)
)
os.remove(filename)
@ui.button(label="No", style=ButtonStyle.danger)
async def import_button_n(
self, interaction: Interaction, button: ui.Button

View file

@ -1,18 +1,21 @@
# pylint: disable=duplicate-code
import json
from datetime import timedelta
from time import time
from discord import ButtonStyle, Interaction, Message, ui
from redbot.core import commands
from redbot.core.utils.chat_formatting import box, warning
from ..utilities.database import connect, create_guild_table, mysql_log
from ..models.moderation import Change, Moderation
from ..utilities.utils import create_guild_table
class ImportGalacticBotView(ui.View):
def __init__(self, timeout, ctx, message):
super().__init__()
self.ctx: commands.Context = ctx
self.timeout = timeout
self.message: Message = message
@ui.button(label="Yes", style=ButtonStyle.success)
@ -24,14 +27,14 @@ class ImportGalacticBotView(ui.View):
"Deleting original table...", ephemeral=True
)
database = connect()
cursor = database.cursor()
database = await Moderation.connect()
cursor = await database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query)
await cursor.execute(query)
cursor.close()
database.commit()
await cursor.close()
await database.commit()
await interaction.edit_original_response(content="Creating new table...")
@ -67,12 +70,12 @@ class ImportGalacticBotView(ui.View):
if case["duration"] is not None and float(case["duration"]) != 0:
duration = timedelta(seconds=round(float(case["duration"]) / 1000))
else:
duration = "NULL"
duration = None
except OverflowError:
failed_cases.append(case["case"])
continue
metadata = {"imported_from": "GalacticBot"}
metadata = {"imported_from": "GalacticBot", "imported_timestamp": int(time())}
if case["type"] == "SLOWMODE":
metadata["seconds"] = case["data"]["seconds"]
@ -98,37 +101,37 @@ class ImportGalacticBotView(ui.View):
if resolved_timestamp is None:
resolved_timestamp = timestamp
changes = [
{
Change.from_dict(interaction.client, {
"type": "ORIGINAL",
"reason": case["reason"],
"user_id": case["executor"],
"timestamp": timestamp,
},
{
}),
Change.from_dict(interaction.client, {
"type": "RESOLVE",
"reason": resolved_reason,
"user_id": resolved_by,
"timestamp": resolved_timestamp,
},
}),
]
else:
resolved = 0
resolved_by = "NULL"
resolved_reason = "NULL"
changes = []
resolved = None
resolved_by = None
resolved_reason = None
changes = None
if case["reason"] and case["reason"] != "N/A":
reason = case["reason"]
else:
reason = "NULL"
reason = None
await mysql_log(
await Moderation.log(
self.ctx.guild.id,
case["executor"],
case["type"],
case["targetType"],
case["target"],
0,
None,
duration,
reason,
timestamp=timestamp,

View file

@ -9,6 +9,7 @@
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["pydantic", "aiosqlite", "phx-class-registry==5.0.0"],
"tags": [
"mod",
"moderate",

View file

@ -1,20 +1,24 @@
from discord import ButtonStyle, Interaction, Message, ui
from discord.errors import NotFound
from redbot.core import commands
from redbot.core.utils.chat_formatting import error
from aurora.utilities.config import config
from aurora.utilities.factory import addrole_embed
from ..utilities.config import config
from ..utilities.factory import addrole_embed
class Addrole(ui.View):
def __init__(self, ctx: commands.Context, message: Message, timeout: int = None):
def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None):
super().__init__()
self.ctx = ctx
self.message = message
self.timeout = timeout
async def on_timeout(self):
try:
await self.message.edit(view=None)
except NotFound:
pass
@ui.select(cls=ui.RoleSelect, placeholder="Select a role", min_values=0, max_values=25)
async def addrole_select(self, interaction: Interaction, select: ui.RoleSelect):

View file

@ -1,20 +1,24 @@
from discord import ButtonStyle, Interaction, Message, ui
from discord.errors import NotFound
from redbot.core import commands
from aurora.utilities.config import config
from aurora.utilities.factory import guild_embed
from aurora.utilities.utils import create_pagesize_options
from ..utilities.config import config
from ..utilities.factory import guild_embed
from ..utilities.utils import create_pagesize_options
class Guild(ui.View):
def __init__(self, ctx: commands.Context, message: Message, timeout: int = None):
def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None):
super().__init__()
self.ctx = ctx
self.message = message
self.timeout = timeout
async def on_timeout(self):
try:
await self.message.edit(view=None)
except NotFound:
pass
@ui.button(label="Show Moderator", style=ButtonStyle.green, row=0)
async def show_moderator(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument

View file

@ -1,20 +1,24 @@
from discord import ButtonStyle, Interaction, Message, ui
from discord.errors import NotFound
from redbot.core import commands
from redbot.core.utils.chat_formatting import error
from aurora.utilities.config import config
from aurora.utilities.factory import immune_embed
from ..utilities.config import config
from ..utilities.factory import immune_embed
class Immune(ui.View):
def __init__(self, ctx: commands.Context, message: Message, timeout: int = None):
def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None):
super().__init__()
self.ctx = ctx
self.message = message
self.timeout = timeout
async def on_timeout(self):
try:
await self.message.edit(view=None)
except NotFound:
pass
@ui.select(cls=ui.RoleSelect, placeholder="Select a role", min_values=0, max_values=25)
async def immune_select(self, interaction: Interaction, select: ui.RoleSelect):

View file

@ -1,20 +1,24 @@
from discord import ButtonStyle, Interaction, Message, ui
from discord.errors import NotFound
from redbot.core import commands
from aurora.utilities.config import config
from aurora.utilities.factory import overrides_embed
from aurora.utilities.utils import create_pagesize_options
from ..utilities.config import config
from ..utilities.factory import overrides_embed
from ..utilities.utils import create_pagesize_options
class Overrides(ui.View):
def __init__(self, ctx: commands.Context, message: Message, timeout: int = None):
def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None):
super().__init__()
self.ctx = ctx
self.message = message
self.timeout = timeout
async def on_timeout(self):
try:
await self.message.edit(view=None)
except NotFound:
pass
@ui.button(label="Auto Evidence Format", style=ButtonStyle.green, row=0)
async def auto_evidenceformat(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument

71
aurora/menus/types.py Normal file
View file

@ -0,0 +1,71 @@
from discord import ButtonStyle, Interaction, Message, ui
from discord.errors import NotFound
from redbot.core import commands
from ..models.type import Type
from ..utilities.config import config
from ..utilities.factory import type_embed
class Types(ui.View):
def __init__(self, ctx: commands.Context, message: Message, moderation_type: Type, timeout: int | None = None):
super().__init__()
self.ctx = ctx
self.message = message
self.type = moderation_type
self.timeout = timeout
async def on_timeout(self):
try:
await self.message.edit(view=None)
except NotFound:
pass
@ui.button(label="Show in History", style=ButtonStyle.green, row=0)
async def show_in_history(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
await interaction.response.defer()
current_setting = await config.custom("types", interaction.guild.id, self.type.key).show_in_history()
await config.custom("types", interaction.guild.id, self.type.key).show_in_history.set(not current_setting)
await interaction.message.edit(embed=await type_embed(self.ctx, self.type))
@ui.button(label="Show Moderator", style=ButtonStyle.green, row=0)
async def show_moderator(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
await interaction.response.defer()
current_setting = await config.custom("types", interaction.guild.id, self.type.key).show_moderator()
await config.custom("types", interaction.guild.id, self.type.key).show_moderator.set(not current_setting)
await interaction.message.edit(embed=await type_embed(self.ctx, self.type))
@ui.button(label="Use Discord Permissions", style=ButtonStyle.green, row=0)
async def use_discord_permissions(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
await interaction.response.defer()
current_setting = await config.custom("types", interaction.guild.id, self.type.key).use_discord_permissions()
await config.custom("types", interaction.guild.id, self.type.key).use_discord_permissions.set(not current_setting)
await interaction.message.edit(embed=await type_embed(self.ctx, self.type))
@ui.button(label="DM Users", style=ButtonStyle.green, row=0)
async def dm_users(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
await interaction.response.defer()
current_setting = await config.custom("types", interaction.guild.id, self.type.key).dm_users()
await config.custom("types", interaction.guild.id, self.type.key).dm_users.set(not current_setting)
await interaction.message.edit(embed=await type_embed(self.ctx, self.type))
@ui.button(label="Reset", style=ButtonStyle.red, row=1)
async def reset(self, interaction: Interaction, button: ui.Button): # pylint: disable=unused-argument
if not interaction.user.guild_permissions.manage_guild and not interaction.user.guild_permissions.administrator:
await interaction.response.send_message("You must have the manage guild permission to change this setting.", ephemeral=True)
return
await interaction.response.defer()
await config.custom("types", interaction.guild.id, self.type.key).clear()
await interaction.message.edit(embed=await type_embed(self.ctx, self.type))

View file

@ -0,0 +1,2 @@
from .moderation_types import * # noqa: F403
# This just imports all the built-in moderation types so they can be registered, as they aren't imported anywhere else.

30
aurora/models/base.py Normal file
View file

@ -0,0 +1,30 @@
from typing import Any, Optional
from discord import Guild
from pydantic import BaseModel, ConfigDict
from redbot.core.bot import Red
class AuroraBaseModel(BaseModel):
"""Base class for all models in Aurora."""
model_config = ConfigDict(ignored_types=(Red,), arbitrary_types_allowed=True)
bot: Red
def dump(self) -> dict:
return self.model_dump(exclude={"bot"})
def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs) -> str:
from ..utilities.json import dump, dumps # pylint: disable=cyclic-import
return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.dump(), indent=indent, **kwargs)
class AuroraGuildModel(AuroraBaseModel):
"""Subclass of AuroraBaseModel that includes a guild_id attribute and a guild attribute."""
model_config = ConfigDict(ignored_types=(Red, Guild), arbitrary_types_allowed=True)
guild_id: int
guild: Optional[Guild] = None
def dump(self) -> dict:
return self.model_dump(exclude={"bot", "guild_id", "guild"})
def __repr__(self) -> str:
return f"<{self.__class__.__name__} guild_id={self.guild_id}>"

83
aurora/models/change.py Normal file
View file

@ -0,0 +1,83 @@
import json
from datetime import datetime, timedelta
from typing import Literal, Optional
from redbot.core.bot import Red
from ..utilities.utils import timedelta_from_string
from .base import AuroraBaseModel
from .partials import PartialUser
class Change(AuroraBaseModel):
type: Literal["ORIGINAL", "RESOLVE", "EDIT"]
timestamp: datetime
user_id: int
reason: Optional[str] = None
duration: Optional[timedelta] = None
end_timestamp: Optional[datetime] = None
@property
def unix_timestamp(self) -> int:
return int(self.timestamp.timestamp())
@property
def unix_end_timestamp(self) -> Optional[int]:
if self.end_timestamp:
return int(self.end_timestamp.timestamp())
return None
def __str__(self):
return f"{self.type} {self.user_id} {self.reason}"
def __repr__(self) -> str:
attrs = [
('type', self.type),
('timestamp', self.timestamp),
('user_id', self.user_id),
('reason', self.reason),
('duration', self.duration),
('end_timestamp', self.end_timestamp),
]
joined = ' '.join(f'{key}={value!r}' for key, value in attrs)
return f"<{self.__class__.__name__} {joined}>"
async def get_user(self) -> "PartialUser":
return await PartialUser.from_id(self.bot, self.user_id)
@classmethod
def from_dict(cls, bot: Red, data: dict) -> "Change":
if isinstance(data, str):
data = json.loads(data)
if data.get('duration') and not isinstance(data["duration"], timedelta) and not data["duration"] == "NULL":
duration = timedelta_from_string(data["duration"])
elif data.get('duration') and isinstance(data["duration"], timedelta):
duration = data["duration"]
else:
duration = None
if data.get('end_timestamp') and not isinstance(data["end_timestamp"], datetime):
end_timestamp = datetime.fromtimestamp(data["end_timestamp"])
elif data.get('end_timestamp') and isinstance(data["end_timestamp"], datetime):
end_timestamp = data["end_timestamp"]
else:
end_timestamp = None
if not isinstance(data["timestamp"], datetime):
timestamp = datetime.fromtimestamp(data["timestamp"])
else:
timestamp = data["timestamp"]
try:
data["user_id"] = int(data["user_id"])
except ValueError:
data["user_id"] = 0
data.update({
"timestamp": timestamp,
"end_timestamp": end_timestamp,
"duration": duration
})
if "bot" in data:
del data["bot"]
return cls(bot=bot, **data)

565
aurora/models/moderation.py Normal file
View file

@ -0,0 +1,565 @@
import json
import sqlite3
from datetime import datetime, timedelta
from time import time
from typing import Dict, Iterable, List, Optional, Tuple, Union
import discord
from aiosqlite import Connection, Cursor, OperationalError, Row
from aiosqlite import connect as aiosqlite_connect
from redbot.core import data_manager
from redbot.core.bot import Red
from ..utilities.logger import logger
from ..utilities.utils import timedelta_to_string
from .base import AuroraGuildModel
from .change import Change
from .partials import PartialChannel, PartialRole, PartialUser
from .type import Type, type_registry
class Moderation(AuroraGuildModel):
"""This class represents a moderation case in the database.
Attributes:
bot (Red): The bot instance.
guild (discord.Guild): The guild the case belongs to.
moderation_id (int): The ID of the moderation case.
timestamp (datetime): The timestamp of the case.
moderation_type (Type): The type of moderation case.
target_type (str): The type of target. Should be either `user` or `channel`.
target_id (int): The ID of the target.
moderator_id (int): The ID of the moderator who issued the case.
role_id (int): The ID of the role, if applicable.
duration (timedelta): The duration of the case, if applicable.
end_timestamp (datetime): The end timestamp of the case, if applicable.
reason (str): The reason for the case.
resolved (bool): Whether the case is resolved.
resolved_by (int): The ID of the user who resolved the case.
resolve_reason (str): The reason the case was resolved.
expired (bool): Whether the case is expired.
changes (List[Change]): A list of changes to the case.
metadata (Dict): A dictionary of metadata stored with the case.
Properties:
id (int): The ID of the case.
type (Type): The type of the case.
unix_timestamp (int): The timestamp of the case as a Unix timestamp.
Methods:
get_moderator: Gets the moderator who issued the case.
get_target: Gets the target of the case.
get_resolved_by: Gets the user who resolved the case.
get_role: Gets the role, if applicable.
resolve: Resolves the case.
update: Updates the case in the database.
Class Methods:
from_dict: Creates a `Moderation` object from a dictionary.
from_result: Creates a `Moderation` object from a database result.
execute: Executes a query on the database.
get_latest: Gets the latest cases from the database.
get_next_case_number: Gets the next case number to use.
find_by_id: Finds a case by its ID.
find_by_target: Finds cases by the target.
find_by_moderator: Finds cases by the moderator.
log: Logs a moderation case in the database.
Static Methods:
connect: Connects to the SQLite database.
"""
moderation_id: int
timestamp: datetime
moderation_type: Type
target_type: str
target_id: int
moderator_id: int
role_id: Optional[int] = None
duration: Optional[timedelta] = None
end_timestamp: Optional[datetime] = None
reason: Optional[str] = None
resolved: bool
resolved_by: Optional[int] = None
resolve_reason: Optional[str] = None
expired: bool
changes: List["Change"]
metadata: Dict
@property
def id(self) -> int:
return self.moderation_id
@property
def type(self) -> Type:
return self.moderation_type
@property
def unix_timestamp(self) -> int:
return int(self.timestamp.timestamp())
async def get_moderator(self) -> "PartialUser":
return await PartialUser.from_id(self.bot, self.moderator_id)
async def get_target(self) -> Union["PartialUser", "PartialChannel"]:
if self.target_type.lower() == "user":
return await PartialUser.from_id(self.bot, self.target_id)
return await PartialChannel.from_id(self.bot, self.target_id, self.guild)
async def get_resolved_by(self) -> Optional["PartialUser"]:
if self.resolved_by:
return await PartialUser.from_id(self.bot, self.resolved_by)
return None
async def get_role(self) -> Optional["PartialRole"]:
if self.role_id:
return await PartialRole.from_id(self.bot, self.guild, self.role_id)
return None
def __str__(self) -> str:
return f"{self.moderation_type} {self.target_type} {self.target_id} {self.reason}"
def __int__(self) -> int:
return self.moderation_id
def __repr__(self) -> str:
attrs = [
('guild_id', self.guild_id),
('moderation_id', self.moderation_id),
('timestamp', self.timestamp),
('type', self.type),
('target_type', self.target_type),
('target_id', self.target_id),
('moderator_id', self.moderator_id),
('role_id', self.role_id),
('duration', self.duration),
('end_timestamp', self.end_timestamp),
('reason', self.reason),
('resolved', self.resolved),
('resolved_by', self.resolved_by),
('resolve_reason', self.resolve_reason),
('expired', self.expired),
('changes', self.changes),
('metadata', self.metadata),
]
joined = ' '.join(f'{key}={value!r}' for key, value in attrs)
return f"<{self.__class__.__name__} {joined}>"
async def resolve(self, resolved_by: int, reason: str) -> Tuple[bool, str]:
if self.resolved:
raise ValueError("Case is already resolved!")
self.resolved = True
self.resolved_by = resolved_by
self.resolve_reason = reason
success, msg = await self.type.resolve_handler(moderation=self, reason=reason)
if not self.changes:
self.changes.append(Change.from_dict(self.bot, {
"type": "ORIGINAL",
"timestamp": self.timestamp,
"reason": self.reason,
"user_id": self.moderator_id,
"duration": self.duration,
"end_timestamp": self.end_timestamp,
}))
self.changes.append(Change.from_dict(self.bot, {
"type": "RESOLVE",
"timestamp": datetime.now(),
"reason": reason,
"user_id": resolved_by,
}))
await self.update()
return success, msg
async def update(self) -> None:
from ..utilities.json import dumps
query = f"UPDATE moderation_{self.guild_id} SET timestamp = ?, moderation_type = ?, target_type = ?, moderator_id = ?, role_id = ?, duration = ?, end_timestamp = ?, reason = ?, resolved = ?, resolved_by = ?, resolve_reason = ?, expired = ?, changes = ?, metadata = ? WHERE moderation_id = ?;"
await self.execute(query, (
self.timestamp.timestamp(),
self.moderation_type.key,
self.target_type,
self.moderator_id,
self.role_id,
timedelta_to_string(self.duration) if self.duration else None,
self.end_timestamp.timestamp() if self.end_timestamp else None,
self.reason,
self.resolved,
self.resolved_by,
self.resolve_reason,
self.expired,
dumps(self.changes),
dumps(self.metadata),
self.moderation_id,
))
logger.verbose("Row updated in moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
self.moderation_id,
self.guild_id,
self.timestamp.timestamp(),
self.moderation_type.key,
self.target_type,
self.moderator_id,
self.role_id,
timedelta_to_string(self.duration) if self.duration else None,
self.end_timestamp.timestamp() if self.end_timestamp else None,
self.reason,
self.resolved,
self.resolved_by,
self.resolve_reason,
self.expired,
dumps(self.changes),
dumps(self.metadata),
)
@classmethod
async def from_dict(cls, bot: Red, data: dict) -> "Moderation":
if data.get("guild_id"):
try:
guild = bot.get_guild(data["guild_id"])
if not guild:
guild = await bot.fetch_guild(data["guild_id"])
except (discord.Forbidden, discord.HTTPException):
guild = None
data.update({"guild": guild})
return cls(bot=bot, **data)
@classmethod
async def from_result(cls, bot: Red, result: Iterable, guild_id: int) -> "Moderation":
if result[7] is not None and result[7] != "NULL":
try:
hours, minutes, seconds = map(int, result[7].split(':'))
duration = timedelta(hours=hours, minutes=minutes, seconds=seconds)
except ValueError as e:
logger.error("Error parsing duration for case %s: %s", result[0], result[7])
raise e
else:
duration = None
if result[14] is not None:
changes = json.loads(result[14])
change_obj_list = []
if changes:
for change in changes:
change_obj_list.append(Change.from_dict(bot=bot, data=change))
if result[15] is not None:
metadata = json.loads(result[15])
else:
metadata = {}
moderation_type = str.lower(result[2])
if moderation_type in type_registry:
moderation_type = type_registry[moderation_type]
else:
logger.error("Unknown moderation type in case %s: %s", result[0], result[2])
case = {
"moderation_id": int(result[0]),
"guild_id": int(guild_id),
"timestamp": datetime.fromtimestamp(result[1]),
"moderation_type": moderation_type,
"target_type": str(result[3]),
"target_id": int(result[4]),
"moderator_id": int(result[5]),
"role_id": int(result[6]) if result[6] is not None else None,
"duration": duration,
"end_timestamp": datetime.fromtimestamp(result[8]) if result[8] is not None else None,
"reason": result[9],
"resolved": bool(result[10]),
"resolved_by": result[11],
"resolve_reason": result[12],
"expired": bool(result[13]),
"changes": change_obj_list,
"metadata": metadata if metadata else {},
}
return await cls.from_dict(bot=bot, data=case)
@staticmethod
async def connect() -> Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = await aiosqlite_connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
except OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
) from e
@classmethod
async def execute(cls, query: str, parameters: tuple | None = None, bot: Red | None = None, guild_id: int | None = None, cursor: Cursor | None = None, return_obj: bool = True) -> Union[Tuple["Moderation"], Iterable[Row]]:
"""Executes a query on the database.
Arguments:
query (str): The query to execute.
parameters (tuple): The parameters to pass to the query.
bot (Red): The bot instance.
guild_id (int): The ID of the guild to execute the query on.
cursor (Cursor): The cursor to use for the query.
return_obj (bool): Whether to return the case object(s). Defaults to `True`. If `False`, returns a `Iterable` of `aiosqlite.Row` objects.
Returns: The result of the query, either as a `Tuple` of `Moderation` objects or an `Iterable` of `aiosqlite.Row` objects.
"""
logger.trace("Executing query: \"%s\" with parameters \"%s\"", query, parameters)
if not parameters:
parameters = ()
if not cursor:
no_cursor = True
database = await cls.connect()
cursor = await database.cursor()
else:
no_cursor = False
try:
await cursor.execute(query, parameters)
except OperationalError as e:
logger.error("Error executing query: \"%s\" with parameters \"%s\"\nError:\n%s",
query, parameters, e)
raise OperationalError(f"Error executing query: \"{query}\" with parameters \"{parameters}\"") from e
results = await cursor.fetchall()
await database.commit()
if no_cursor:
await cursor.close()
await database.close()
if results and return_obj and bot and guild_id:
cases = []
for result in results:
if result[0] == 0:
continue
case = await cls.from_result(bot=bot, result=result, guild_id=guild_id)
cases.append(case)
return tuple(cases)
return results
@classmethod
async def get_latest(cls, bot: Red, guild_id: int, before: datetime | None = None, after: datetime | None = None, limit: int | None = None, offset: int = 0, types: Iterable[Type] | None = None, expired: bool | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
params = []
query = f"SELECT * FROM moderation_{guild_id}"
conditions = []
if types:
conditions.append(f"moderation_type IN ({', '.join(['?' for _ in types])})")
params.extend([t.key for t in types])
if before:
conditions.append("timestamp < ?")
params.append(int(before.timestamp()))
if after:
conditions.append("timestamp > ?")
params.append(int(after.timestamp()))
if expired is not None:
conditions.append("expired = ?")
params.append(int(expired))
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY moderation_id DESC"
if limit:
query += " LIMIT ? OFFSET ?"
params.extend((limit, offset))
query += ";"
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor)
@classmethod
async def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int:
result = await cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1)
return (result[0].moderation_id + 1) if result else 1
@classmethod
async def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation":
query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;"
case = await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor)
if case:
return case[0]
raise ValueError(f"Case {moderation_id} not found in moderation_{guild_id}!")
@classmethod
async def find_by_target(cls, bot: Red, guild_id: int, target: int, before: datetime = None, after: datetime = None, types: Iterable[Type] | None = None, expired: bool | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE target_id = ?"
params = [target]
if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
for t in types:
params.append(t.key)
if before:
query += " AND timestamp < ?"
params.append(int(before.timestamp()))
if after:
query += " AND timestamp > ?"
params.append(int(after.timestamp()))
if expired is not None:
query += " AND expired = ?"
params.append(int(expired))
query += " ORDER BY moderation_id DESC;"
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=params, cursor=cursor)
@classmethod
async def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, before: datetime = None, after: datetime = None, types: Iterable[Type] | None = None, expired: bool | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE moderator_id = ?"
params = [moderator]
if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
for t in types:
params.append(t.key)
if before:
query += " AND timestamp < ?"
params.append(int(before.timestamp()))
if after:
query += " AND timestamp > ?"
params.append(int(after.timestamp()))
if expired is not None:
query += " AND expired = ?"
params.append(int(expired))
query += " ORDER BY moderation_id DESC;"
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=params, cursor=cursor)
@classmethod
async def log(
cls,
bot: Red,
guild_id: int,
moderator_id: int,
moderation_type: Type,
target_type: str,
target_id: int,
role_id: int | None = None,
duration: timedelta | None = None,
reason: str | None = None,
database: sqlite3.Connection | None = None,
timestamp: datetime | None = None,
resolved: bool = False,
resolved_by: int | None = None,
resolved_reason: str | None = None,
expired: bool | None = None,
changes: list | None = None,
metadata: dict | None = None,
return_obj: bool = True,
) -> Union["Moderation", int]:
"""Logs a moderation case in the database.
Args:
bot (Red): The bot instance.
guild_id (int): The ID of the guild to log the case in.
moderator_id (int): The ID of the moderator who issued the case.
moderation_type (Type): The type of moderation case. See `aurora.models.moderation_types` for the built-in options.
target_type (str): The type of target. Should be either `user` or `channel`.
target_id (int): The ID of the target.
role_id (int): The ID of the role, if applicable.
duration (timedelta): The duration of the case, if applicable.
reason (str): The reason for the case.
database (sqlite3.Connection): The database connection to use to log the case. A connection will be automatically created if not provided.
timestamp (datetime): The timestamp of the case. Will be automatically generated if not provided.
resolved (bool): Whether the case is resolved.
resolved_by (int): The ID of the user who resolved the case.
resolved_reason (str): The reason the case was resolved.
expired (bool): Whether the case is expired.
changes (list): A list of changes to log. You usually shouldn't pass this, as it's automatically generated by the `/edit` and `/resolve` commands.
metadata (dict): A dictionary of metadata to store with the case.
return_obj (bool): Whether to return the case object. Defaults to `True`. If `False`, returns the case ID.
Returns:
Union[Moderation, int]: The `Moderation` object if `return_obj` is `True`, otherwise the case ID.
"""
from ..utilities.json import dumps
if not timestamp:
timestamp = datetime.fromtimestamp(time())
elif not isinstance(timestamp, datetime):
timestamp = datetime.fromtimestamp(timestamp)
if duration == "NULL":
duration = None
if duration is not None:
end_timestamp = timestamp + duration
else:
duration = None
end_timestamp = None
if not expired:
if end_timestamp:
expired = bool(timestamp > end_timestamp)
else:
expired = False
if reason == "NULL":
reason = None
if resolved_by in ["NULL", "?"]:
resolved_by = None
if resolved_reason == "NULL":
resolved_reason = None
if role_id == 0:
role_id = None
if not database:
database = await cls.connect()
close_db = True
else:
close_db = False
moderation_id = await cls.get_next_case_number(bot=bot, guild_id=guild_id)
case = {
"moderation_id": moderation_id,
"timestamp": timestamp.timestamp(),
"moderation_type": moderation_type.key,
"target_type": target_type,
"target_id": target_id,
"moderator_id": moderator_id,
"role_id": role_id,
"duration": timedelta_to_string(duration) if duration else None,
"end_timestamp": end_timestamp.timestamp() if end_timestamp else None,
"reason": reason,
"resolved": resolved,
"resolved_by": resolved_by,
"resolve_reason": resolved_reason,
"expired": expired,
"changes": dumps(changes),
"metadata": dumps(metadata)
}
sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
await database.execute(sql, tuple(case.values()))
await database.commit()
if close_db:
await database.close()
logger.verbose(
"Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
guild_id,
case["moderation_id"],
case["timestamp"],
case["moderation_type"],
case["target_type"],
case["target_id"],
case["moderator_id"],
case["role_id"],
case["duration"],
case["end_timestamp"],
case["reason"],
case["resolved"],
case["resolved_by"],
case["resolve_reason"],
case["expired"],
case["changes"],
case["metadata"],
)
if return_obj:
return await cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id)
return moderation_id

File diff suppressed because it is too large Load diff

89
aurora/models/partials.py Normal file
View file

@ -0,0 +1,89 @@
from discord import ChannelType, Forbidden, Guild, HTTPException, InvalidData, NotFound, Role, User
from discord.abc import Messageable
from redbot.core.bot import Red
from .base import AuroraBaseModel, AuroraGuildModel
class PartialUser(AuroraBaseModel):
id: int
username: str
discriminator: int
_obj: User | None
@property
def name(self):
return f"{self.username}#{self.discriminator}" if self.discriminator != 0 else self.username
def __str__(self):
return self.name
def __repr__(self):
return f"<{self.__class__.__name__} id={self.id}>"
@classmethod
async def from_id(cls, bot: Red, user_id: int) -> "PartialUser":
user = bot.get_user(user_id)
if not user:
try:
user = await bot.fetch_user(user_id)
return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator, _obj=user)
except NotFound:
return cls(bot=bot, id=user_id, username="Deleted User", discriminator=0, _obj=None)
return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator, _obj=user)
class PartialChannel(AuroraGuildModel):
id: int
name: str
type: ChannelType
_obj: Messageable | None
@property
def mention(self):
if self.name in ["Deleted Channel", "Forbidden Channel"]:
return self.name
return f"<#{self.id}>"
def __str__(self):
return self.mention
def __repr__(self):
return f"<{self.__class__.__name__} id={self.id} guild_id={self.guild_id}>"
@classmethod
async def from_id(cls, bot: Red, channel_id: int, guild: Guild) -> "PartialChannel":
channel = bot.get_channel(channel_id)
if not channel:
try:
channel = await bot.fetch_channel(channel_id)
return cls(bot=bot, guild_id=channel.guild.id, guild=guild, id=channel.id, name=channel.name, type=channel.type, _obj=channel)
except (NotFound, InvalidData, HTTPException, Forbidden) as e:
if e == Forbidden:
return cls(bot=bot, guild_id=0, id=channel_id, name="Forbidden Channel")
return cls(bot=bot, guild_id=0, id=channel_id, name="Deleted Channel", type=ChannelType.text, _obj=None)
return cls(bot=bot, guild_id=channel.guild.id, guild=guild, id=channel.id, name=channel.name, type=channel.type, _obj=channel)
class PartialRole(AuroraGuildModel):
id: int
name: str
_obj: Role | None
@property
def mention(self):
if self.name in ["Deleted Role", "Forbidden Role"]:
return self.name
return f"<@&{self.id}>"
def __str__(self):
return self.mention
def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} guild_id={self.guild_id}>"
@classmethod
async def from_id(cls, bot: Red, guild: Guild, role_id: int) -> "PartialRole":
role = guild.get_role(role_id)
if not role:
return cls(bot=bot, guild_id=guild.id, id=role_id, name="Deleted Role", _obj=None)
return cls(bot=bot, guild_id=guild.id, id=role.id, name=role.name, _obj=role)

97
aurora/models/type.py Normal file
View file

@ -0,0 +1,97 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, Tuple
from class_registry import ClassRegistry
from class_registry.base import AutoRegister
from discord import Interaction, Member, User
from discord.abc import Messageable
from redbot.core import commands
type_registry: Dict['str', 'Type'] = ClassRegistry(attr_name='key', unique=True)
class Type(AutoRegister(type_registry), ABC):
"""This is a base class for moderation types.
Attributes:
key (str): The key to use for this type. This should be unique, as this is how the type is registered internally. Changing this key will break existing cases with this type.
string (str): The string to display for this type.
verb (str): The verb to use for this type.
embed_desc (str): The string to use for embed descriptions.
channel (bool): Whether this type targets channels or users. If this is `true` in a subclass, its overridden handler methods should be typed with `discord.abc.Messageable` instead of `discord.Member | discord.User`.
removes_from_guild (bool): Whether this type's handler removes the target from the guild, or if the moderation is expected to occur whenever the user is not in the guild. This does not actually remove the target from the guild, the handler method is responsible for that.
Properties:
name (str): The string to display for this type. This is the same as the `string` attribute.
"""
key = "type"
string = "type"
verb = "typed"
embed_desc = "been "
channel = False
removes_from_guild = False
@abstractmethod
def void(self) -> Any:
"""This method should be overridden by any child classes. This is a placeholder to allow for automatic class registration."""
raise NotImplementedError
@property
def name(self) -> str:
"""Alias for the `string` attribute."""
return self.string
def __str__(self) -> str:
return self.string
def __repr__(self) -> str:
attrs = [
('key', self.key),
('channel', self.channel),
]
joined = ' '.join(f'{key}={value!r}' for key, value in attrs)
return f"<{self.__class__.__name__} {joined}>"
@classmethod
async def handler(cls, ctx: commands.Context, target: Member | User | Messageable, silent: bool, **kwargs) -> 'Type': # pylint: disable=unused-argument
"""This method should be overridden by any child classes, but should retain the same starting keyword arguments.
Arguments:
ctx (commands.Context): The context of the command.
target (discord.Member | discord.User | discord.abc.Messageable): The target of the moderation.
silent (bool): Whether details about the moderation should be DM'ed to the target of the moderation.
"""
raise NotImplementedError
@classmethod
async def resolve_handler(cls, moderation, reason: str) -> Tuple[bool, str]: # pylint: disable=unused-argument
"""This method should be overridden by any resolvable child classes, but should retain the same keyword arguments.
If your moderation type should not be resolvable, do not override this.
Arguments:
moderation (aurora.models.Moderation): The moderation to resolve.
reason (str): The reason for resolving the moderation.
"""
raise NotImplementedError
@classmethod
async def expiry_handler(cls, moderation) -> int: # pylint: disable=unused-argument
"""This method should be overridden by any expirable child classes, but should retain the same keyword arguments and return an integer.
If your moderation type should not expire, do not override this, but also do not set an `end_timestamp` when you log your moderation.
Arguments:
moderation (aurora.models.Moderation): The moderation that is expiring.
"""
raise NotImplementedError
@classmethod
async def duration_edit_handler(cls, interaction: Interaction, old_moderation, new_moderation) -> bool: # pylint: disable=unused-argument
"""This method should be overridden by any child classes with editable durations, but should retain the same keyword arguments and should return True if the duration was successfully modified, or False if it was not.
If your moderation type's duration should not be editable, do not override this.
Arguments:
interaction (discord.Interaction): The interaction that triggered the duration edit.
old_moderation (aurora.models.Moderation): The old moderation, from before the `/edit` command was invoked.
new_moderation (aurora.models.Moderation): The current state of the moderation.
"""
raise NotImplementedError

View file

@ -27,3 +27,13 @@ def register_config(config_obj: Config):
history_inline_pagesize=None,
auto_evidenceformat=None,
)
moderation_type = {
"show_in_history": True,
"show_moderator": None,
"use_discord_permissions": None,
"dm_users": None,
}
config_obj.init_custom("types", 2)
config_obj.register_custom("types", **moderation_type)

View file

@ -1,219 +0,0 @@
# pylint: disable=cyclic-import
import json
import sqlite3
import time
from datetime import datetime, timedelta
from discord import Guild
from redbot.core import data_manager
from .logger import logger
from .utils import (convert_timedelta_to_str, generate_dict,
get_next_case_number)
def connect() -> sqlite3.Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = sqlite3.connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
except sqlite3.OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
) from e
async def create_guild_table(guild: Guild):
database = connect()
cursor = database.cursor()
try:
cursor.execute(f"SELECT * FROM `moderation_{guild.id}`")
logger.debug("SQLite Table exists for server %s (%s)", guild.name, guild.id)
except sqlite3.OperationalError:
query = f"""
CREATE TABLE `moderation_{guild.id}` (
moderation_id INTEGER PRIMARY KEY,
timestamp INTEGER NOT NULL,
moderation_type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
role_id TEXT,
duration TEXT,
end_timestamp INTEGER,
reason TEXT,
resolved INTEGER NOT NULL,
resolved_by TEXT,
resolve_reason TEXT,
expired INTEGER NOT NULL,
changes TEXT NOT NULL,
metadata TEXT NOT NULL
)
"""
cursor.execute(query)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
cursor.execute(index_query_1)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
cursor.execute(index_query_2)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
cursor.execute(index_query_3)
insert_query = f"""
INSERT INTO `moderation_{guild.id}`
(moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
insert_values = (
0,
0,
"NULL",
"NULL",
0,
0,
0,
"NULL",
0,
"NULL",
0,
"NULL",
"NULL",
0,
json.dumps([]),
json.dumps({}),
)
cursor.execute(insert_query, insert_values)
database.commit()
logger.debug(
"SQLite Table (moderation_%s) created for %s (%s)",
guild.id,
guild.name,
guild.id,
)
database.close()
async def mysql_log(
guild_id: str,
author_id: str,
moderation_type: str,
target_type: str,
target_id: int,
role_id: int,
duration: timedelta,
reason: str,
database: sqlite3.Connection = None,
timestamp: int = None,
resolved: bool = False,
resolved_by: str = None,
resolved_reason: str = None,
expired: bool = None,
changes: list = None,
metadata: dict = None,
) -> int:
if not timestamp:
timestamp = int(time.time())
if duration != "NULL":
end_timedelta = datetime.fromtimestamp(timestamp) + duration
end_timestamp = int(end_timedelta.timestamp())
duration = convert_timedelta_to_str(duration)
else:
end_timestamp = 0
if not expired:
if int(time.time()) > end_timestamp:
expired = 1
else:
expired = 0
if resolved_by is None:
resolved_by = "NULL"
if resolved_reason is None:
resolved_reason = "NULL"
if not database:
database = connect()
close_db = True
else:
close_db = False
cursor = database.cursor()
moderation_id = await get_next_case_number(guild_id=guild_id, cursor=cursor)
sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
val = (
moderation_id,
timestamp,
moderation_type,
target_type,
target_id,
author_id,
role_id,
duration,
end_timestamp,
reason,
int(resolved),
resolved_by,
resolved_reason,
expired,
json.dumps(changes if changes else []),
json.dumps(metadata if metadata else {}),
)
cursor.execute(sql, val)
cursor.close()
database.commit()
if close_db:
database.close()
logger.debug(
"Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
guild_id,
moderation_id,
timestamp,
moderation_type,
target_type,
target_id,
author_id,
role_id,
duration,
end_timestamp,
reason,
int(resolved),
resolved_by,
resolved_reason,
expired,
changes,
metadata,
)
return moderation_id
async def fetch_case(moderation_id: int, guild_id: str) -> dict:
"""This method fetches a case from the database and returns the case's dictionary."""
database = connect()
cursor = database.cursor()
query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;"
cursor.execute(query, (moderation_id,))
result = cursor.fetchone()
cursor.close()
database.close()
return generate_dict(result)

View file

@ -2,78 +2,68 @@
from datetime import datetime, timedelta
from typing import Union
from discord import Color, Embed, Guild, Interaction, InteractionMessage, Member, Role, User
from discord import Color, Embed, Guild, Interaction, Member, Message, Role, User
from redbot.core import commands
from redbot.core.utils.chat_formatting import bold, box, error, humanize_timedelta, warning
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import bold, box, error, humanize_timedelta, inline, warning
from aurora.utilities.config import config
from aurora.utilities.utils import fetch_channel_dict, fetch_user_dict, get_bool_emoji, get_next_case_number, get_pagesize_str
from ..models.moderation import Moderation
from ..models.partials import PartialUser
from ..models.type import Type
from .config import config
from .utils import get_bool_emoji, get_pagesize_str
async def message_factory(
bot: Red,
color: Color,
guild: Guild,
reason: str,
moderation_type: str,
moderator: Union[Member, User] = None,
duration: timedelta = None,
response: InteractionMessage = None,
role: Role = None,
moderation_type: Type,
moderator: Union[Member, User] | None = None,
duration: timedelta | None = None,
response: Message | None = None,
case: bool = True,
) -> Embed:
"""This function creates a message from set parameters, meant for contacting the moderated user.
Args:
bot (Red): The bot instance.
color (Color): The color of the embed.
guild (Guild): The guild the moderation occurred in.
reason (str): The reason for the moderation.
moderation_type (str): The type of moderation.
moderation_type (Type): The type of moderation.
moderator (Union[Member, User], optional): The moderator who performed the moderation. Defaults to None.
duration (timedelta, optional): The duration of the moderation. Defaults to None.
response (InteractionMessage, optional): The response message. Defaults to None.
role (Role, optional): The role that was added or removed. Defaults to None.
response (Message, optional): The response message. Defaults to None.
case (bool, optional): Whether the message is for a moderation case. Defaults to True.
Returns:
embed: The message embed.
"""
if response is not None and moderation_type not in [
"kicked",
"banned",
"tempbanned",
"unbanned",
]:
if response is not None and not moderation_type.removes_from_guild:
guild_name = f"[{guild.name}]({response.jump_url})"
else:
guild_name = guild.name
title = moderation_type
if moderation_type in ["tempbanned", "muted"] and duration:
if duration:
embed_duration = f" for {humanize_timedelta(timedelta=duration)}"
else:
embed_duration = ""
if moderation_type == "note":
embed_desc = "received a"
elif moderation_type == "addrole":
embed_desc = f"received the {role.name} role"
title = "Role Added"
moderation_type = ""
elif moderation_type == "removerole":
embed_desc = f"lost the {role.name} role"
title = "Role Removed"
moderation_type = ""
else:
embed_desc = "been"
embed = Embed(
title=str.title(title),
description=f"You have {embed_desc} {moderation_type}{embed_duration} in {guild_name}.",
title=str.title(moderation_type.verb),
description=f"You have {moderation_type.embed_desc}{moderation_type.verb}{embed_duration} in {guild_name}.",
color=color,
timestamp=datetime.now(),
)
if await config.guild(guild).show_moderator() and moderator is not None:
show_moderator = await config.custom("types", guild.id, moderation_type.key).show_moderator()
if show_moderator is None:
show_moderator = await config.guild(guild).show_moderator()
if show_moderator and moderator is not None:
embed.add_field(
name="Moderator", value=f"`{moderator.name} ({moderator.id})`", inline=False
)
@ -85,265 +75,211 @@ async def message_factory(
else:
embed.set_author(name=guild.name)
if case:
embed.set_footer(
text=f"Case #{await get_next_case_number(guild.id):,}",
text=f"Case #{await Moderation.get_next_case_number(bot=bot, guild_id=guild.id):,}",
icon_url="attachment://arrow.png",
)
return embed
async def resolve_factory(moderation: Moderation, reason: str) -> Embed:
"""This function creates a resolved embed from set parameters, meant for contacting the moderated user.
Args:
moderation (aurora.models.Moderation): The moderation object.
reason (str): The reason for resolving the moderation.
Returns: `discord.Embed`
"""
embed = Embed(
title=str.title(moderation.type.name) + " Resolved",
description=f"Your {moderation.type.name} in {moderation.guild.name} has been resolved.",
color=await moderation.bot.get_embed_color(moderation.guild.channels[0]),
timestamp=datetime.now(),
)
embed.add_field(name="Reason", value=f"`{reason}`", inline=False)
if moderation.guild.icon.url is not None:
embed.set_author(name=moderation.guild.name, icon_url=moderation.guild.icon.url)
else:
embed.set_author(name=moderation.guild.name)
embed.set_footer(
text=f"Case #{moderation.id:,}",
icon_url="attachment://arrow.png",
)
return embed
async def log_factory(
interaction: Interaction, case_dict: dict, resolved: bool = False
ctx: commands.Context, moderation: Moderation, resolved: bool = False
) -> Embed:
"""This function creates a log embed from set parameters, meant for moderation logging.
Args:
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
ctx (commands.Context): The ctx object.
moderation (aurora.models.Moderation): The moderation object.
resolved (bool, optional): Whether the case is resolved or not. Defaults to False.
"""
target = await moderation.get_target()
moderator = await moderation.get_moderator()
if resolved:
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
target_name = f"{target_user['mention']}"
else:
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,} Resolved",
color=await interaction.client.get_embed_color(interaction.channel),
title=f"📕 Case #{moderation.id:,} Resolved",
color=await ctx.bot.get_embed_color(ctx.channel),
)
embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** <t:{case_dict['timestamp']}> | <t:{case_dict['timestamp']}:R>"
resolved_by = await moderation.get_resolved_by()
embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if case_dict["duration"] != "NULL":
td = timedelta(
**{
unit: int(val)
for unit, val in zip(
["hours", "minutes", "seconds"],
case_dict["duration"].split(":"),
)
}
)
if moderation.duration is not None:
duration_embed = (
f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
if case_dict["expired"] == "0"
else str(humanize_timedelta(timedelta=td))
f"{humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.end_timestamp}:R>"
if not moderation.expired
else str(humanize_timedelta(timedelta=moderation.duration))
)
embed.description = (
embed.description
+ f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}"
+ f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}"
)
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
if moderation.metadata.items():
for key, value in moderation.metadata.items():
embed.description += f"\n**{key.title()}:** {value}"
embed.add_field(name="Reason", value=box(moderation.reason), inline=False)
resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"])
resolved_name = (
resolved_user["name"]
if resolved_user["discriminator"] == "0"
else f"{resolved_user['name']}#{resolved_user['discriminator']}"
)
embed.add_field(
name="Resolve Reason",
value=f"Resolved by `{resolved_name}` ({resolved_user['id']}) for:\n"
+ box(case_dict["resolve_reason"]),
value=f"Resolved by `{resolved_by.name}` ({resolved_by.id}) for:\n"
+ box(moderation.resolve_reason),
inline=False,
)
else:
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
target_name = target_user["mention"]
else:
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,}",
color=await interaction.client.get_embed_color(interaction.channel),
title=f"📕 Case #{moderation.id:,}",
color=await ctx.bot.get_embed_color(ctx.channel),
)
embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** <t:{case_dict['timestamp']}> | <t:{case_dict['timestamp']}:R>"
embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if case_dict["duration"] != "NULL":
td = timedelta(
**{
unit: int(val)
for unit, val in zip(
["hours", "minutes", "seconds"],
case_dict["duration"].split(":"),
)
}
)
if moderation.duration:
embed.description = (
embed.description
+ f"\n**Duration:** {humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
+ f"\n**Duration:** {humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.unix_timestamp}:R>"
)
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
if moderation.metadata.items():
for key, value in moderation.metadata.items():
embed.description += f"\n**{key.title()}:** {value}"
embed.add_field(name="Reason", value=box(moderation.reason), inline=False)
return embed
async def case_factory(interaction: Interaction, case_dict: dict) -> Embed:
async def case_factory(interaction: Interaction, moderation: Moderation) -> Embed:
"""This function creates a case embed from set parameters.
Args:
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
interaction (discord.Interaction): The interaction object.
moderation (aurora.models.Moderation): The moderation object.
"""
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
f"`{target_user['name']}`"
if target_user["discriminator"] == "0"
else f"`{target_user['name']}#{target_user['discriminator']}`"
)
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
if target_user["mention"]:
target_name = f"{target_user['mention']}"
else:
target_name = f"`{target_user['name']}`"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
f"`{moderator_user['name']}`"
if moderator_user["discriminator"] == "0"
else f"`{moderator_user['name']}#{moderator_user['discriminator']}`"
)
target = await moderation.get_target()
moderator = await moderation.get_moderator()
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,}",
title=f"📕 Case #{moderation.id:,}",
color=await interaction.client.get_embed_color(interaction.channel),
)
embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Resolved:** {bool(case_dict['resolved'])}\n**Timestamp:** <t:{case_dict['timestamp']}> | <t:{case_dict['timestamp']}:R>"
embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** `{target.name}` ({target.id})\n**Moderator:** `{moderator.name}` ({moderator.id})\n**Resolved:** {moderation.resolved}\n**Timestamp:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if case_dict["duration"] != "NULL":
td = timedelta(
**{
unit: int(val)
for unit, val in zip(
["hours", "minutes", "seconds"], case_dict["duration"].split(":")
)
}
)
if moderation.duration:
duration_embed = (
f"{humanize_timedelta(timedelta=td)} | <t:{case_dict['end_timestamp']}:R>"
if bool(case_dict["expired"]) is False
else str(humanize_timedelta(timedelta=td))
f"{humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.unix_timestamp}:R>"
if moderation.expired is False
else str(humanize_timedelta(timedelta=moderation.duration))
)
embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}"
embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}"
embed.description += (
f"\n**Changes:** {len(case_dict['changes']) - 1}"
if case_dict["changes"]
f"\n**Changes:** {len(moderation.changes) - 1}"
if moderation.changes
else "\n**Changes:** 0"
)
if case_dict["role_id"]:
embed.description += f"\n**Role:** <@&{case_dict['role_id']}>"
if moderation.role_id:
role = await moderation.get_role()
embed.description += f"\n**Role:** {role.name}"
if case_dict["metadata"]:
if case_dict["metadata"]["imported_from"]:
if moderation.metadata:
if moderation.metadata.get("imported_from"):
embed.description += (
f"\n**Imported From:** {case_dict['metadata']['imported_from']}"
f"\n**Imported From:** {moderation.metadata['imported_from']}"
)
embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False)
if case_dict["resolved"] == 1:
resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"])
resolved_name = (
f"`{resolved_user['name']}`"
if resolved_user["discriminator"] == "0"
else f"`{resolved_user['name']}#{resolved_user['discriminator']}`"
moderation.metadata.pop("imported_from")
if moderation.metadata.get("imported_timestamp"):
embed.description += (
f"\n**Imported Timestamp:** <t:{moderation.metadata['imported_timestamp']}> | <t:{moderation.metadata['imported_timestamp']}:R>"
)
moderation.metadata.pop("imported_timestamp")
if moderation.metadata.items():
for key, value in moderation.metadata.items():
embed.description += f"\n**{key.title()}:** {value}"
embed.add_field(name="Reason", value=box(moderation.reason), inline=False)
if moderation.resolved:
resolved_user = await moderation.get_resolved_by()
if not resolved_user:
resolved_user = PartialUser(bot=interaction.client, id=0, username="Deleted User", discriminator="0")
embed.add_field(
name="Resolve Reason",
value=f"Resolved by {resolved_name} ({resolved_user['id']}) for:\n{box(case_dict['resolve_reason'])}",
value=f"Resolved by `{resolved_user.name or 'Deleted User'}` ({resolved_user.id or '0'}) for:\n{box(moderation.resolve_reason)}",
inline=False,
)
return embed
async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed:
async def changes_factory(interaction: Interaction, moderation: Moderation) -> Embed:
"""This function creates a changes embed from set parameters.
Args:
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
interaction (discord.Interaction): The interaction object.
moderation (aurora.models.Moderation): The moderation object.
"""
embed = Embed(
title=f"📕 Case #{case_dict['moderation_id']:,} Changes",
title=f"📕 Case #{moderation.id:,} Changes",
color=await interaction.client.get_embed_color(interaction.channel),
)
memory_dict = {}
if case_dict["changes"]:
for change in case_dict["changes"]:
if change["user_id"] not in memory_dict:
memory_dict[str(change["user_id"])] = await fetch_user_dict(
interaction.client, change["user_id"]
)
if moderation.changes:
for change in moderation.changes:
if change.user_id not in memory_dict:
memory_dict[str(change.user_id)] = await change.get_user()
user = memory_dict[str(change["user_id"])]
name = (
user["name"]
if user["discriminator"] == "0"
else f"{user['name']}#{user['discriminator']}"
)
user: PartialUser = memory_dict[str(change.user_id)]
timestamp = f"<t:{change['timestamp']}> | <t:{change['timestamp']}:R>"
timestamp = f"<t:{change.unix_timestamp}> | <t:{change.unix_timestamp}:R>"
end_timestamp = f"<t:{change.unix_end_timestamp}> | <t:{change.unix_end_timestamp}:R>" if change.end_timestamp else None
change_str = [
f"{bold('User:')} {inline(user.name)} ({user.id})",
f"{bold('Reason:')} {change.reason}" if change.reason else "",
f"{bold('Duration:')} {humanize_timedelta(timedelta=change.duration)}" if change.duration else "",
f"{bold('End Timestamp:')} {end_timestamp}" if end_timestamp else "",
f"{bold('Timestamp')} {timestamp}",
]
copy = change_str.copy()
for string in change_str:
if string == "":
copy.remove(string)
if change["type"] == "ORIGINAL":
embed.add_field(
name="Original",
value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}",
inline=False,
)
elif change["type"] == "EDIT":
embed.add_field(
name="Edit",
value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}",
inline=False,
)
elif change["type"] == "RESOLVE":
embed.add_field(
name="Resolve",
value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}",
name=change.type.title(),
value="\n".join(copy),
inline=False,
)
@ -353,44 +289,29 @@ async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed:
return embed
async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> str:
async def evidenceformat_factory(moderation: Moderation) -> str:
"""This function creates a codeblock in evidence format from set parameters.
Args:
interaction (Interaction): The interaction object.
case_dict (dict): The case dictionary.
interaction (discord.Interaction): The interaction object.
moderation (aurora.models.Moderation): The moderation object.
"""
if case_dict["target_type"] == "USER":
target_user = await fetch_user_dict(interaction.client, case_dict["target_id"])
target_name = (
target_user["name"]
if target_user["discriminator"] == "0"
else f"{target_user['name']}#{target_user['discriminator']}"
)
target = await moderation.get_target()
moderator = await moderation.get_moderator()
elif case_dict["target_type"] == "CHANNEL":
target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"])
target_name = target_user["name"]
content = f"Case: {moderation.id:,} ({str.title(moderation.type.string)})\nTarget: {target.name} ({target.id})\nModerator: {moderator.name} ({moderator.id})"
moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"])
moderator_name = (
moderator_user["name"]
if moderator_user["discriminator"] == "0"
else f"{moderator_user['name']}#{moderator_user['discriminator']}"
)
if moderation.duration is not None:
content += f"\nDuration: {humanize_timedelta(timedelta=moderation.duration)}"
content = f"Case: {case_dict['moderation_id']:,} ({str.title(case_dict['moderation_type'])})\nTarget: {target_name} ({target_user['id']})\nModerator: {moderator_name} ({moderator_user['id']})"
if moderation.role_id:
role = await moderation.get_role()
content += "\nRole: " + (role.name if role is not None else moderation.role_id)
if case_dict["role_id"] != "0":
role = interaction.guild.get_role(int(case_dict["role_id"]))
content += "\nRole: " + (role.name if role is not None else case_dict["role_id"])
content += f"\nReason: {moderation.reason}"
if case_dict["duration"] != "NULL":
hours, minutes, seconds = map(int, case_dict["duration"].split(":"))
td = timedelta(hours=hours, minutes=minutes, seconds=seconds)
content += f"\nDuration: {humanize_timedelta(timedelta=td)}"
content += f"\nReason: {case_dict['reason']}"
for key, value in moderation.metadata.items():
content += f"\n{key.title()}: {value}"
return box(content, "prolog")
@ -622,3 +543,41 @@ async def immune_embed(ctx: commands.Context) -> Embed:
e.description += "\n\n" + immune_str
return e
async def type_embed(ctx: commands.Context, moderation_type = Type) -> Embed:
"""Generates a configuration menu field value for a guild's settings."""
type_settings = {
"show_in_history": await config.custom("types", ctx.guild.id, moderation_type.key).show_in_history(),
"show_moderator": await config.custom("types", ctx.guild.id, moderation_type.key).show_moderator(),
"use_discord_permissions": await config.custom("types", ctx.guild.id, moderation_type.key).use_discord_permissions(),
"dm_users": await config.custom("types", ctx.guild.id, moderation_type.key).dm_users(),
}
guild_str = [
"- "
+ bold("Show in History: ")
+ get_bool_emoji(type_settings["show_in_history"]),
"- "
+ bold("Show Moderator: ")
+ get_bool_emoji(type_settings["show_moderator"]),
"- "
+ bold("Use Discord Permissions: ")
+ get_bool_emoji(type_settings["use_discord_permissions"]),
"- "
+ bold("DM Users: ")
+ get_bool_emoji(type_settings["dm_users"]),
]
guild_str = "\n".join(guild_str)
e = await _config(ctx)
e.title += f": {moderation_type.string.title()} Configuration"
e.description = (
f"""
Use the buttons below to manage Aurora's configuration for the {bold(moderation_type.string)} moderation type.
If an option has a question mark (\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}) next to it, Aurora will default to the guild level setting instead.
See `{ctx.prefix}aurora set guild` for more information.\n
"""
+ guild_str
)
return e

135
aurora/utilities/json.py Normal file
View file

@ -0,0 +1,135 @@
import json
from datetime import datetime, timedelta
from typing import Any
from redbot.core.bot import Red
from ..models.base import AuroraBaseModel
from ..models.type import Type
class JSONEncoder(json.JSONEncoder):
def default(self, o) -> Any:
match o:
case datetime():
return int(o.timestamp())
case timedelta():
from ..utilities.utils import timedelta_to_string
return timedelta_to_string(o)
case AuroraBaseModel():
return o.dump()
case Type():
return o.key
case Red():
return None
case _:
return super().default(o)
# This is a wrapper around the json module's dumps function that uses our custom JSONEncoder class
def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, indent=None, separators=None,
default=None, sort_keys=False, **kw) -> str:
"""Serialize ``obj`` to a JSON formatted ``str``.
If ``skipkeys`` is true then ``dict`` keys that are not basic types
(``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the return value can contain non-ASCII
characters if they appear in strings contained in ``obj``. Otherwise, all
such characters are escaped in JSON strings.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``RecursionError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and
object members will be pretty-printed with that indent level. An indent
level of 0 will only insert newlines. ``None`` is the most compact
representation.
If specified, ``separators`` should be an ``(item_separator, key_separator)``
tuple. The default is ``(', ', ': ')`` if *indent* is ``None`` and
``(',', ': ')`` otherwise. To get the most compact JSON representation,
you should specify ``(',', ':')`` to eliminate whitespace.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
If *sort_keys* is true (default: ``False``), then the output of
dictionaries will be sorted by key.
"""
return json.dumps(
obj,
cls=JSONEncoder,
skipkeys=skipkeys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
indent=indent,
separators=separators,
default=default,
sort_keys=sort_keys,
**kw
)
# This is a wrapper around the json module's dump function that uses our custom JSONEncoder class
def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, indent=None, separators=None,
default=None, sort_keys=False, **kw) -> str:
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
``.write()``-supporting file-like object).
If ``skipkeys`` is true then ``dict`` keys that are not basic types
(``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped
instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the strings written to ``fp`` can
contain non-ASCII characters if they appear in strings contained in
``obj``. Otherwise, all such characters are escaped in JSON strings.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``RecursionError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
in strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and
object members will be pretty-printed with that indent level. An indent
level of 0 will only insert newlines. ``None`` is the most compact
representation.
If specified, ``separators`` should be an ``(item_separator, key_separator)``
tuple. The default is ``(', ', ': ')`` if *indent* is ``None`` and
``(',', ': ')`` otherwise. To get the most compact JSON representation,
you should specify ``(',', ':')`` to eliminate whitespace.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
If *sort_keys* is true (default: ``False``), then the output of
dictionaries will be sorted by key.
"""
return json.dump(
obj,
fp,
cls=JSONEncoder,
skipkeys=skipkeys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
indent=indent,
separators=separators,
default=default,
sort_keys=sort_keys,
**kw
)

View file

@ -1,23 +1,25 @@
# pylint: disable=cyclic-import
import json
from datetime import datetime
from datetime import timedelta as td
from typing import Optional, Union
from datetime import datetime, timedelta
from typing import Optional, Tuple, Union
import aiosqlite
from dateutil.relativedelta import relativedelta as rd
from discord import File, Guild, Interaction, Member, SelectOption, User
from discord.errors import Forbidden, NotFound
from discord import File, Guild, Interaction, Member, SelectOption, TextChannel, User
from discord.errors import Forbidden
from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import error
from .config import config
from ..models.type import Type
from ..utilities.config import config
from ..utilities.json import dumps
from ..utilities.logger import logger
def check_permissions(
user: User,
permissions: list,
ctx: Union[commands.Context, Interaction] = None,
guild: Guild = None,
permissions: Tuple[str],
ctx: commands.Context | Interaction | None = None,
guild: Guild | None = None,
) -> Union[bool, str]:
"""Checks if a user has a specific permission (or a list of permissions) in a channel."""
if ctx:
@ -42,11 +44,17 @@ def check_permissions(
async def check_moddable(
target: Union[User, Member], interaction: Interaction, permissions: list
target: Union[User, Member, TextChannel], ctx: commands.Context, permissions: Tuple[str], moderation_type: Type,
) -> bool:
"""Checks if a moderator can moderate a target."""
if check_permissions(interaction.client.user, permissions, guild=interaction.guild):
await interaction.response.send_message(
is_channel = isinstance(target, TextChannel)
use_discord_permissions = await config.custom("types", ctx.guild.id, moderation_type.key).use_discord_permissions()
if use_discord_permissions is None:
use_discord_permissions = await config.guild(ctx.guild).use_discord_permissions()
if check_permissions(ctx.bot.user, permissions, guild=ctx.guild):
await ctx.send(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
@ -54,9 +62,9 @@ async def check_moddable(
)
return False
if await config.guild(interaction.guild).use_discord_permissions() is True:
if check_permissions(interaction.user, permissions, guild=interaction.guild):
await interaction.response.send_message(
if use_discord_permissions is True:
if check_permissions(ctx.author, permissions, guild=ctx.guild):
await ctx.send(
error(
f"You do not have the `{permissions}` permission, required for this action."
),
@ -64,21 +72,21 @@ async def check_moddable(
)
return False
if interaction.user.id == target.id:
await interaction.response.send_message(
if ctx.author.id == target.id:
await ctx.send(
content="You cannot moderate yourself!", ephemeral=True
)
return False
if target.bot:
await interaction.response.send_message(
if not is_channel and target.bot:
await ctx.send(
content="You cannot moderate bots!", ephemeral=True
)
return False
if isinstance(target, Member):
if interaction.user.top_role <= target.top_role and await config.guild(interaction.guild).respect_hierarchy() is True:
await interaction.response.send_message(
if ctx.author.top_role <= target.top_role and await config.guild(ctx.guild).respect_hierarchy() is True:
await ctx.send(
content=error(
"You cannot moderate members with a higher role than you!"
),
@ -86,11 +94,17 @@ async def check_moddable(
)
return False
if target.guild_permissions.administrator:
await ctx.send(
content="You cannot moderate members with the Administrator permission!", ephemeral=True
)
return False
if (
interaction.guild.get_member(interaction.client.user.id).top_role
ctx.guild.get_member(ctx.bot.user.id).top_role
<= target.top_role
):
await interaction.response.send_message(
await ctx.send(
content=error(
"You cannot moderate members with a role higher than the bot!"
),
@ -102,7 +116,7 @@ async def check_moddable(
for role in target.roles:
if role.id in immune_roles:
await interaction.response.send_message(
await ctx.send(
content=error("You cannot moderate members with an immune role!"),
ephemeral=True,
)
@ -111,151 +125,55 @@ async def check_moddable(
return True
async def get_next_case_number(guild_id: str, cursor=None) -> int:
"""This function returns the next case number from the MySQL table for a specific guild."""
from .database import connect
if not cursor:
database = connect()
cursor = database.cursor()
cursor.execute(
f"SELECT moderation_id FROM `moderation_{guild_id}` ORDER BY moderation_id DESC LIMIT 1"
)
result = cursor.fetchone()
return (result[0] + 1) if result else 1
def generate_dict(result) -> dict:
case = {
"moderation_id": result[0],
"timestamp": result[1],
"moderation_type": result[2],
"target_type": result[3],
"target_id": result[4],
"moderator_id": result[5],
"role_id": result[6],
"duration": result[7],
"end_timestamp": result[8],
"reason": result[9],
"resolved": result[10],
"resolved_by": result[11],
"resolve_reason": result[12],
"expired": result[13],
"changes": json.loads(result[14]),
"metadata": json.loads(result[15]),
}
return case
async def fetch_user_dict(client: commands.Bot, user_id: str) -> dict:
"""This function returns a dictionary containing either user information or a standard deleted user template."""
if user_id == "?":
user_dict = {"id": "?", "name": "Unknown User", "discriminator": "0"}
else:
try:
user = client.get_user(int(user_id))
if user is None:
user = await client.fetch_user(int(user_id))
user_dict = {
"id": user.id,
"name": user.name,
"discriminator": user.discriminator,
}
except NotFound:
user_dict = {
"id": user_id,
"name": "Deleted User",
"discriminator": "0",
}
return user_dict
async def fetch_channel_dict(guild: Guild, channel_id: int) -> dict:
"""This function returns a dictionary containing either channel information or a standard deleted channel template."""
try:
channel = guild.get_channel(int(channel_id))
if not channel:
channel = await guild.fetch_channel(channel_id)
channel_dict = {
"id": channel.id,
"name": channel.name,
"mention": channel.mention,
}
except NotFound:
channel_dict = {"id": channel_id, "name": "Deleted Channel", "mention": None}
return channel_dict
async def fetch_role_dict(guild: Guild, role_id: int) -> dict:
"""This function returns a dictionary containing either role information or a standard deleted role template."""
role = guild.get_role(int(role_id))
if not role:
role_dict = {"id": role_id, "name": "Deleted Role"}
role_dict = {"id": role.id, "name": role.name}
return role_dict
async def log(interaction: Interaction, moderation_id: int, resolved: bool = False) -> None:
async def log(ctx: commands.Context, moderation_id: int, resolved: bool = False) -> None:
"""This function sends a message to the guild's configured logging channel when an infraction takes place."""
from .database import fetch_case
from ..models.moderation import Moderation
from .factory import log_factory
logging_channel_id = await config.guild(interaction.guild).log_channel()
logging_channel_id = await config.guild(ctx.guild).log_channel()
if logging_channel_id != " ":
logging_channel = interaction.guild.get_channel(logging_channel_id)
logging_channel = ctx.guild.get_channel(logging_channel_id)
case = await fetch_case(moderation_id, interaction.guild.id)
if case:
try:
moderation = await Moderation.find_by_id(ctx.bot, moderation_id, ctx.guild.id)
embed = await log_factory(
interaction=interaction, case_dict=case, resolved=resolved
ctx=ctx, moderation=moderation, resolved=resolved
)
try:
await logging_channel.send(embed=embed)
except Forbidden:
return
except ValueError:
return
async def send_evidenceformat(interaction: Interaction, case_dict: dict) -> None:
async def send_evidenceformat(ctx: commands.Context, moderation_id: int) -> None:
"""This function sends an ephemeral message to the moderator who took the moderation action, with a pre-made codeblock for use in the mod-evidence channel."""
from ..models.moderation import Moderation
from .factory import evidenceformat_factory
send_evidence_bool = (
await config.user(interaction.user).auto_evidenceformat()
or await config.guild(interaction.guild).auto_evidenceformat()
await config.user(ctx.author).auto_evidenceformat()
or await config.guild(guild=ctx.guild).auto_evidenceformat()
or False
)
if send_evidence_bool is False:
return
content = await evidenceformat_factory(interaction=interaction, case_dict=case_dict)
await interaction.followup.send(content=content, ephemeral=True)
def convert_timedelta_to_str(timedelta: td) -> str:
"""This function converts a timedelta object to a string."""
total_seconds = int(timedelta.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
return f"{hours}:{minutes}:{seconds}"
if send_evidence_bool is True:
moderation = await Moderation.find_by_id(ctx.bot, moderation_id, ctx.guild.id)
content = await evidenceformat_factory(moderation=moderation)
if not ctx.interaction:
await ctx.author.send(content=content)
else:
await ctx.send(content=content, ephemeral=True)
def get_bool_emoji(value: Optional[bool]) -> str:
"""Returns a unicode emoji based on a boolean value."""
if value is True:
match value:
case True:
return "\N{WHITE HEAVY CHECK MARK}"
if value is False:
case False:
return "\N{NO ENTRY SIGN}"
case _:
return "\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}"
@ -286,13 +204,91 @@ def create_pagesize_options() -> list[SelectOption]:
)
return options
def timedelta_from_relativedelta(relativedelta: rd) -> td:
def timedelta_from_relativedelta(relativedelta: rd) -> timedelta:
"""Converts a relativedelta object to a timedelta object."""
now = datetime.now()
then = now - relativedelta
return now - then
def timedelta_from_string(string: str) -> timedelta:
"""Converts a string to a timedelta object."""
hours, minutes, seconds = map(int, string.split(":"))
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
def timedelta_to_string(td: timedelta) -> str:
"""Converts a timedelta object to a string."""
days = td.days * 24
hours, remainder = divmod(td.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{days + hours}:{minutes:02}:{seconds:02}"
def get_footer_image(coginstance: commands.Cog) -> File:
"""Returns the footer image for the embeds."""
image_path = data_manager.bundled_data_path(coginstance) / "arrow.png"
return File(image_path, filename="arrow.png", description="arrow")
async def create_guild_table(guild: Guild) -> None:
from ..models.moderation import Moderation
try:
await Moderation.execute(f"SELECT * FROM `moderation_{guild.id}`", return_obj=False)
logger.trace("SQLite Table exists for server %s (%s)", guild.name, guild.id)
except aiosqlite.OperationalError:
query = f"""
CREATE TABLE `moderation_{guild.id}` (
moderation_id INTEGER PRIMARY KEY NOT NULL,
timestamp INTEGER NOT NULL,
moderation_type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER NOT NULL,
moderator_id INTEGER NOT NULL,
role_id INTEGER,
duration TEXT,
end_timestamp INTEGER,
reason TEXT,
resolved INTEGER NOT NULL,
resolved_by TEXT,
resolve_reason TEXT,
expired INTEGER NOT NULL,
changes JSON NOT NULL,
metadata JSON NOT NULL
)
"""
await Moderation.execute(query=query, return_obj=False)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
await Moderation.execute(query=index_query_1, return_obj=False)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
await Moderation.execute(query=index_query_2, return_obj=False)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
await Moderation.execute(query=index_query_3, return_obj=False)
insert_query = f"""
INSERT INTO `moderation_{guild.id}`
(moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
insert_values = (
0,
0,
"NULL",
"NULL",
0,
0,
None,
None,
None,
None,
0,
None,
None,
0,
dumps([]),
dumps({}),
)
await Moderation.execute(query=insert_query, parameters=insert_values, return_obj=False)
logger.trace("SQLite Table created for server %s (%s)", guild.name, guild.id)

View file

@ -17,16 +17,13 @@ from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import bold, error, humanize_list, text_to_file
# Disable Ruff & Pylint complaining about accessing private members
# That's kind of necessary for this cog to function because the Downloader cog has a limited public API
# ruff: noqa: SLF001 # Private member access
# pylint: disable=protected-access
class Backup(commands.Cog):
"""A utility to make reinstalling repositories and cogs after migrating the bot far easier."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.3"
__version__ = "1.1.1"
__documentation__ = "https://seacogs.coastalcommits.com/backup/"
def __init__(self, bot: Red):
@ -45,19 +42,22 @@ class Backup(commands.Cog):
]
return "\n".join(text)
@commands.group(autohelp=True) # type: ignore
@commands.group(autohelp=True)
@commands.is_owner()
async def backup(self, ctx: commands.Context) -> None:
async def backup(self, ctx: commands.Context):
"""Backup your installed cogs."""
pass
@backup.command(name="export")
@commands.is_owner()
async def backup_export(self, ctx: commands.Context) -> None:
async def backup_export(self, ctx: commands.Context):
"""Export your installed repositories and cogs to a file."""
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."))
await ctx.send(
error(
f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."
)
)
return
all_repos = list(downloader._repo_manager.repos)
@ -78,7 +78,7 @@ class Backup(commands.Cog):
if cog.repo_name == repo.name:
cog_dict = {
"name": cog.name,
# "loaded": cog.name in ctx.bot.extensions.keys(), # noqa: ERA001
# "loaded": cog.name in ctx.bot.extensions.keys(),
# this functionality was planned but never implemented due to Red limitations
# and the possibility of restoration functionality being added to Core
"pinned": cog.pinned,
@ -88,24 +88,30 @@ class Backup(commands.Cog):
export_data.append(repo_dict)
await ctx.send(file=text_to_file(json.dumps(export_data, indent=4), "backup.json"))
await ctx.send(
file=text_to_file(json.dumps(export_data, indent=4), "backup.json")
)
@backup.command(name="import")
@commands.is_owner()
async def backup_import(self, ctx: commands.Context) -> None:
async def backup_import(self, ctx: commands.Context):
"""Import your installed repositories and cogs from an export file."""
try:
export = json.loads(await ctx.message.attachments[0].read())
except (json.JSONDecodeError, IndexError):
try:
export = json.loads(await ctx.message.reference.resolved.attachments[0].read()) # type: ignore - this is fine to let error because it gets handled
export = json.loads(await ctx.message.reference.resolved.attachments[0].read())
except (json.JSONDecodeError, IndexError, AttributeError):
await ctx.send(error("Please provide a valid JSON export file."))
return
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."))
await ctx.send(
error(
f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again."
)
)
return
repo_s = []
@ -127,20 +133,32 @@ class Backup(commands.Cog):
repo_e.append("PyLav cogs are not supported.")
continue
if name.startswith(".") or name.endswith("."):
repo_e.append(f"Invalid repository name: {name}\nRepository names cannot start or end with a dot.")
repo_e.append(
f"Invalid repository name: {name}\nRepository names cannot start or end with a dot."
)
continue
if re.match(r"^[a-zA-Z0-9_\-\.]+$", name) is None:
repo_e.append(f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots.")
repo_e.append(
f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots."
)
continue
try:
repository = await downloader._repo_manager.add_repo(url, name, branch)
repo_s.append(f"Added repository {name} from {url} on branch {branch}.")
self.logger.debug("Added repository %s from %s on branch %s", name, url, branch)
repository = await downloader._repo_manager.add_repo(
url, name, branch
)
repo_s.append(
f"Added repository {name} from {url} on branch {branch}."
)
self.logger.debug(
"Added repository %s from %s on branch %s", name, url, branch
)
except errors.ExistingGitRepo:
repo_e.append(f"Repository {name} already exists.")
repository = downloader._repo_manager.get_repo(name)
repository = downloader._repo_manager.get_repo(
name
)
self.logger.debug("Repository %s already exists", name)
except errors.AuthenticationError as err:
@ -154,7 +172,9 @@ class Backup(commands.Cog):
continue
except errors.CloningError as err:
repo_e.append(f"Cloning error while adding repository {name}. See logs for more information.")
repo_e.append(
f"Cloning error while adding repository {name}. See logs for more information."
)
self.logger.exception(
"Something went wrong whilst cloning %s (to revision %s)",
url,
@ -164,7 +184,9 @@ class Backup(commands.Cog):
continue
except OSError:
repo_e.append(f"OS error while adding repository {name}. See logs for more information.")
repo_e.append(
f"OS error while adding repository {name}. See logs for more information."
)
self.logger.exception(
"Something went wrong trying to add repo %s under name %s",
url,
@ -184,19 +206,23 @@ class Backup(commands.Cog):
continue
cog_modules.append(cog_module)
for cog in {cog.name for cog in cog_modules}:
for cog in set(cog.name for cog in cog_modules):
poss_installed_path = (await downloader.cog_install_path()) / cog
if poss_installed_path.exists():
with contextlib.suppress(commands.ExtensionNotLoaded):
await ctx.bot.unload_extension(cog)
await ctx.bot.remove_loaded_package(cog)
await downloader._delete_cog(poss_installed_path)
await downloader._delete_cog(
poss_installed_path
)
uninstall_s.append(f"Uninstalled {cog}")
self.logger.debug("Uninstalled %s", cog)
else:
uninstall_e.append(f"Failed to uninstall {cog}")
self.logger.warning("Failed to uninstall %s", cog)
await downloader._remove_from_installed(cog_modules)
await downloader._remove_from_installed(
cog_modules
)
for cog in cogs:
cog_name = cog["name"]
@ -210,15 +236,25 @@ class Backup(commands.Cog):
if cog_name == "backup" and "cswimr/SeaCogs" in url:
continue
async with repository.checkout(commit, exit_to_rev=repository.branch):
cogs_c, message = await downloader._filter_incorrect_cogs_by_names(repository, [cog_name])
async with repository.checkout(
commit, exit_to_rev=repository.branch
):
cogs_c, message = (
await downloader._filter_incorrect_cogs_by_names(
repository, [cog_name]
)
)
if not cogs_c:
install_e.append(message)
self.logger.error(message)
continue
failed_reqs = await downloader._install_requirements(cogs_c)
failed_reqs = await downloader._install_requirements(
cogs_c
)
if failed_reqs:
install_e.append(f"Failed to install {cog_name} due to missing requirements: {failed_reqs}")
install_e.append(
f"Failed to install {cog_name} due to missing requirements: {failed_reqs}"
)
self.logger.error(
"Failed to install %s due to missing requirements: %s",
cog_name,
@ -226,37 +262,51 @@ class Backup(commands.Cog):
)
continue
installed_cogs, failed_cogs = await downloader._install_cogs(cogs_c)
installed_cogs, failed_cogs = await downloader._install_cogs(
cogs_c
)
if repository.available_libraries:
installed_libs, failed_libs = await repository.install_libraries(
installed_libs, failed_libs = (
await repository.install_libraries(
target_dir=downloader.SHAREDLIB_PATH,
req_target_dir=downloader.LIB_PATH,
)
)
else:
installed_libs = None
failed_libs = None
if cog_pinned:
for cog in installed_cogs: # noqa: PLW2901
for cog in installed_cogs:
cog.pinned = True
await downloader._save_to_installed(installed_cogs + installed_libs if installed_libs else installed_cogs)
await downloader._save_to_installed(
installed_cogs + installed_libs
if installed_libs
else installed_cogs
)
if installed_cogs:
installed_cog_name = installed_cogs[0].name
install_s.append(f"Installed {installed_cog_name}")
self.logger.debug("Installed %s", installed_cog_name)
if installed_libs:
for lib in installed_libs:
install_s.append(f"Installed {lib.name} required for {cog_name}")
self.logger.debug("Installed %s required for %s", lib.name, cog_name)
install_s.append(
f"Installed {lib.name} required for {cog_name}"
)
self.logger.debug(
"Installed %s required for %s", lib.name, cog_name
)
if failed_cogs:
failed_cog_name = failed_cogs[0].name
install_e.append(f"Failed to install {failed_cog_name}")
self.logger.error("Failed to install %s", failed_cog_name)
if failed_libs:
for lib in failed_libs:
install_e.append(f"Failed to install {lib.name} required for {cog_name}")
install_e.append(
f"Failed to install {lib.name} required for {cog_name}"
)
self.logger.error(
"Failed to install %s required for %s",
lib.name,

View file

@ -1,22 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": [
"cswimr"
],
"install_msg": "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "Backup",
"short": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"description": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"end_user_data_statement": "This cog does not store end user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "Backup",
"short" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"description" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.6",
"max_bot_version": "3.5.16",
"min_python_version": [
3,
9,
0
],
"max_bot_version": "3.5.13",
"min_python_version": [3, 9, 0],
"tags": [
"utility",
"backup",

View file

@ -6,7 +6,6 @@
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import random
from asyncio import create_task
from io import BytesIO
import aiohttp
@ -27,21 +26,20 @@ class Bible(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.4"
__version__ = "1.1.1"
__documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/"
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.session = aiohttp.ClientSession()
self.config = Config.get_conf(self, identifier=481923957134912, force_registration=True)
self.config = Config.get_conf(
self, identifier=481923957134912, force_registration=True
)
self.logger = getLogger("red.SeaCogs.Bible")
self.config.register_global(bible="de4e12af7f28f599-02")
self.config.register_user(bible=None)
async def cog_unload(self):
create_task(self.session.close())
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
@ -53,6 +51,7 @@ class Bible(commands.Cog):
]
return "\n".join(text)
def get_icon(self, color: Colour) -> File:
"""Get the docs.api.bible favicon with a given color."""
image_path = data_manager.bundled_data_path(self) / "api.bible-logo.png"
@ -71,7 +70,9 @@ class Bible(commands.Cog):
async def translate_book_name(self, bible_id: str, book_name: str) -> str:
"""Translate a book name to a book ID."""
book_name_list = [w.lower() if w.lower() == "of" else w.title() for w in book_name.split()]
book_name_list = [
w.lower() if w.lower() == "of" else w.title() for w in book_name.split()
]
book_name = " ".join(book_name_list)
books = await self._get_books(bible_id)
for book in books:
@ -91,20 +92,20 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.UnauthorizedError
raise bible.errors.Unauthorized()
if response.status == 403:
raise bible.errors.BibleAccessError
raise bible.errors.BibleAccessError()
if response.status == 503:
raise bible.errors.ServiceUnavailableError
raise bible.errors.ServiceUnavailable()
return Version(
bible_id=bible_id,
abbreviation=data["data"]["abbreviation"],
language=data["data"]["language"]["name"],
abbreviation_local=data["data"]["abbreviationLocal"],
language_local=data["data"]["language"]["nameLocal"],
description=data["data"]["description"],
description_local=data["data"]["descriptionLocal"],
version_copyright=data["data"]["copyright"],
bible_id,
data["data"]["abbreviation"],
data["data"]["language"]["name"],
data["data"]["abbreviationLocal"],
data["data"]["language"]["nameLocal"],
data["data"]["description"],
data["data"]["descriptionLocal"],
data["data"]["copyright"],
)
async def _get_passage(
@ -135,17 +136,16 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 400:
raise bible.errors.InexplicableError
raise bible.errors.InexplicableError()
if response.status == 401:
raise bible.errors.UnauthorizedError
raise bible.errors.Unauthorized()
if response.status == 403:
raise bible.errors.BibleAccessError
raise bible.errors.BibleAccessError()
if response.status == 404:
raise bible.errors.NotFoundError
raise bible.errors.NotFound()
if response.status == 503:
raise bible.errors.ServiceUnavailableError
raise bible.errors.ServiceUnavailable()
assert self.bot.user is not None # bot will always be logged in
fums_url = "https://fums.api.bible/f3"
fums_params = {
"t": data["meta"]["fumsToken"],
@ -177,11 +177,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.UnauthorizedError
raise bible.errors.Unauthorized()
if response.status == 403:
raise bible.errors.BibleAccessError
raise bible.errors.BibleAccessError()
if response.status == 503:
raise bible.errors.ServiceUnavailableError
raise bible.errors.ServiceUnavailable()
return data["data"]
async def _get_chapters(self, bible_id: str, book_id: str) -> dict:
@ -196,11 +196,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.UnauthorizedError
raise bible.errors.Unauthorized()
if response.status == 403:
raise bible.errors.BibleAccessError
raise bible.errors.BibleAccessError()
if response.status == 503:
raise bible.errors.ServiceUnavailableError
raise bible.errors.ServiceUnavailable()
return data["data"]
async def _get_verses(self, bible_id: str, book_id: str, chapter: int) -> dict:
@ -215,11 +215,11 @@ class Bible(commands.Cog):
response.status,
)
if response.status == 401:
raise bible.errors.UnauthorizedError
raise bible.errors.Unauthorized()
if response.status == 403:
raise bible.errors.BibleAccessError
raise bible.errors.BibleAccessError()
if response.status == 503:
raise bible.errors.ServiceUnavailableError
raise bible.errors.ServiceUnavailable()
return data["data"]
@commands.group(autohelp=True)
@ -247,34 +247,41 @@ class Bible(commands.Cog):
from_verse, to_verse = passage.replace(":", ".").split("-")
if "." not in to_verse:
to_verse = f"{from_verse.split('.')[0]}.{to_verse}"
retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True)
passage = await self._get_passage(
ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True
)
else:
retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False)
passage = await self._get_passage(
ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False
)
except (
bible.errors.BibleAccessError,
bible.errors.NotFoundError,
bible.errors.NotFound,
bible.errors.InexplicableError,
bible.errors.ServiceUnavailableError,
bible.errors.UnauthorizedError,
bible.errors.ServiceUnavailable,
bible.errors.Unauthorized,
) as e:
await ctx.send(e.message)
return
if len(retrieved_passage["content"]) > 4096:
if len(passage["content"]) > 4096:
await ctx.send("The passage is too long to send.")
return
if await ctx.embed_requested():
icon = self.get_icon(await ctx.embed_color())
embed = Embed(
title=f"{retrieved_passage['reference']}",
description=retrieved_passage["content"].replace("", ""),
title=f"{passage['reference']}",
description=passage["content"].replace("", ""),
color=await ctx.embed_color(),
)
embed.set_footer(text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png")
embed.set_footer(
text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
await ctx.send(embed=embed, file=icon)
else:
await ctx.send(f"## {retrieved_passage['reference']}\n{retrieved_passage['content']}")
await ctx.send(f"## {passage['reference']}\n{passage['content']}")
@bible.command(name="random")
async def bible_random(self, ctx: commands.Context):
@ -295,10 +302,10 @@ class Bible(commands.Cog):
passage = await self._get_passage(ctx, bible_id, verse, False)
except (
bible.errors.BibleAccessError,
bible.errors.NotFoundError,
bible.errors.NotFound,
bible.errors.InexplicableError,
bible.errors.ServiceUnavailableError,
bible.errors.UnauthorizedError,
bible.errors.ServiceUnavailable,
bible.errors.Unauthorized,
) as e:
await ctx.send(e.message)
return
@ -310,7 +317,10 @@ class Bible(commands.Cog):
description=passage["content"].replace("", ""),
color=await ctx.embed_color(),
)
embed.set_footer(text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png")
embed.set_footer(
text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
await ctx.send(embed=embed, file=icon)
else:
await ctx.send(f"## {passage['reference']}\n{passage['content']}")

View file

@ -4,22 +4,26 @@ from redbot.core.utils.chat_formatting import error
class BibleAccessError(Exception):
def __init__(
self,
message: str = error("The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."),
message: str = error(
"The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."
),
):
super().__init__(message)
self.message = message
class UnauthorizedError(Exception):
class Unauthorized(Exception):
def __init__(
self,
message: str = error("The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."),
message: str = error(
"The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."
),
):
super().__init__(message)
self.message = message
class NotFoundError(Exception):
class NotFound(Exception):
def __init__(
self,
message: str = error("The requested passage was not found."),
@ -28,7 +32,7 @@ class NotFoundError(Exception):
self.message = message
class ServiceUnavailableError(Exception):
class ServiceUnavailable(Exception):
def __init__(
self,
message: str = error("The API.Bible service is currently unavailable."),
@ -40,7 +44,9 @@ class ServiceUnavailableError(Exception):
class InexplicableError(Exception):
def __init__(
self,
message: str = error("An inexplicable 'Bad Request' error occurred. This error happens occasionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."),
message: str = error(
"An inexplicable 'Bad Request' error occurred. This error happens occassionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."
),
):
super().__init__(message)
self.message = message

View file

@ -1,15 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "Bible",
"short": "Retrieve Bible verses from API.Bible.",
"description": "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.",
"end_user_data_statement": "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "Bible",
"short" : "Retrieve Bible verses from API.Bible.",
"description" : "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.",
"end_user_data_statement" : "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["numpy", "pillow"],
"tags": ["fun", "utility", "api"]
"tags": [
"fun",
"utility",
"api"
]
}

View file

@ -4,23 +4,23 @@ class Version:
bible_id,
abbreviation,
language,
abbreviation_local,
language_local,
abbreviationLocal,
languageLocal,
description,
description_local,
descriptionLocal,
version_copyright,
):
self.bible_id = bible_id
self.abbreviation = abbreviation
self.language = language
self.abbreviation_local = abbreviation_local
self.language_local = language_local
self.abbreviationLocal = abbreviationLocal
self.languageLocal = languageLocal
self.description = description
self.description_local = description_local
self.descriptionLocal = descriptionLocal
self.copyright = version_copyright
def __str__(self):
return self.abbreviation_local
return self.abbreviationLocal
def __repr__(self):
return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviation_local}", "{self.language_local}", "{self.description}", "{self.description_local}", "{self.copyright}")'
return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviationLocal}", "{self.languageLocal}", "{self.description}", "{self.descriptionLocal}", "{self.copyright}")'

View file

@ -16,13 +16,13 @@ class EmojiInfo(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.3"
__version__ = "1.0.1"
__documentation__ = "https://seacogs.coastalcommits.com/emojiinfo/"
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot: Red = bot
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.EmojiInfo")
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.Emoji")
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
@ -35,12 +35,14 @@ class EmojiInfo(commands.Cog):
]
return "\n".join(text)
async def fetch_twemoji(self, unicode_emoji) -> str:
base_url = "https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/"
emoji_codepoint = "-".join([hex(ord(char))[2:] for char in unicode_emoji])
segments = emoji_codepoint.split("-")
valid_segments = [seg for seg in segments if len(seg) >= 4]
return f"{base_url}{valid_segments[0]}.png"
emoji_url = f"{base_url}{valid_segments[0]}.png"
return emoji_url
async def fetch_primary_color(self, emoji_url: str) -> discord.Color | None:
async with aiohttp.ClientSession() as session:
@ -49,7 +51,8 @@ class EmojiInfo(commands.Cog):
return None
image = await response.read()
dominant_color = ColorThief(io.BytesIO(image)).get_color(quality=1)
return discord.Color.from_rgb(*dominant_color)
color = discord.Color.from_rgb(*dominant_color)
return color
async def get_emoji_info(self, emoji: PartialEmoji) -> tuple[str, str]:
if emoji.is_unicode_emoji():
@ -69,51 +72,59 @@ class EmojiInfo(commands.Cog):
else:
emoji_id = ""
markdown = f"`{emoji}`"
name = f"{bold('Name:')} {emoji.aliases.pop(0) if emoji.aliases else emoji.name}\n"
name = f"{bold('Name:')} {emoji.aliases.pop(0)}\n"
aliases = f"{bold('Aliases:')} {', '.join(emoji.aliases)}\n" if emoji.aliases else ""
group = f"{bold('Group:')} {emoji.group}\n"
return (f"{name}{emoji_id}{bold('Native:')} {emoji.is_unicode_emoji()}\n{group}{aliases}{bold('Animated:')} {emoji.animated}\n{bold('Markdown:')} {markdown}\n{bold('URL:')} [Click Here]({emoji_url})"), emoji_url
return (
f"{name}"
f"{emoji_id}"
f"{bold('Native:')} {emoji.is_unicode_emoji()}\n"
f"{group}"
f"{aliases}"
f"{bold('Animated:')} {emoji.animated}\n"
f"{bold('Markdown:')} {markdown}\n"
f"{bold('URL:')} [Click Here]({emoji_url})"
), emoji_url
@app_commands.command(name="emoji")
@app_commands.describe(emoji="What emoji would you like to get information on?", ephemeral="Would you like the response to be hidden?")
@app_commands.describe(
emoji="What emoji would you like to get information on?",
ephemeral="Would you like the response to be hidden?"
)
async def emoji_slash(self, interaction: discord.Interaction, emoji: str, ephemeral: bool = True) -> None:
"""Retrieve information about an emoji."""
await interaction.response.defer(ephemeral=ephemeral)
try:
retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url = await self.get_emoji_info(retrieved_emoji)
emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url, = await self.get_emoji_info(emoji)
self.logger.verbose(f"Emoji:\n{string}")
except (IndexError, UnboundLocalError):
return await interaction.followup.send("Please provide a valid emoji!")
assert isinstance(interaction.channel, discord.TextChannel)
if await self.bot.embed_requested(channel=interaction.channel):
embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel))
embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel))
embed.set_thumbnail(url=emoji_url)
await interaction.followup.send(embed=embed)
return None
else:
await interaction.followup.send(content=string)
return None
@commands.command(name="emoji")
async def emoji(self, ctx: commands.Context, *, emoji: str) -> None:
"""Retrieve information about an emoji."""
try:
retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url = await self.get_emoji_info(retrieved_emoji)
emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji)
string, emoji_url, = await self.get_emoji_info(emoji)
self.logger.verbose(f"Emoji:\n{string}")
except (IndexError, UnboundLocalError):
await ctx.send("Please provide a valid emoji!")
return
return await ctx.send("Please provide a valid emoji!")
if await ctx.embed_requested():
embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await ctx.embed_color())
embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await ctx.embed_color)
embed.set_thumbnail(url=emoji_url)
await ctx.send(embed=embed)
return
else:
await ctx.send(content=string)
return

View file

@ -1,15 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Emoji!",
"name": "Emoji",
"short": "Retrieve information about emojis.",
"description": "Retrieve information about emojis.",
"end_user_data_statement": "This cog does not store end user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Emoji!",
"name" : "Emoji",
"short" : "Retrieve information about emojis.",
"description" : "Retrieve information about emojis.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["colorthief"],
"tags": ["utility"]
"tags": [
"utility"
]
}

View file

@ -39,7 +39,7 @@ class PartialEmoji(discord.PartialEmoji):
The group name of the emoji if it is a native emoji.
"""
def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin # noqa: A002
def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin
super().__init__(name=name, animated=animated, id=id)
self.group = group
self.aliases = aliases
@ -72,12 +72,12 @@ class PartialEmoji(discord.PartialEmoji):
match = cls._CUSTOM_EMOJI_RE.match(value)
if match is not None:
groups = match.groupdict()
animated = bool(groups["animated"])
emoji_id = int(groups["id"])
name = groups["name"]
animated = bool(groups['animated'])
emoji_id = int(groups['id'])
name = groups['name']
return cls(name=name, animated=animated, id=emoji_id)
path = data_manager.bundled_data_path(coginstance) / "emojis.json"
path: data_manager.Path = data_manager.bundled_data_path(coginstance) / "emojis.json"
with open(path, "r", encoding="UTF-8") as file:
emojis: dict = json.load(file)
emoji_aliases = []

25
flake.lock generated
View file

@ -1,25 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1738680400,
"narHash": "sha256-ooLh+XW8jfa+91F1nhf9OF7qhuA/y1ChLx6lXDNeY5U=",
"rev": "799ba5bffed04ced7067a91798353d360788b30d",
"revCount": 747653,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.747653%2Brev-799ba5bffed04ced7067a91798353d360788b30d/0194d302-29da-7009-8f43-5b8a58825954/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,72 +0,0 @@
{
description = "SeaCogs Nix Flake";
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";
outputs =
{ self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forEachSupportedSystem =
f:
nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import nixpkgs { inherit system; };
lib = nixpkgs.lib;
}
);
in
{
devShells = forEachSupportedSystem (
{ pkgs, lib }:
let
myPython = pkgs.python311;
lib-path =
with pkgs;
lib.makeLibraryPath [
stdenv.cc.cc
# Red-DiscordBot dependencies
libffi
libsodium
# PyLav dependency
libaio
# Material for MkDocs dependency
cairo
];
in
{
default = pkgs.mkShell {
lib-path = lib-path;
packages = with pkgs; [
myPython
uv
ruff # the ruff pip package installs a dynamically linked binary that cannot run on NixOS
forgejo-runner
# Red-DiscordBot dependencies
git
jdk17
# Material for MkDocs dependencies
pngquant
# SeaCogs dependencies
dig
];
shellHook = # bash
''
export "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib-path}"
export "UV_PYTHON_PREFERENCE=only-system"
export "UV_PYTHON_DOWNLOADS=never"
uv sync --all-groups
source ./.venv/bin/activate
export "PYTHONPATH=`pwd`/.venv/${myPython.sitePackages}/:$PYTHONPATH"
export "PATH=${pkgs.ruff}/bin:$PATH"
'';
};
}
);
};
}

View file

@ -1,5 +0,0 @@
from .hotreload import HotReload
async def setup(bot):
await bot.add_cog(HotReload(bot))

View file

@ -1,191 +0,0 @@
import py_compile
from asyncio import run_coroutine_threadsafe
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Generator, List, Sequence
import discord
from red_commons.logging import RedTraceLogger, getLogger
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.core_commands import CoreLogic
from redbot.core.utils.chat_formatting import bold, box, humanize_list
from typing_extensions import override
from watchdog.events import FileSystemEvent, FileSystemMovedEvent, RegexMatchingEventHandler
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
class HotReload(commands.Cog):
"""Automatically reload cogs in local cog paths on file change."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.4.1"
__documentation__ = "https://seacogs.coastalcommits.com/hotreload/"
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot: Red = bot
self.config: Config = Config.get_conf(cog_instance=self, identifier=294518358420750336, force_registration=True)
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload")
self.observers: List[BaseObserver] = []
self.config.register_global(notify_channel=None, compile_before_reload=False)
watchdog_loggers = [getLogger(name="watchdog.observers.inotify_buffer")]
for watchdog_logger in watchdog_loggers:
watchdog_logger.setLevel("INFO") # SHUT UP!!!!
@override
async def cog_load(self) -> None:
"""Start the observer when the cog is loaded."""
_ = self.bot.loop.create_task(self.start_observer())
@override
async def cog_unload(self) -> None:
"""Stop the observer when the cog is unloaded."""
for observer in self.observers:
observer.stop()
observer.join()
self.logger.info("Stopped observer. No longer watching for file changes.")
@override
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"{bold('Cog Version:')} [{self.__version__}]({self.__git__})",
f"{bold('Author:')} {humanize_list(self.__author__)}",
f"{bold('Documentation:')} {self.__documentation__}",
]
return "\n".join(text)
async def get_paths(self) -> Generator[Path, None, None]:
"""Retrieve user defined paths."""
cog_manager = self.bot._cog_mgr # noqa: SLF001 # We have to use this private method because there is no public API to get user defined paths
cog_paths = await cog_manager.user_defined_paths()
return (Path(path) for path in cog_paths)
async def start_observer(self) -> None:
"""Start the observer to watch for file changes."""
self.observers.append(Observer())
paths = await self.get_paths()
is_first = True
for observer in self.observers:
if not is_first:
observer.stop()
observer.join()
self.logger.debug("Stopped hanging observer.")
continue
for path in paths:
if not path.exists():
self.logger.warning("Path %s does not exist. Skipping.", path)
continue
self.logger.debug("Adding observer schedule for path %s.", path)
observer.schedule(event_handler=HotReloadHandler(cog=self, path=path), path=str(path), recursive=True)
observer.start()
self.logger.info("Started observer. Watching for file changes.")
is_first = False
@checks.is_owner()
@commands.group(name="hotreload")
async def hotreload_group(self, ctx: commands.Context) -> None:
"""HotReload configuration commands."""
pass
@hotreload_group.command(name="notifychannel")
async def hotreload_notifychannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send notifications to."""
await self.config.notify_channel.set(channel.id)
await ctx.send(f"Notifications will be sent to {channel.mention}.")
@hotreload_group.command(name="compile") # type: ignore
async def hotreload_compile(self, ctx: commands.Context, compile_before_reload: bool) -> None:
"""Set whether to compile modified files before reloading."""
await self.config.compile_before_reload.set(compile_before_reload)
await ctx.send(f"I {'will' if compile_before_reload else 'will not'} compile modified files before hotreloading cogs.")
@hotreload_group.command(name="list") # type: ignore
async def hotreload_list(self, ctx: commands.Context) -> None:
"""List the currently active observers."""
if not self.observers:
await ctx.send("No observers are currently active.")
return
await ctx.send(f"Currently active observers (If there are more than one of these, report an issue): {box(humanize_list([str(o) for o in self.observers], style='unit'))}")
class HotReloadHandler(RegexMatchingEventHandler):
"""Handler for file changes."""
def __init__(self, cog: HotReload, path: Path) -> None:
super().__init__(regexes=[r".*\.py$"])
self.cog: HotReload = cog
self.path: Path = path
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload.Observer")
def on_any_event(self, event: FileSystemEvent) -> None:
"""Handle filesystem events."""
if event.is_directory:
return
allowed_events = ("moved", "deleted", "created", "modified")
if event.event_type not in allowed_events:
return
relative_src_path = Path(str(event.src_path)).relative_to(self.path)
src_package_name = relative_src_path.parts[0]
cogs_to_reload = [src_package_name]
if isinstance(event, FileSystemMovedEvent):
dest = f" to {event.dest_path}"
relative_dest_path = Path(str(event.dest_path)).relative_to(self.path)
dest_package_name = relative_dest_path.parts[0]
if dest_package_name != src_package_name:
cogs_to_reload.append(dest_package_name)
else:
dest = ""
self.logger.info("File %s has been %s%s.", event.src_path, event.event_type, dest)
run_coroutine_threadsafe(
coro=self.reload_cogs(
cog_names=cogs_to_reload,
paths=[Path(str(p)) for p in (event.src_path, getattr(event, "dest_path", None)) if p],
),
loop=self.cog.bot.loop,
)
async def reload_cogs(self, cog_names: Sequence[str], paths: Sequence[Path]) -> None:
"""Reload modified cogs."""
if not self.compile_modified_files(cog_names, paths):
return
core_logic = CoreLogic(bot=self.cog.bot)
self.logger.info("Reloading cogs: %s", humanize_list(cog_names, style="unit"))
await core_logic._reload(pkg_names=cog_names) # noqa: SLF001 # We have to use this private method because there is no public API to reload other cogs
self.logger.info("Reloaded cogs: %s", humanize_list(cog_names, style="unit"))
channel = self.cog.bot.get_channel(await self.cog.config.notify_channel())
if channel and isinstance(channel, discord.TextChannel):
await channel.send(f"Reloaded cogs: {humanize_list(cog_names, style='unit')}")
def compile_modified_files(self, cog_names: Sequence[str], paths: Sequence[Path]) -> bool:
"""Compile modified files to ensure they are valid Python files."""
for path in paths:
if not path.exists() or path.suffix != ".py":
self.logger.debug("Path %s does not exist or does not point to a Python file. Skipping compilation step.", path)
continue
try:
with NamedTemporaryFile() as temp_file:
self.logger.debug("Attempting to compile %s", path)
py_compile.compile(file=str(path), cfile=temp_file.name, doraise=True)
self.logger.debug("Successfully compiled %s", path)
except py_compile.PyCompileError as e:
e.__suppress_context__ = True
self.logger.exception("%s failed to compile. Not reloading cogs %s.", path, humanize_list(cog_names, style="unit"))
return False
except OSError:
self.logger.exception("Failed to create tempfile for compilation step. Skipping.")
return True

View file

@ -1,15 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing HotReload! Please see the [documentation](https://seacogs.coastalcommits.com/hotreload) to get started.",
"name": "HotReload",
"short": "Automatically reload cogs in local cog paths on file change.",
"description": "Automatically reload cogs in local cog paths on file change.",
"end_user_data_statement": "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"requirements": ["watchdog"],
"tags": ["utility", "development"]
}

View file

@ -1,6 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"author": [
"cswimr"
],
"install_msg": "Thanks for installing my repo!\n\nIf you have any issues with any of the cogs, please create an issue [here](https://coastalcommits.com/cswimr/SeaCogs/issues) or join my [Discord Server](https://discord.gg/eMUMe77Yb8 ).",
"index_name": "sea-cogs",
"short": "Various cogs for Red, by cswimr",

View file

@ -1,8 +1,8 @@
site_name: SeaCogs Documentation
site_url: !ENV [SITE_URL, "https://seacogs.coastalcommits.com"]
site_url: !ENV [SITE_URL, 'https://seacogs.coastalcommits.com']
repo_name: CoastalCommits
repo_url: https://coastalcommits.com/cswimr/SeaCogs
edit_uri: !ENV [EDIT_URI, "src/branch/main/.docs"]
edit_uri: !ENV [EDIT_URI, 'src/branch/main/.docs']
copyright: Copyright &copy; 2023-2024, cswimr
docs_dir: .docs
@ -19,7 +19,6 @@ nav:
- Bible: bible.md
- Backup: backup.md
- EmojiInfo: emojiinfo.md
- HotReload: hotreload.md
- Nerdify: nerdify.md
- Pterodactyl:
- pterodactyl/index.md
@ -73,7 +72,7 @@ markdown_extensions:
theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
- media: '(prefers-color-scheme: light)'
scheme: default
primary: white
accent: light blue
@ -81,7 +80,7 @@ theme:
icon: material/toggle-switch
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
- media: '(prefers-color-scheme: dark)'
scheme: slate
primary: black
accent: light blue

View file

@ -1,14 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU](<https://github.com/PhasecoreX/PCXCogs/tree/master/uwu>) cog.",
"name": "Nerdify",
"short": "Nerdify your text!",
"description": "Nerdify your text!",
"end_user_data_statement": "This cog does not store end user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU](<https://github.com/PhasecoreX/PCXCogs/tree/master/uwu>) cog.",
"name" : "Nerdify",
"short" : "Nerdify your text!",
"description" : "Nerdify your text!",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"tags": ["fun", "text", "meme"]
"tags": [
"fun",
"text",
"meme"
]
}

View file

@ -37,20 +37,16 @@ class Nerdify(commands.Cog):
]
return "\n".join(text)
@commands.command(aliases=["nerd"])
async def nerdify(
self,
ctx: commands.Context,
*,
text: Optional[str] = None,
self, ctx: commands.Context, *, text: Optional[str] = None
) -> None:
"""Nerdify the replied to message, previous message, or your own text."""
if not text:
if hasattr(ctx.message, "reference") and ctx.message.reference:
with suppress(
discord.Forbidden,
discord.NotFound,
discord.HTTPException,
discord.Forbidden, discord.NotFound, discord.HTTPException
):
message_id = ctx.message.reference.message_id
if message_id:
@ -66,9 +62,7 @@ class Nerdify(commands.Cog):
ctx.channel,
self.nerdify_text(text),
allowed_mentions=discord.AllowedMentions(
everyone=False,
users=False,
roles=False,
everyone=False, users=False, roles=False
),
)
@ -83,10 +77,7 @@ class Nerdify(commands.Cog):
return f'"{text}" 🤓'
async def type_message(
self,
destination: discord.abc.Messageable,
content: str,
**kwargs: Any,
self, destination: discord.abc.Messageable, content: str, **kwargs: Any
) -> Union[discord.Message, None]:
"""Simulate typing and sending a message to a destination.

View file

@ -1,15 +1,19 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).",
"name": "Pterodactyl",
"short": "Interface with Pterodactyl through websockets.",
"description": "Interface with Pterodactyl through websockets.",
"end_user_data_statement": "This cog does not store end user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).",
"name" : "Pterodactyl",
"short" : "Interface with Pterodactyl through websockets.",
"description" : "Interface with Pterodactyl through websockets.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0],
"requirements": ["git+https://github.com/cswimr/pydactyl", "websockets"],
"tags": ["pterodactyl", "minecraft", "server", "management"]
"tags": [
"pterodactyl",
"minecraft",
"server",
"management"
]
}

View file

@ -1,8 +1,8 @@
from red_commons import logging
from red_commons.logging import getLogger
logger = getLogger("red.SeaCogs.Pterodactyl")
websocket_logger = getLogger("red.SeaCogs.Pterodactyl.Websocket")
logger = getLogger('red.SeaCogs.Pterodactyl')
websocket_logger = getLogger('red.SeaCogs.Pterodactyl.websocket')
if logger.level >= logging.VERBOSE:
websocket_logger.setLevel(logging.logging.INFO)
elif logger.level < logging.VERBOSE:

View file

@ -3,8 +3,8 @@ import aiohttp
async def get_status(host: str, port: int = 25565) -> tuple[bool, dict]:
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.mcsrvstat.us/2/{host}:{port}") as response:
response = await response.json() # noqa: PLW2901
if response["online"]:
async with session.get(f'https://api.mcsrvstat.us/2/{host}:{port}') as response:
response = await response.json()
if response['online']:
return (True, response)
return (False, response)

View file

@ -1,6 +1,6 @@
import asyncio
import json
from typing import AsyncIterable, Iterable, Mapping, Optional, Tuple, Union
from typing import Mapping, Optional, Tuple, Union
import discord
import websockets
@ -9,9 +9,8 @@ from pydactyl import PterodactylClient
from redbot.core import app_commands, commands
from redbot.core.app_commands import Choice
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import bold, box, humanize_list
from redbot.core.utils.chat_formatting import bold, box, error, humanize_list
from redbot.core.utils.views import ConfirmView
from typing_extensions import override
from pterodactyl import mcsrvstatus
from pterodactyl.config import config, register_config
@ -23,20 +22,19 @@ class Pterodactyl(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "2.0.6"
__version__ = "2.0.4"
__documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/"
def __init__(self, bot: Red):
self.bot = bot
self.client: Optional[PterodactylClient] = None
self.task: Optional[asyncio.Task] = None
self.websocket: Optional[websockets.ClientConnection] = None
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
self.retry_counter: int = 0
register_config(config)
self.task = self._get_task()
self.task = self.get_task()
self.update_topic.start()
@override
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
@ -48,57 +46,47 @@ class Pterodactyl(commands.Cog):
]
return "\n".join(text)
@override
async def cog_load(self) -> None:
pterodactyl_keys = await self.bot.get_shared_api_tokens("pterodactyl")
api_key = pterodactyl_keys.get("api_key")
if api_key is None:
self.maybe_cancel_task()
logger.error("Pterodactyl API key not set. Please set it using `[p]set api`.")
return
self.task.cancel()
raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.")
base_url = await config.base_url()
if base_url is None:
self.maybe_cancel_task()
logger.error("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
return
self.task.cancel()
raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
server_id = await config.server_id()
if server_id is None:
self.maybe_cancel_task()
logger.error("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
return
self.task.cancel()
raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
self.client = PterodactylClient(base_url, api_key).client
@override
async def cog_unload(self) -> None:
self.update_topic.cancel()
self.maybe_cancel_task()
def maybe_cancel_task(self, reset_retry_counter: bool = True) -> None:
if self.task:
self.task.cancel()
if reset_retry_counter:
self.retry_counter = 0
await self.client._session.close() # pylint: disable=protected-access
def _get_task(self) -> asyncio.Task:
def get_task(self) -> asyncio.Task:
from pterodactyl.websocket import establish_websocket_connection
task = self.bot.loop.create_task(establish_websocket_connection(self), name="Pterodactyl Websocket Connection")
task.add_done_callback(self._error_callback)
task.add_done_callback(self.error_callback)
return task
def _error_callback(self, fut) -> None: # NOTE Thanks flame442 and zephyrkul for helping me figure this out
def error_callback(self, fut) -> None: #NOTE - Thanks flame442 and zephyrkul for helping me figure this out
try:
fut.result()
except asyncio.CancelledError:
logger.info("WebSocket task has been cancelled.")
except Exception as e: # pylint: disable=broad-exception-caught
logger.error("WebSocket task has failed: %s", e, exc_info=e)
self.maybe_cancel_task(reset_retry_counter=False)
self.task.cancel()
if self.retry_counter < 5:
self.retry_counter += 1
logger.info("Retrying in %s seconds...", 5 * self.retry_counter)
self.task = self.bot.loop.call_later(5 * self.retry_counter, self._get_task)
self.task = self.bot.loop.call_later(5 * self.retry_counter, self.get_task)
else:
logger.info("Retry limit reached. Stopping task.")
@ -109,9 +97,9 @@ class Pterodactyl(commands.Cog):
console = self.bot.get_channel(await config.console_channel())
chat = self.bot.get_channel(await config.chat_channel())
if console:
await console.edit(topic=topic) # type: ignore
await console.edit(topic=topic)
if chat:
await chat.edit(topic=topic) # type: ignore
await chat.edit(topic=topic)
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message) -> None:
@ -122,7 +110,13 @@ class Pterodactyl(commands.Cog):
return
logger.debug("Received console command from %s: %s", message.author.id, message.content)
await message.channel.send(f"Received console command from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none())
await self._send(json.dumps({"event": "send command", "args": [message.content]}))
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [message.content]}))
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
if message.channel.id == await config.chat_channel() and message.author.bot is False:
logger.debug("Received chat message from %s: %s", message.author.id, message.content)
channel = self.bot.get_channel(await config.console_channel())
@ -130,22 +124,13 @@ class Pterodactyl(commands.Cog):
await channel.send(f"Received chat message from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none())
msg = json.dumps({"event": "send command", "args": [await self.get_chat_command(message)]})
logger.debug("Sending chat message to server:\n%s", msg)
await self._send(message=msg)
async def _send(self, message: Union[websockets.Data, Iterable[websockets.Data], AsyncIterable[websockets.Data]], text: bool = False):
"""Send a message through the websocket connection. Restarts the websocket connection task if it is closed, and reinvokes itself."""
try:
await self.websocket.send(message=message, text=text) # type: ignore - we want this to error if `self.websocket` is none
await self.websocket.send(msg)
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
self.maybe_cancel_task()
self.task = self._get_task()
try:
await asyncio.wait_for(fut=self.task, timeout=60)
await self._send(message=message, text=text)
except asyncio.TimeoutError:
logger.error("Timeout while waiting for websocket connection")
raise
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
async def get_topic(self) -> str:
topic: str = await config.topic()
@ -156,27 +141,23 @@ class Pterodactyl(commands.Cog):
if await config.api_endpoint() == "minecraft":
status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port())
if status:
placeholders.update(
{
"I": response["ip"],
"M": str(response["players"]["max"]),
"P": str(response["players"]["online"]),
"V": response["version"],
"D": response["motd"]["clean"][0] if response["motd"]["clean"] else "unset",
},
)
placeholders.update({
"I": response['ip'],
"M": str(response['players']['max']),
"P": str(response['players']['online']),
"V": response['version'],
"D": response['motd']['clean'][0] if response['motd']['clean'] else "unset",
})
else:
placeholders.update(
{
"I": response["ip"],
placeholders.update({
"I": response['ip'],
"M": "0",
"P": "0",
"V": "Server Offline",
"D": "Server Offline",
},
)
})
for key, value in placeholders.items():
topic = topic.replace(".$" + key, value)
topic = topic.replace('.$' + key, value)
return topic
async def get_chat_command(self, message: discord.Message) -> str:
@ -185,45 +166,42 @@ class Pterodactyl(commands.Cog):
"C": str(message.author.color),
"D": message.author.discriminator,
"I": str(message.author.id),
"M": message.content.replace('"', "").replace("\n", " "),
"M": message.content.replace('"','').replace("\n", " "),
"N": message.author.display_name,
"U": message.author.name,
"V": await config.invite() or "use [p]pterodactyl config invite to change me",
}
for key, value in placeholders.items():
command = command.replace(".$" + key, value)
command = command.replace('.$' + key, value)
return command
async def get_player_list(self) -> Optional[Tuple[str, list]]:
if await config.api_endpoint() == "minecraft":
status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port())
if status and "list" in response["players"]:
output_str = "\n".join(response["players"]["list"])
return output_str, response["players"]["list"]
return None
if status and 'list' in response['players']:
output_str = '\n'.join(response['players']['list'])
return output_str, response['players']['list']
return None
async def get_player_list_embed(self, ctx: Union[commands.Context, discord.Interaction]) -> Optional[discord.Embed]:
player_list = await self.get_player_list()
if player_list and isinstance(ctx.channel, discord.abc.Messageable):
if player_list:
embed = discord.Embed(color=await self.bot.get_embed_color(ctx.channel), title="Players Online")
embed.description = player_list[0]
return embed
return None
async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = "") -> None:
async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = '') -> None:
if isinstance(ctx, discord.Interaction):
ctx = await self.bot.get_context(ctx)
current_status = await config.current_status()
if current_status == action_ing:
await ctx.send(f"Server is already {action_ing}.", ephemeral=True)
return
return await ctx.send(f"Server is already {action_ing}.", ephemeral=True)
if current_status in ["starting", "stopping"] and action != "kill":
await ctx.send("Another power action is already in progress.", ephemeral=True)
return
return await ctx.send("Another power action is already in progress.", ephemeral=True)
view = ConfirmView(ctx.author, disable_buttons=True)
@ -234,13 +212,12 @@ class Pterodactyl(commands.Cog):
if view.result is True:
await message.edit(content=f"Sending websocket command to {action} server...", view=None)
await self._websocket_send(json.dumps({"event": "set state", "args": [action]}))
await self.websocket.send(json.dumps({"event": "set state", "args": [action]}))
await message.edit(content=f"Server {action_ing}", view=None)
return
else:
await message.edit(content="Cancelled.", view=None)
return
async def send_command(self, ctx: Union[discord.Interaction, commands.Context], command: str):
channel = self.bot.get_channel(await config.console_channel())
@ -248,19 +225,27 @@ class Pterodactyl(commands.Cog):
ctx = await self.bot.get_context(ctx)
if channel:
await channel.send(f"Received console command from {ctx.author.id}: {command[:1900]}", allowed_mentions=discord.AllowedMentions.none())
await self._websocket_send(json.dumps({"event": "send command", "args": [command]}))
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [command]}))
await ctx.send(f"Command sent to server. {box(command, 'json')}")
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
await ctx.send(error("WebSocket connection closed."))
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
@commands.Cog.listener()
async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str, str]): # pylint: disable=unused-argument
async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str,str]): # pylint: disable=unused-argument
if service_name == "pterodactyl":
logger.info("Configuration value set: api_key\nRestarting task...")
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
slash_pterodactyl = app_commands.Group(name="pterodactyl", description="Pterodactyl allows you to manage your Pterodactyl Panel from Discord.")
@slash_pterodactyl.command(name="command", description="Send a command to the server console.")
@slash_pterodactyl.command(name = "command", description = "Send a command to the server console.")
async def slash_pterodactyl_command(self, interaction: discord.Interaction, command: str) -> None:
"""Send a command to the server console.
@ -270,7 +255,7 @@ class Pterodactyl(commands.Cog):
The command to send to the server."""
return await self.send_command(interaction, command)
@slash_pterodactyl.command(name="players", description="Retrieve a list of players on the server.")
@slash_pterodactyl.command(name = "players", description = "Retrieve a list of players on the server.")
async def slash_pterodactyl_players(self, interaction: discord.Interaction) -> None:
"""Retrieve a list of players on the server."""
e = await self.get_player_list_embed(interaction)
@ -279,8 +264,13 @@ class Pterodactyl(commands.Cog):
else:
await interaction.response.send_message("No players online.", ephemeral=True)
@slash_pterodactyl.command(name="power", description="Send power actions to the server.")
@app_commands.choices(action=[Choice(name="Start", value="start"), Choice(name="Stop", value="stop"), Choice(name="Restart", value="restart"), Choice(name="⚠️ Kill ⚠️", value="kill")])
@slash_pterodactyl.command(name = "power", description = "Send power actions to the server.")
@app_commands.choices(action=[
Choice(name="Start", value="start"),
Choice(name="Stop", value="stop"),
Choice(name="Restart", value="restart"),
Choice(name="⚠️ Kill ⚠️", value="kill")
])
async def slash_pterodactyl_power(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None:
"""Send power actions to the server.
@ -294,11 +284,11 @@ class Pterodactyl(commands.Cog):
return await self.power(interaction, action.value, "stopping...")
return await self.power(interaction, action.value, f"{action.value}ing...")
@commands.group(autohelp=True, name="pterodactyl", aliases=["ptero"])
@commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"])
async def pterodactyl(self, ctx: commands.Context) -> None:
"""Pterodactyl allows you to manage your Pterodactyl Panel from Discord."""
@pterodactyl.command(name="players", aliases=["list", "online", "playerlist", "who"])
@pterodactyl.command(name = "players", aliases=["list", "online", "playerlist", "who"])
async def pterodactyl_players(self, ctx: commands.Context) -> None:
"""Retrieve a list of players on the server."""
e = await self.get_player_list_embed(ctx)
@ -307,43 +297,43 @@ class Pterodactyl(commands.Cog):
else:
await ctx.send("No players online.")
@pterodactyl.command(name="command", aliases=["cmd", "execute", "exec"])
@pterodactyl.command(name = "command", aliases = ["cmd", "execute", "exec"])
@commands.admin()
async def pterodactyl_command(self, ctx: commands.Context, *, command: str) -> None:
"""Send a command to the server console."""
return await self.send_command(ctx, command)
@pterodactyl.group(autohelp=True, name="power")
@pterodactyl.group(autohelp = True, name = "power")
@commands.admin()
async def pterodactyl_power(self, ctx: commands.Context) -> None:
"""Send power actions to the server."""
@pterodactyl_power.command(name="start")
@pterodactyl_power.command(name = "start")
async def pterodactyl_power_start(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Start the server."""
return await self.power(ctx, "start", "starting...")
@pterodactyl_power.command(name="stop")
@pterodactyl_power.command(name = "stop")
async def pterodactyl_power_stop(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Stop the server."""
return await self.power(ctx, "stop", "stopping...")
@pterodactyl_power.command(name="restart")
@pterodactyl_power.command(name = "restart")
async def pterodactyl_power_restart(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Restart the server."""
return await self.power(ctx, "restart", "restarting...")
@pterodactyl_power.command(name="kill")
@pterodactyl_power.command(name = "kill")
async def pterodactyl_power_kill(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Kill the server."""
return await self.power(ctx, "kill", "stopping... (forcefully killed)", warning="**⚠️ Forcefully killing the server process can corrupt data in some cases. ⚠️**\n")
@pterodactyl.group(autohelp=True, name="config", aliases=["settings", "set"])
@pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"])
@commands.is_owner()
async def pterodactyl_config(self, ctx: commands.Context) -> None:
"""Configure Pterodactyl settings."""
@pterodactyl_config.command(name="url")
@pterodactyl_config.command(name = "url")
async def pterodactyl_config_base_url(self, ctx: commands.Context, *, base_url: str) -> None:
"""Set the base URL of your Pterodactyl Panel.
@ -352,57 +342,59 @@ class Pterodactyl(commands.Cog):
await config.base_url.set(base_url)
await ctx.send(f"Base URL set to {base_url}")
logger.info("Configuration value set: base_url = %s\nRestarting task...", base_url)
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
@pterodactyl_config.command(name="serverid")
@pterodactyl_config.command(name = "serverid")
async def pterodactyl_config_server_id(self, ctx: commands.Context, *, server_id: str) -> None:
"""Set the ID of your server."""
await config.server_id.set(server_id)
await ctx.send(f"Server ID set to {server_id}")
logger.info("Configuration value set: server_id = %s\nRestarting task...", server_id)
self.maybe_cancel_task(reset_retry_counter=True)
self.task = self._get_task()
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
@pterodactyl_config.group(name="console")
@pterodactyl_config.group(name = "console")
async def pterodactyl_config_console(self, ctx: commands.Context):
"""Configure console settings."""
@pterodactyl_config_console.command(name="channel")
@pterodactyl_config_console.command(name = "channel")
async def pterodactyl_config_console_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send console output to."""
await config.console_channel.set(channel.id)
await ctx.send(f"Console channel set to {channel.mention}")
@pterodactyl_config_console.command(name="commands")
@pterodactyl_config_console.command(name = "commands")
async def pterodactyl_config_console_commands(self, ctx: commands.Context, enabled: bool) -> None:
"""Enable or disable console commands."""
await config.console_commands_enabled.set(enabled)
await ctx.send(f"Console commands set to {enabled}")
@pterodactyl_config.command(name="invite")
@pterodactyl_config.command(name = "invite")
async def pterodactyl_config_invite(self, ctx: commands.Context, invite: str) -> None:
"""Set the invite link for your server."""
await config.invite.set(invite)
await ctx.send(f"Invite link set to {invite}")
@pterodactyl_config.group(name="topic")
@pterodactyl_config.group(name = "topic")
async def pterodactyl_config_topic(self, ctx: commands.Context):
"""Set the topic for the console and chat channels."""
@pterodactyl_config_topic.command(name="host", aliases=["hostname", "ip"])
@pterodactyl_config_topic.command(name = "host", aliases = ["hostname", "ip"])
async def pterodactyl_config_topic_host(self, ctx: commands.Context, host: str) -> None:
"""Set the hostname or IP address of your server."""
await config.topic_hostname.set(host)
await ctx.send(f"Hostname/IP set to `{host}`")
@pterodactyl_config_topic.command(name="port")
@pterodactyl_config_topic.command(name = "port")
async def pterodactyl_config_topic_port(self, ctx: commands.Context, port: int) -> None:
"""Set the port of your server."""
await config.topic_port.set(port)
await ctx.send(f"Port set to `{port}`")
@pterodactyl_config_topic.command(name="text")
@pterodactyl_config_topic.command(name = "text")
async def pterodactyl_config_topic_text(self, ctx: commands.Context, *, text: str) -> None:
"""Set the text for the console and chat channels.
@ -418,17 +410,17 @@ class Pterodactyl(commands.Cog):
await config.topic.set(text)
await ctx.send(f"Topic set to:\n{box(text, 'yaml')}")
@pterodactyl_config.group(name="chat")
@pterodactyl_config.group(name = "chat")
async def pterodactyl_config_chat(self, ctx: commands.Context):
"""Configure chat settings."""
@pterodactyl_config_chat.command(name="channel")
@pterodactyl_config_chat.command(name = "channel")
async def pterodactyl_config_chat_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
"""Set the channel to send chat output to."""
await config.chat_channel.set(channel.id)
await ctx.send(f"Chat channel set to {channel.mention}")
@pterodactyl_config_chat.command(name="command")
@pterodactyl_config_chat.command(name = "command")
async def pterodactyl_config_chat_command(self, ctx: commands.Context, *, command: str) -> None:
"""Set the command that will be used to send messages from Discord.
@ -437,11 +429,11 @@ class Pterodactyl(commands.Cog):
await config.chat_command.set(command)
await ctx.send(f"Chat command set to:\n{box(command, 'json')}")
@pterodactyl_config.group(name="regex")
@pterodactyl_config.group(name = "regex")
async def pterodactyl_config_regex(self, ctx: commands.Context) -> None:
"""Set regex patterns."""
@pterodactyl_config_regex.command(name="chat")
@pterodactyl_config_regex.command(name = "chat")
async def pterodactyl_config_regex_chat(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match chat messages on the server.
@ -449,7 +441,7 @@ class Pterodactyl(commands.Cog):
await config.chat_regex.set(regex)
await ctx.send(f"Chat regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name="server")
@pterodactyl_config_regex.command(name = "server")
async def pterodactyl_config_regex_server(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match server messages on the server.
@ -457,7 +449,7 @@ class Pterodactyl(commands.Cog):
await config.server_regex.set(regex)
await ctx.send(f"Server regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name="join")
@pterodactyl_config_regex.command(name = "join")
async def pterodactyl_config_regex_join(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match join messages on the server.
@ -465,7 +457,7 @@ class Pterodactyl(commands.Cog):
await config.join_regex.set(regex)
await ctx.send(f"Join regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name="leave")
@pterodactyl_config_regex.command(name = "leave")
async def pterodactyl_config_regex_leave(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match leave messages on the server.
@ -473,7 +465,7 @@ class Pterodactyl(commands.Cog):
await config.leave_regex.set(regex)
await ctx.send(f"Leave regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config_regex.command(name="achievement")
@pterodactyl_config_regex.command(name = "achievement")
async def pterodactyl_config_regex_achievement(self, ctx: commands.Context, *, regex: str) -> None:
"""Set the regex pattern to match achievement messages on the server.
@ -481,41 +473,41 @@ class Pterodactyl(commands.Cog):
await config.achievement_regex.set(regex)
await ctx.send(f"Achievement regex set to:\n{box(regex, 'regex')}")
@pterodactyl_config.group(name="messages", aliases=["msg", "msgs", "message"])
@pterodactyl_config.group(name = "messages", aliases = ['msg', 'msgs', 'message'])
async def pterodactyl_config_messages(self, ctx: commands.Context):
"""Configure message settings."""
@pterodactyl_config_messages.command(name="startup")
@pterodactyl_config_messages.command(name = "startup")
async def pterodactyl_config_messages_startup(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when the server starts."""
await config.startup_msg.set(message)
await ctx.send(f"Startup message set to: {message}")
@pterodactyl_config_messages.command(name="shutdown")
@pterodactyl_config_messages.command(name = "shutdown")
async def pterodactyl_config_messages_shutdown(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when the server stops."""
await config.shutdown_msg.set(message)
await ctx.send(f"Shutdown message set to: {message}")
@pterodactyl_config_messages.command(name="join")
@pterodactyl_config_messages.command(name = "join")
async def pterodactyl_config_messages_join(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when a user joins the server. This is only shown in embeds."""
await config.join_msg.set(message)
await ctx.send(f"Join message set to: {message}")
@pterodactyl_config_messages.command(name="leave")
@pterodactyl_config_messages.command(name = "leave")
async def pterodactyl_config_messages_leave(self, ctx: commands.Context, *, message: str) -> None:
"""Set the message that will be sent when a user leaves the server. This is only shown in embeds."""
await config.leave_msg.set(message)
await ctx.send(f"Leave message set to: {message}")
@pterodactyl_config.command(name="ip")
@pterodactyl_config.command(name = "ip")
async def pterodactyl_config_mask_ip(self, ctx: commands.Context, mask: bool) -> None:
"""Mask the IP addresses of users in console messages."""
await config.mask_ip.set(mask)
await ctx.send(f"IP masking set to {mask}")
@pterodactyl_config.command(name="api")
@pterodactyl_config.command(name = "api")
async def pterodactyl_config_api(self, ctx: commands.Context, endpoint: str) -> None:
"""Set the API endpoint for retrieving user avatars.
@ -524,14 +516,11 @@ class Pterodactyl(commands.Cog):
await config.api_endpoint.set(endpoint)
await ctx.send(f"API endpoint set to {endpoint}")
@pterodactyl_config_regex.group(
name="blacklist",
aliases=["block", "blocklist"],
)
@pterodactyl_config_regex.group(name = "blacklist", aliases = ['block', 'blocklist'],)
async def pterodactyl_config_regex_blacklist(self, ctx: commands.Context):
"""Blacklist regex patterns."""
@pterodactyl_config_regex_blacklist.command(name="add")
@pterodactyl_config_regex_blacklist.command(name = "add")
async def pterodactyl_config_regex_blacklist_add(self, ctx: commands.Context, name: str, *, regex: str) -> None:
"""Add a regex pattern to the blacklist."""
async with config.regex_blacklist() as blacklist:
@ -549,7 +538,7 @@ class Pterodactyl(commands.Cog):
else:
await msg.edit(content="Cancelled.")
@pterodactyl_config_regex_blacklist.command(name="remove")
@pterodactyl_config_regex_blacklist.command(name = "remove")
async def pterodactyl_config_regex_blacklist_remove(self, ctx: commands.Context, name: str) -> None:
"""Remove a regex pattern from the blacklist."""
async with config.regex_blacklist() as blacklist:
@ -566,7 +555,7 @@ class Pterodactyl(commands.Cog):
else:
await ctx.send(f"Name `{name}` does not exist in the blacklist.")
@pterodactyl_config.command(name="view", aliases=["show"])
@pterodactyl_config.command(name = 'view', aliases = ['show'])
async def pterodactyl_config_view(self, ctx: commands.Context) -> None:
"""View the current configuration."""
base_url = await config.base_url()
@ -591,7 +580,7 @@ class Pterodactyl(commands.Cog):
topic_text = await config.topic()
topic_hostname = await config.topic_hostname()
topic_port = await config.topic_port()
embed = discord.Embed(color=await ctx.embed_color(), title="Pterodactyl Configuration")
embed = discord.Embed(color = await ctx.embed_color(), title="Pterodactyl Configuration")
embed.description = f"""**Base URL:** {base_url}
**Server ID:** `{server_id}`
**Console Channel:** <#{console_channel}>
@ -607,19 +596,19 @@ class Pterodactyl(commands.Cog):
**Topic Hostname:** `{topic_hostname}`
**Topic Port:** `{topic_port}`
**Topic Text:** {box(topic_text, "yaml")}
**Topic Text:** {box(topic_text, 'yaml')}
**Chat Command:** {box(chat_command, "json")}
**Chat Regex:** {box(chat_regex, "re")}
**Server Regex:** {box(server_regex, "re")}
**Join Regex:** {box(join_regex, "re")}
**Leave Regex:** {box(leave_regex, "re")}
**Achievement Regex:** {box(achievement_regex, "re")}"""
**Chat Command:** {box(chat_command, 'json')}
**Chat Regex:** {box(chat_regex, 're')}
**Server Regex:** {box(server_regex, 're')}
**Join Regex:** {box(join_regex, 're')}
**Leave Regex:** {box(leave_regex, 're')}
**Achievement Regex:** {box(achievement_regex, 're')}"""
await ctx.send(embed=embed)
if not len(regex_blacklist) == 0:
regex_blacklist_embed = discord.Embed(color=await ctx.embed_color(), title="Regex Blacklist")
regex_blacklist_embed = discord.Embed(color = await ctx.embed_color(), title="Regex Blacklist")
for name, regex in regex_blacklist.items():
regex_blacklist_embed.add_field(name=name, value=box(regex, "re"), inline=False)
regex_blacklist_embed.add_field(name=name, value=box(regex, 're'), inline=False)
await ctx.send(embed=regex_blacklist_embed)
def get_bool_str(self, inp: bool) -> str:

View file

@ -2,14 +2,14 @@
import json
import re
from pathlib import Path
from typing import Any, Optional, Tuple, Union
from typing import Optional, Tuple, Union
import aiohttp
import discord
import websockets
from pydactyl import PterodactylClient
from redbot.core.data_manager import bundled_data_path
from redbot.core.utils.chat_formatting import bold, pagify
from websockets.asyncio.client import connect
from pterodactyl.config import config
from pterodactyl.logger import logger, websocket_logger
@ -19,16 +19,16 @@ from pterodactyl.pterodactyl import Pterodactyl
async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
await coginstance.bot.wait_until_red_ready()
base_url = await config.base_url()
base_url = base_url[:-1] if base_url.endswith("/") else base_url
base_url = base_url[:-1] if base_url.endswith('/') else base_url
logger.info("Establishing WebSocket connection")
websocket_credentials = await retrieve_websocket_credentials(coginstance)
async with connect(websocket_credentials["data"]["socket"], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket:
async with websockets.connect(websocket_credentials['data']['socket'], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket:
logger.info("WebSocket connection established")
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]})
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]})
await websocket.send(auth_message)
logger.info("Authentication message sent")
@ -36,31 +36,29 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
while True: # pylint: disable=too-many-nested-blocks
message = json.loads(await websocket.recv())
if message["event"] in ("token expiring", "token expired"):
if message['event'] in ('token expiring', 'token expired'):
logger.info("Received token expiring/expired event. Refreshing token.")
websocket_credentials = await retrieve_websocket_credentials(coginstance)
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]})
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]})
await websocket.send(auth_message)
logger.info("Authentication message sent")
if message["event"] == "auth success":
if message['event'] == 'auth success':
logger.info("WebSocket authentication successful")
if message["event"] == "console output" and await config.console_channel() is not None:
if message['event'] == 'console output' and await config.console_channel() is not None:
regex_blacklist: dict = await config.regex_blacklist()
matches = [re.search(regex, message["args"][0]) for regex in regex_blacklist.values()]
matches = [re.search(regex, message['args'][0]) for regex in regex_blacklist.values()]
if await config.current_status() in ("running", "") and not any(matches):
content = remove_ansi_escape_codes(message["args"][0])
if await config.current_status() in ('running', '') and not any(matches):
content = remove_ansi_escape_codes(message['args'][0])
if await config.mask_ip() is True:
content = mask_ip(content)
console_channel = coginstance.bot.get_channel(await config.console_channel())
assert isinstance(console_channel, discord.abc.Messageable)
chat_channel = coginstance.bot.get_channel(await config.chat_channel())
assert isinstance(chat_channel, discord.abc.Messageable)
if console_channel is not None:
if content.startswith("["):
if content.startswith('['):
pagified_content = pagify(content, delims=[" ", "\n"])
for page in pagified_content:
await console_channel.send(content=page, allowed_mentions=discord.AllowedMentions.none())
@ -68,24 +66,24 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
server_message = await check_if_server_message(content)
if server_message:
if chat_channel is not None:
await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + "...", allowed_mentions=discord.AllowedMentions.none())
await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + '...', allowed_mentions=discord.AllowedMentions.none())
chat_message = await check_if_chat_message(content)
if chat_message:
info = await get_info(chat_message["username"])
info = await get_info(chat_message['username'])
if info is not None:
await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], info["data"]["player"]["avatar"])
await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], info['data']['player']['avatar'])
else:
await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], "https://seafsh.cc/u/j3AzqQ.png")
await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], 'https://seafsh.cc/u/j3AzqQ.png')
join_message = await check_if_join_message(content)
if join_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message, join=True)
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message,join=True)
if img:
with open(img, "rb") as file:
await chat_channel.send(embed=embed, file=discord.File(fp=file))
with open(img, 'rb') as file:
await chat_channel.send(embed=embed, file=file)
else:
await chat_channel.send(embed=embed)
else:
@ -95,10 +93,10 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if leave_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message, join=False)
embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message,join=False)
if img:
with open(img, "rb") as file:
await chat_channel.send(embed=embed, file=discord.File(fp=file))
with open(img, 'rb') as file:
await chat_channel.send(embed=embed, file=file)
else:
await chat_channel.send(embed=embed)
else:
@ -108,17 +106,13 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if achievement_message:
if chat_channel is not None:
if coginstance.bot.embed_requested(chat_channel):
embed, img = await generate_achievement_embed(coginstance, achievement_message["username"], achievement_message["achievement"], achievement_message["challenge"])
if img:
await chat_channel.send(embed=embed, file=discord.File(fp=img))
else:
await chat_channel.send(embed=embed)
await chat_channel.send(embed=await generate_achievement_embed(coginstance, achievement_message['username'], achievement_message['achievement'], achievement_message['challenge']))
else:
await chat_channel.send(f"{achievement_message['username']} has {'completed the challenge' if achievement_message['challenge'] else 'made the advancement'} {achievement_message['achievement']}")
if message["event"] == "status":
if message['event'] == 'status':
old_status = await config.current_status()
current_status = message["args"][0]
current_status = message['args'][0]
if old_status != current_status:
await config.current_status.set(current_status)
if await config.console_channel() is not None:
@ -126,92 +120,81 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if console is not None:
await console.send(f"Server status changed! `{current_status}`")
if await config.chat_channel() is not None:
if current_status == "running" and await config.startup_msg() is not None:
if current_status == 'running' and await config.startup_msg() is not None:
chat = coginstance.bot.get_channel(await config.chat_channel())
if chat is not None:
await chat.send(await config.startup_msg())
if current_status == "stopping" and await config.shutdown_msg() is not None:
if current_status == 'stopping' and await config.shutdown_msg() is not None:
chat = coginstance.bot.get_channel(await config.chat_channel())
if chat is not None:
await chat.send(await config.shutdown_msg())
async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> dict:
async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> Optional[dict]:
pterodactyl_keys = await coginstance.bot.get_shared_api_tokens("pterodactyl")
api_key = pterodactyl_keys.get("api_key")
if api_key is None:
coginstance.maybe_cancel_task()
coginstance.task.cancel()
raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.")
base_url = await config.base_url()
if base_url is None:
coginstance.maybe_cancel_task()
coginstance.task.cancel()
raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.")
server_id = await config.server_id()
if server_id is None:
coginstance.maybe_cancel_task()
coginstance.task.cancel()
raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
client = PterodactylClient(base_url, api_key).client
coginstance.client = client
websocket_credentials: dict[str, Any] = client.servers.get_websocket(server_id).json()
if not websocket_credentials:
coginstance.maybe_cancel_task()
raise ValueError("Failed to retrieve websocket credentials. Please ensure the API details are correctly configured.")
logger.debug(
"""Websocket connection details retrieved:
websocket_credentials = client.servers.get_websocket(server_id)
logger.debug("""Websocket connection details retrieved:
Socket: %s
Token: %s...""",
websocket_credentials["data"]["socket"],
websocket_credentials["data"]["token"][:20],
websocket_credentials['data']['socket'],
websocket_credentials['data']['token'][:20]
)
return websocket_credentials
# NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons
#NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons
def remove_ansi_escape_codes(text: str) -> str:
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
# NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540
return ansi_escape.sub("", text)
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
#NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540
return ansi_escape.sub('', text)
async def check_if_server_message(text: str) -> Optional[str]:
async def check_if_server_message(text: str) -> Union[bool, str]:
regex = await config.server_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a server message")
return match.group(1)
return None
return False
async def check_if_chat_message(text: str) -> Optional[dict]:
async def check_if_chat_message(text: str) -> Union[bool, dict]:
regex = await config.chat_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
groups = {"username": match.group(1), "message": match.group(2)}
logger.trace("Message is a chat message\n%s", json.dumps(groups))
return groups
return None
return False
async def check_if_join_message(text: str) -> Optional[str]:
async def check_if_join_message(text: str) -> Union[bool, str]:
regex = await config.join_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a join message")
return match.group(1)
return None
return False
async def check_if_leave_message(text: str) -> Optional[str]:
async def check_if_leave_message(text: str) -> Union[bool, str]:
regex = await config.leave_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
logger.trace("Message is a leave message")
return match.group(1)
return None
return False
async def check_if_achievement_message(text: str) -> Optional[dict]:
async def check_if_achievement_message(text: str) -> Union[bool, dict]:
regex = await config.achievement_regex()
match: Optional[re.Match[str]] = re.match(regex, text)
if match:
@ -222,8 +205,7 @@ async def check_if_achievement_message(text: str) -> Optional[dict]:
groups["challenge"] = False
logger.trace("Message is an achievement message")
return groups
return None
return False
async def get_info(username: str) -> Optional[dict]:
logger.verbose("Retrieving player info for %s", username)
@ -236,7 +218,6 @@ async def get_info(username: str) -> Optional[dict]:
logger.warning("Failed to retrieve player info for %s: %s", username, response.status)
return None
async def send_chat_discord(coginstance: Pterodactyl, username: str, message: str, avatar_url: str) -> None:
logger.trace("Sending chat message to Discord")
channel = coginstance.bot.get_channel(await config.chat_channel())
@ -250,7 +231,6 @@ async def send_chat_discord(coginstance: Pterodactyl, username: str, message: st
else:
logger.warning("Chat channel not set. Skipping sending chat message to Discord")
async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, join: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]:
embed = discord.Embed()
embed.color = discord.Color.green() if join else discord.Color.red()
@ -258,32 +238,30 @@ async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, joi
info = await get_info(username)
if info:
img = None
embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"])
embed.set_author(name=username, icon_url=info['data']['player']['avatar'])
else:
img = bundled_data_path(coginstance) / "unknown.png"
embed.set_author(name=username, icon_url="attachment://unknown.png")
embed.set_author(name=username, icon_url='attachment://unknown.png')
embed.timestamp = discord.utils.utcnow()
return embed, img
async def generate_achievement_embed(coginstance: Pterodactyl, username: str, achievement: str, challenge: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]:
embed = discord.Embed()
embed.color = discord.Color.from_str("#a800a7") if challenge else discord.Color.from_str("#54fb54")
embed.color = discord.Color.from_str('#a800a7') if challenge else discord.Color.from_str('#54fb54')
embed.description = f"{bold(username)} has {'completed the challenge' if challenge else 'made the advancement'} {bold(achievement)}"
info = await get_info(username)
if info:
img = None
embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"])
embed.set_author(name=username, icon_url=info['data']['player']['avatar'])
else:
img = bundled_data_path(coginstance) / "unknown.png"
embed.set_author(name=username, icon_url="attachment://unknown.png")
embed.set_author(name=username, icon_url='attachment://unknown.png')
embed.timestamp = discord.utils.utcnow()
return embed, img
def mask_ip(string: str) -> str:
def check(match: re.Match[str]):
ip = match.group(0)
return ".".join(r"\*" * len(octet) for octet in ip.split("."))
return re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", check, string)
masked_ip = '.'.join(r'\*' * len(octet) for octet in ip.split('.'))
return masked_ip
return re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', check, string)

View file

@ -2,46 +2,45 @@
name = "seacogs"
version = "0.1.0"
description = "My assorted cogs for Red-DiscordBot."
authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }]
license = { file = "LICENSE" }
authors = [{name = "cswimr", email = "seaswimmerthefsh@gmail.com"}]
license = {file="LICENSE"}
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"aiosqlite>=0.20.0",
"beautifulsoup4>=4.12.3",
"colorthief>=0.2.1",
"markdownify>=0.14.1",
"numpy>=2.2.2",
"phx-class-registry>=5.1.1",
"markdownify>=0.13.1",
"numpy>=2.1.2",
"phx-class-registry>=5.0.0",
"pillow>=10.4.0",
"pip>=25.0",
"pip>=24.3.1",
"py-dactyl",
"pydantic>=2.10.6",
"pydantic>=2.9.2",
"red-discordbot>=3.5.14",
"watchdog>=6.0.0",
"websockets>=14.2",
"websockets>=13.1",
]
[dependency-groups]
[project.optional-dependencies]
documentation = [
"mkdocs>=1.6.1",
"mkdocs-git-authors-plugin>=0.9.2",
"mkdocs-git-revision-date-localized-plugin>=1.3.0",
"mkdocs-material[imaging]>=9.5.50",
"mkdocs-redirects>=1.2.2",
"mkdocstrings[python]>=0.27.0",
"mkdocs-git-authors-plugin>=0.9.0",
"mkdocs-git-revision-date-localized-plugin>=1.2.9",
"mkdocs-material[imaging]>=9.5.40",
"mkdocstrings[python]>=0.26.1",
"mkdocs-redirects>=1.2.1",
]
[tool.uv]
dev-dependencies = ["pylint>=3.3.3", "ruff>=0.9.3", "sqlite-web>=0.6.4"]
dev-dependencies = [
"pylint>=3.3.1",
"ruff>=0.6.9",
"sqlite-web>=0.6.4",
]
[tool.uv.sources]
py-dactyl = { git = "https://github.com/cswimr/pydactyl" }
[tool.basedpyright]
typeCheckingMode = "basic"
reportAttributeAccessIssue = false # disabled because `commands.group.command` is listed as Any / Unknown for some reason
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
@ -84,32 +83,8 @@ target-version = "py311"
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = [
"I",
"N",
"F",
"W",
"E",
"G",
"A",
"COM",
"INP",
"T20",
"PLC",
"PLE",
"PLW",
"PLR",
"LOG",
"SLF",
"ERA",
"FIX",
"PERF",
"C4",
"EM",
"RET",
"RSE",
]
ignore = ["PLR0911", "PLR0912", "PLR0915", "PLR2004", "PLR0913", "EM101"]
select = ["F", "W", "E", "C901"]
ignore = ["C901"]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]

View file

@ -1,11 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json",
"author": ["cswimr"],
"install_msg": "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name": "SeaUtils",
"short": "A collection of useful utilities.",
"description": "A collection of useful utilities.",
"end_user_data_statement": "This cog does not store end user data.",
"author" : ["cswimr"],
"install_msg" : "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).",
"name" : "SeaUtils",
"short" : "A collection of useful utilities.",
"description" : "A collection of useful utilities.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": true,
"disabled": false,
"min_bot_version": "3.5.0",

View file

@ -29,20 +29,18 @@ from redbot.core.utils.views import SimpleMenu
def md(soup: BeautifulSoup, **options) -> Any | str:
return MarkdownConverter(**options).convert_soup(soup=soup)
def format_rfc_text(text: str, number: int) -> str:
one: str = re.sub(r"\(\.\/rfc(\d+)", r"(https://www.rfc-editor.org/rfc/rfc\1.html", text)
two: str = re.sub(r"\((#(?:section|page)-\d+(?:.\d+)?)\)", f"(https://www.rfc-editor.org/rfc/rfc{number}.html\1)", one)
three: str = re.sub(r"\n{3,}", "\n\n", two)
return three
class SeaUtils(commands.Cog):
"""A collection of random utilities."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.0.2"
__version__ = "1.0.1"
__documentation__ = "https://seacogs.coastalcommits.com/seautils/"
def __init__(self, bot: Red) -> None:
@ -59,6 +57,7 @@ class SeaUtils(commands.Cog):
]
return "\n".join(text)
def format_src(self, obj: Any) -> str:
"""A large portion of this code is repurposed from Zephyrkul's RTFS cog.
https://github.com/Zephyrkul/FluffyCogs/blob/master/rtfs/rtfs.py"""
@ -74,9 +73,9 @@ class SeaUtils(commands.Cog):
src = obj.function
return inspect.getsource(object=src)
@commands.command(aliases=["source", "src", "code", "showsource"]) # type: ignore
@commands.command(aliases=["source", "src", "code", "showsource"])
@commands.is_owner()
async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin # noqa: A002
async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin
"""Show the code for a particular object."""
try:
if object.startswith("/") and (obj := ctx.bot.tree.get_command(object[1:])):
@ -87,7 +86,11 @@ class SeaUtils(commands.Cog):
text = self.format_src(obj)
else:
raise AttributeError
temp_content = cf.pagify(text=cleanup_code(text), escape_mass_mentions=True, page_length=1977)
temp_content = cf.pagify(
text=cleanup_code(text),
escape_mass_mentions=True,
page_length = 1977
)
content = []
max_i = operator.length_hint(temp_content)
i = 1
@ -102,7 +105,7 @@ class SeaUtils(commands.Cog):
else:
await ctx.send(content="Object not found!", reference=ctx.message.to_reference(fail_if_not_exists=False))
@commands.command(name="dig", aliases=["dnslookup", "nslookup"]) # type: ignore
@commands.command(name='dig', aliases=['dnslookup', 'nslookup'])
@commands.is_owner()
async def dig(self, ctx: commands.Context, name: str, record_type: str | None = None, server: str | None = None, port: int = 53) -> None:
"""Retrieve DNS information for a domain.
@ -110,13 +113,13 @@ class SeaUtils(commands.Cog):
Uses `dig` to perform a DNS query. Will fall back to `nslookup` if `dig` is not installed on the system.
`nslookup` does not provide as much information as `dig`, so only the `name` parameter will be used if `nslookup` is used.
Will return the A, AAAA, and CNAME records for a domain by default. You can specify a different record type with the `type` parameter."""
command_opts: list[str] = ["dig"]
query_types: list[str] = [record_type] if record_type else ["A", "AAAA", "CNAME"]
command_opts: list[str | int] = ['dig']
query_types: list[str] = [record_type] if record_type else ['A', 'AAAA', 'CNAME']
if server:
command_opts.extend(["@", server])
command_opts.extend(['@', server])
for query_type in query_types:
command_opts.extend([name, query_type])
command_opts.extend(["-p", str(port), "+yaml"])
command_opts.extend(['-p', str(port), '+yaml'])
try:
process: Process = await asyncio.create_subprocess_exec(*command_opts, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
@ -125,18 +128,22 @@ class SeaUtils(commands.Cog):
await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=stderr.decode()))
else:
data = yaml.safe_load(stdout.decode())
message_data: dict = data[0]["message"]
response_data: dict = message_data["response_message_data"]
message_data: dict = data[0]['message']
response_data: dict = message_data['response_message_data']
if ctx.embed_requested():
embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=message_data["response_time"])
embed.add_field(name="Response Address", value=message_data["response_address"], inline=True)
embed.add_field(name="Response Port", value=message_data["response_port"], inline=True)
embed.add_field(name="Query Address", value=message_data["query_address"], inline=True)
embed.add_field(name="Query Port", value=message_data["query_port"], inline=True)
embed.add_field(name="Status", value=response_data["status"], inline=True)
embed.add_field(name="Flags", value=response_data["flags"], inline=True)
embed = Embed(
title="DNS Query Result",
color=await ctx.embed_color(),
timestamp=message_data['response_time']
)
embed.add_field(name="Response Address", value=message_data['response_address'], inline=True)
embed.add_field(name="Response Port", value=message_data['response_port'], inline=True)
embed.add_field(name="Query Address", value=message_data['query_address'], inline=True)
embed.add_field(name="Query Port", value=message_data['query_port'], inline=True)
embed.add_field(name="Status", value=response_data['status'], inline=True)
embed.add_field(name="Flags", value=response_data['flags'], inline=True)
if response_data.get("status") != "NOERROR":
if response_data.get('status') != 'NOERROR':
embed.colour = Color.red()
embed.description = cf.error("Dig query did not return `NOERROR` status.")
@ -144,19 +151,19 @@ class SeaUtils(commands.Cog):
answers = []
authorities = []
for m in data:
response = m["message"]["response_message_data"]
if "QUESTION_SECTION" in response:
for question in response["QUESTION_SECTION"]:
response = m['message']['response_message_data']
if 'QUESTION_SECTION' in response:
for question in response['QUESTION_SECTION']:
if question not in questions:
questions.append(question)
if "ANSWER_SECTION" in response:
for answer in response["ANSWER_SECTION"]:
if 'ANSWER_SECTION' in response:
for answer in response['ANSWER_SECTION']:
if answer not in answers:
answers.append(answer)
if "AUTHORITY_SECTION" in response:
for authority in response["AUTHORITY_SECTION"]:
if 'AUTHORITY_SECTION' in response:
for authority in response['AUTHORITY_SECTION']:
if authority not in authorities:
authorities.append(authority)
@ -176,22 +183,26 @@ class SeaUtils(commands.Cog):
embed.add_field(name="Authority Section", value=f"{cf.box(text=authority_section, lang='prolog')}", inline=False)
await ctx.send(embed=embed)
else:
await ctx.send(content=cf.box(text=str(stdout), lang="yaml"))
except FileNotFoundError:
await ctx.send(content=cf.box(text=stdout, lang='yaml'))
except (FileNotFoundError):
try:
ns_process = await asyncio.create_subprocess_exec("nslookup", name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
ns_process = await asyncio.create_subprocess_exec('nslookup', name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
ns_stdout, ns_stderr = await ns_process.communicate()
if ns_stderr:
await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=ns_stderr.decode()))
else:
warning = cf.warning("`dig` is not installed! Defaulting to `nslookup`.\nThis command provides more information when `dig` is installed on the system.\n")
if await ctx.embed_requested():
embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=ctx.message.created_at)
embed = Embed(
title="DNS Query Result",
color=await ctx.embed_color(),
timestamp=ctx.message.created_at
)
embed.description = warning + cf.box(text=ns_stdout.decode())
await ctx.send(embed=embed)
else:
await ctx.send(content=warning + cf.box(text=ns_stdout.decode()))
except FileNotFoundError:
await ctx.send(content = warning + cf.box(text=ns_stdout.decode()))
except (FileNotFoundError):
await ctx.maybe_send_embed(message=cf.error("Neither `dig` nor `nslookup` are installed on the system. Unable to resolve DNS query."))
@commands.command()
@ -206,27 +217,38 @@ class SeaUtils(commands.Cog):
async with session.get(url=url) as response:
if response.status == 200:
html = await response.text()
soup = BeautifulSoup(html, "html.parser")
pre_tags = soup.find_all("pre")
content: list[str | Embed] = []
soup = BeautifulSoup(html, 'html.parser')
pre_tags = soup.find_all('pre')
content: list[Embed | str] = []
for pre_tag in pre_tags:
text = format_rfc_text(md(pre_tag), number)
if len(text) > 4096:
pagified_text = cf.pagify(text, delims=["\n\n"], page_length=4096)
for page in pagified_text:
if await ctx.embed_requested():
embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=page, color=await ctx.embed_color())
embed = Embed(
title=f"RFC Document {number}",
url=datatracker_url,
description=page,
color=await ctx.embed_color()
)
content.append(embed)
else:
content.append(page)
elif await ctx.embed_requested():
embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=text, color=await ctx.embed_color())
else:
if await ctx.embed_requested():
embed = Embed(
title=f"RFC Document {number}",
url=datatracker_url,
description=text,
color=await ctx.embed_color()
)
content.append(embed)
else:
content.append(text)
if await ctx.embed_requested():
for embed in content:
embed.set_footer(text=f"Page {content.index(embed) + 1}/{len(content)}")
await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx) # type: ignore
await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx)
else:
await ctx.maybe_send_embed(message=cf.error(f"An error occurred while fetching RFC {number}. Status code: {response.status}."))

919
uv.lock generated

File diff suppressed because it is too large Load diff