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

Merge branch 'main' into githubapi

This commit is contained in:
Kasen Engel 2024-04-14 06:56:36 -07:00 committed by GitHub
commit 5f7178b2f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1017 additions and 187 deletions

248
poetry.lock generated
View file

@ -255,6 +255,47 @@ files = [
[package.extras]
develop = ["aiomisc-pytest", "pytest", "pytest-cov"]
[[package]]
name = "cairocffi"
version = "1.6.1"
description = "cffi-based cairo bindings for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "cairocffi-1.6.1-py3-none-any.whl", hash = "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e"},
{file = "cairocffi-1.6.1.tar.gz", hash = "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7"},
]
[package.dependencies]
cffi = ">=1.1.0"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["flake8", "isort", "numpy", "pikepdf", "pytest"]
xcb = ["xcffib (>=1.4.0)"]
[[package]]
name = "cairosvg"
version = "2.7.1"
description = "A Simple SVG Converter based on Cairo"
optional = false
python-versions = ">=3.5"
files = [
{file = "CairoSVG-2.7.1-py3-none-any.whl", hash = "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b"},
{file = "CairoSVG-2.7.1.tar.gz", hash = "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0"},
]
[package.dependencies]
cairocffi = "*"
cssselect2 = "*"
defusedxml = "*"
pillow = "*"
tinycss2 = "*"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["flake8", "isort", "pytest"]
[[package]]
name = "certifi"
version = "2024.2.2"
@ -466,6 +507,7 @@ files = [
]
[[package]]
name = "cryptography"
version = "42.0.5"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
@ -519,6 +561,25 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
name = "cssselect2"
version = "0.7.0"
description = "CSS selectors for Python ElementTree"
optional = false
python-versions = ">=3.7"
files = [
{file = "cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969"},
{file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"},
]
[package.dependencies]
tinycss2 = "*"
webencodings = "*"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["flake8", "isort", "pytest"]
[[package]]
name = "dateparser"
version = "1.2.0"
@ -558,6 +619,18 @@ wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "discord-py"
version = "2.3.2"
@ -1046,6 +1119,92 @@ files = [
[package.dependencies]
setuptools = "*"
[[package]]
name = "pillow"
version = "10.3.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "platformdirs"
version = "4.2.0"
@ -1132,6 +1291,17 @@ files = [
[package.extras]
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "pyasn1"
version = "0.6.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
{file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
]
[[package]]
name = "pycparser"
version = "2.22"
@ -1557,31 +1727,43 @@ urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.3.6"
version = "0.3.7"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.3.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:732ef99984275534f9466fbc01121523caf72aa8c2bdeb36fd2edf2bc294a992"},
{file = "ruff-0.3.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:93699d61116807edc5ca1cdf9d2d22cf8d93335d59e3ff0ca7aee62c1818a736"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc4006cbc6c11fefc25f122d2eb4731d7a3d815dc74d67c54991cc3f99c90177"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878ef1a55ce931f3ca23b690b159cd0659f495a4c231a847b00ca55e4c688baf"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb87788284af96725643eae9ab3ac746d8cc09aad140268523b019f7ac3cd98"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b2e79f8e1b6bd5411d7ddad3f2abff3f9d371beda29daef86400d416dedb7e02"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf48ec2c4bfae7837dc325c431a2932dc23a1485e71c59591c1df471ba234e0e"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c466a52c522e6a08df0af018f550902f154f5649ad09e7f0d43da766e7399ebc"},
{file = "ruff-0.3.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ccf3fb6d1162a73cd286c63a5e4d885f46a1f99f0b392924bc95ccbd18ea8f"},
{file = "ruff-0.3.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b11e09439d9df6cc12d9f622065834654417c40216d271f639512d80e80e3e53"},
{file = "ruff-0.3.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:647f1fb5128a3e24ce68878b8050bb55044c45bb3f3ae4710d4da9ca96ede5cb"},
{file = "ruff-0.3.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2b0c4c70578ef1871a9ac5c85ed7a8c33470e976c73ba9211a111d2771b5f787"},
{file = "ruff-0.3.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e3da499ded004d0b956ab04248b2ae17e54a67ffc81353514ac583af5959a255"},
{file = "ruff-0.3.6-py3-none-win32.whl", hash = "sha256:4056480f5cf38ad278667c31b0ef334c29acdfcea617cb89c4ccbc7d96f1637f"},
{file = "ruff-0.3.6-py3-none-win_amd64.whl", hash = "sha256:f1aa621beed533f46e9c7d6fe00e7f6e4570155b61d8f020387b72ace2b42e04"},
{file = "ruff-0.3.6-py3-none-win_arm64.whl", hash = "sha256:7c8a2a0e0cab077a07465259ffe3b3c090e747ca8097c5dc4c36ca0fdaaac90d"},
{file = "ruff-0.3.6.tar.gz", hash = "sha256:26071fb530038602b984e3bbe1443ef82a38450c4dcb1344a9caf67234ff9756"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
]
[[package]]
@ -1669,6 +1851,24 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "tinycss2"
version = "1.2.1"
description = "A tiny CSS parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"},
{file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"},
]
[package.dependencies]
webencodings = ">=0.4"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["flake8", "isort", "pytest"]
[[package]]
name = "tomlkit"
version = "0.12.4"
@ -1767,6 +1967,17 @@ files = [
{file = "vulture-2.11.tar.gz", hash = "sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"},
]
[[package]]
name = "webencodings"
version = "0.5.1"
description = "Character encoding aliases for legacy web content"
optional = false
python-versions = "*"
files = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]
[[package]]
name = "win32-setctime"
version = "1.1.0"
@ -1967,3 +2178,4 @@ multidict = ">=4.0"
lock-version = "2.0"
python-versions = ">=3.12,<4"
content-hash = "95cd2b25b94d5eb7dd1dc10c590e3ad1b930c7e36775d6882146d84300a0a854"
content-hash = "060fffe973c2ae0e030b0e24362608fa73db573ad19c0d65cf4e1695ea49121a"

View file

@ -35,7 +35,7 @@ model Users {
// Returns a string that allows you to mention the given user.
mention String
// Specifies if the user is a bot account.
bot Boolean
bot Boolean @default(false)
// Returns the users creation time in UTC. This is when the users Discord account was created.
created_at DateTime?
// True if user is a member of a guild (not a discord.py attribute)
@ -45,7 +45,7 @@ model Users {
// An aware datetime object that specifies the date and time in UTC that the member joined the guild. If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be None.
joined_at DateTime?
// This is a relation field and is a list of roles that the user has, linking to the `UserRoles` table. If you fetch a user from the database and include this field, you will get all the roles associated with that user.
// This is a relation field and is a list of roles that the user has, linking to the `UserRoles` table. If you fetch a user from the database and include this field, you will get all the roles associated with that user.
roles UserRoles[]
// This represents all the infractions that this user has given out when acting as a moderator. It has a `relation` annotation to make clear that for these infractions, this user is referred to in the `moderator` field of the `Infractions` table.
@ -63,7 +63,8 @@ model Users {
notes_given Notes[] @relation("Moderator")
notes_received Notes[] @relation("User")
reminders Reminders[]
reminders Reminders[]
CommandStats CommandStats[]
}
model Roles {
@ -92,18 +93,13 @@ model Roles {
}
model UserRoles {
id BigInt @id @default(autoincrement())
// These refer to the `Users` model. `user_id` is the ID of a user from the `Users` table. The line `user Users @relation(fields: [user_id], references: [id])` implies that `user` establishes a relation with `Users` model based on the `user_id` in this `UserRoles` model and the `id` in the `Users` model.
user Users @relation(fields: [user_id], references: [id])
user_id BigInt
// These refer to the `Roles` model. `role_id` is the ID of a role from the `Roles` table. The line `role Roles @relation(fields: [role_id], references: [id])` implies that `role` establishes a relation with `Roles` model based on the `role_id` in this `UserRoles` model and the `id` in the `Roles` model.
role Roles @relation(fields: [role_id], references: [id])
role_id BigInt
// This specifies a composite unique constraint on `user_id` and `role_id`, meaning each combination of a user id and role id must be unique. It prevents a single user from having the same role multiple times. This constraint helps to maintain data integrity.
@@unique([user_id, role_id])
@@id([user_id, role_id])
}
model Infractions {
@ -160,6 +156,24 @@ model Reminders {
user_id BigInt
}
model Commands {
id BigInt @id @default(autoincrement())
name String
content String
created_at DateTime? @default(now())
CommandStats CommandStats[]
}
model CommandStats {
id BigInt @id @default(autoincrement())
command_id BigInt
user_id BigInt
used_at DateTime? @default(now())
command Commands @relation(fields: [command_id], references: [id])
user Users @relation(fields: [user_id], references: [id])
}
// model Logs {
// id BigInt @id
// content String

View file

@ -26,6 +26,10 @@ ruff = "^0.3.6"
sentry-sdk = "^1.45.0"
vulture = "^2.11"
pygithub = "^2.3.0"
httpx = "^0.27.0"
cairosvg = "^2.7.1"
pillow = "^10.3.0"
rsa = "^4.9"
[tool.poetry.scripts]
pyright = "pyright:run"

View file

@ -1,5 +1,6 @@
import asyncio
from collections.abc import Sequence
from collections.abc import Coroutine, Sequence
from typing import Any
import discord
from discord import app_commands
@ -8,6 +9,9 @@ from loguru import logger
from tux.database.controllers import DatabaseController
# TODO: Move to a constants file or set a global check/error handler for all commands to avoid repetition.
GUILD_ONLY_MESSAGE = "This command can only be used in a guild."
class Db(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
@ -18,14 +22,14 @@ class Db(commands.Cog):
@app_commands.checks.has_role("Root")
@group.command(name="members", description="Seeds the database with all members.")
async def seed_members(self, interaction: discord.Interaction):
async def seed_members(self, interaction: discord.Interaction) -> None:
await interaction.response.defer()
if interaction.guild is None:
await interaction.followup.send("This command can only be used in a guild.")
await interaction.followup.send(GUILD_ONLY_MESSAGE)
return
members = interaction.guild.members
members: Sequence[discord.Member] = interaction.guild.members
batch_size = 10
@ -53,11 +57,11 @@ class Db(commands.Cog):
@app_commands.checks.has_role("Root")
@group.command(name="roles", description="Seeds the database with all roles.")
async def seed_roles(self, interaction: discord.Interaction):
async def seed_roles(self, interaction: discord.Interaction) -> None:
await interaction.response.defer()
if interaction.guild is None:
await interaction.followup.send("This command can only be used in a guild.")
await interaction.followup.send(GUILD_ONLY_MESSAGE)
return
roles: Sequence[discord.Role] = interaction.guild.roles
@ -87,6 +91,40 @@ class Db(commands.Cog):
await interaction.followup.send("Seeded all roles.")
logger.info(f"{interaction.user} seeded all roles.")
@app_commands.checks.has_role("Root")
@group.command(name="user_roles", description="Seeds the database with all user roles.")
async def seed_user_roles(self, interaction: discord.Interaction) -> None:
await interaction.response.defer()
if interaction.guild is None:
await interaction.followup.send(GUILD_ONLY_MESSAGE)
return
members: Sequence[discord.Member] = interaction.guild.members
batch_size = 10
for i in range(0, len(members), batch_size):
batch = members[i : i + batch_size]
tasks: list[Coroutine[Any, Any, None]] = []
for member in batch:
role_ids: list[int] = [role.id for role in member.roles]
tasks.append(
self.db_controller.user_roles.sync_user_roles(
user_id=member.id, role_ids=role_ids
)
)
await asyncio.gather(*tasks)
logger.info(f"Processed batch starting with member {batch[0].display_name}")
await asyncio.sleep(1)
await interaction.followup.send("Seeded all user roles.")
logger.info(f"{interaction.user} seeded all user roles.")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Db(bot))

View file

@ -70,7 +70,8 @@ class ErrorHandler(commands.Cog):
"""Handle traditional command errors."""
if isinstance(
error,
commands.UnexpectedQuoteError
commands.CommandNotFound
| commands.UnexpectedQuoteError
| commands.InvalidEndOfQuotedStringError
| commands.CheckFailure,
):

View file

@ -20,6 +20,47 @@ class GuildLogging(commands.Cog):
"""Audit logging - Channel"""
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
# check if the message has no embeds, attachments, or content, stickers, or isnt a nitro gift/boost
# if so its probably a poll
poll_channel = self.bot.get_channel(1228717294788673656)
if message.channel == poll_channel:
# check if the message has content, stickers, or attachments
# if so delete the message as its not a poll
if message.content or message.stickers or message.attachments:
await message.delete()
embed = EmbedCreator.create_log_embed(
title="Non-Poll Deleted",
description=f"Message: {message.id}",
)
await self.send_to_audit_log(embed)
return
# make a thread for the poll
await message.create_thread(
name=f"Poll by {message.author.display_name}",
reason="Poll thread",
)
return
if (
not message.embeds
and not message.attachments
and not message.content
and not message.stickers
):
# check if the message is not a message
if message.type != discord.MessageType.default:
return
# delete the message and log it
await message.delete()
embed = EmbedCreator.create_log_embed(
title="Poll Deleted",
description=f"Message: {message.id}",
)
await self.send_to_audit_log(embed)
@commands.Cog.listener()
async def on_guild_channel_create(self, channel: discord.abc.GuildChannel):
embed = EmbedCreator.create_log_embed(

View file

@ -3,49 +3,80 @@ from discord import app_commands
from discord.ext import commands
from loguru import logger
from tux.database.controllers import DatabaseController
from tux.utils.embeds import EmbedCreator
from tux.utils.enums import InfractionType
class Ban(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.db_controller = DatabaseController().infractions
@app_commands.checks.has_any_role("Admin", "Sr. Mod", "Mod")
async def insert_infraction(
self,
user_id: int,
moderator_id: int,
infraction_type: InfractionType,
infraction_reason: str,
) -> None:
try:
await self.db_controller.create_infraction(
user_id=user_id,
moderator_id=moderator_id,
infraction_type=infraction_type,
infraction_reason=infraction_reason,
)
except Exception as error:
logger.error(f"Failed to create infraction. Error: {error}")
@app_commands.checks.has_any_role("Admin", "Sr. Mod", "Mod", "Jr. Mod")
@app_commands.command(name="ban", description="Bans a member from the server.")
@app_commands.describe(member="Which member to ban", reason="Reason for ban")
async def ban(
self, interaction: discord.Interaction, member: discord.Member, reason: str | None = None
) -> None:
logger.info(f"{interaction.user} banned {member.display_name} in {interaction.channel}")
response = await self.execute_ban(interaction, member, reason or "None provided")
await interaction.response.send_message(embed=response)
async def execute_ban(
self, interaction: discord.Interaction, member: discord.Member, reason: str | None = None
) -> discord.Embed:
try:
await member.ban(reason=reason)
embed = discord.Embed(
title=f"Banned {member.display_name}!",
color=discord.Colour.red(),
embed = EmbedCreator.create_infraction_embed(
title=f"{member.display_name} has been banned.",
description=f"Reason: `{reason}`",
)
embed.set_footer(
text=f"Banned by {interaction.user.display_name}",
icon_url=interaction.user.display_avatar.url,
interaction=interaction,
)
logger.info(f"Successfully banned {member.display_name}.")
embed.add_field(
name="Moderator",
value=f"{interaction.user.mention} ({interaction.user.id})",
inline=False,
)
except discord.errors.Forbidden as error:
embed = discord.Embed(
embed.add_field(
name="Member",
value=f"{member.mention} ({member.id})",
inline=False,
)
await self.insert_infraction(
user_id=member.id,
moderator_id=interaction.user.id,
infraction_type=InfractionType.BAN,
infraction_reason=reason or "None provided",
)
logger.info(f"Bannedd {member.display_name} for: {reason}")
except Exception as error:
embed = EmbedCreator.create_error_embed(
title=f"Failed to ban {member.display_name}",
color=discord.Colour.red(),
description=f"Insufficient permissions. Error Info: `{error}`",
description=f"Error Info: `{error}`",
interaction=interaction,
)
logger.error(f"Failed to ban {member.display_name}. Error: {error}")
return embed
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:

View file

@ -3,49 +3,90 @@ from discord import app_commands
from discord.ext import commands
from loguru import logger
from tux.database.controllers import DatabaseController
from tux.utils.embeds import EmbedCreator
from tux.utils.enums import InfractionType
class Kick(commands.Cog):
"""Cog for handling the kicking of members from a Discord server."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.db_controller = DatabaseController().infractions
async def insert_infraction(
self,
user_id: int,
moderator_id: int,
infraction_type: InfractionType,
infraction_reason: str,
) -> None:
"""Inserts an infraction record into the database."""
try:
await self.db_controller.create_infraction(
user_id=user_id,
moderator_id=moderator_id,
infraction_type=infraction_type,
infraction_reason=infraction_reason,
)
logger.info("Infraction recorded successfully.")
except Exception as error:
logger.error(f"Failed to create infraction. Error: {error}")
@app_commands.checks.has_any_role("Admin", "Sr. Mod", "Mod", "Jr. Mod")
@app_commands.command(name="kick", description="Kicks a member from the server.")
@app_commands.describe(member="Which member to kick", reason="Reason for kick")
@app_commands.describe(member="Member to kick", reason="Reason for the kick")
async def kick(
self, interaction: discord.Interaction, member: discord.Member, reason: str | None = None
) -> None:
logger.info(f"{interaction.user} kicked {member.display_name} in {interaction.channel}")
"""Kicks the specified member with an optional reason."""
if reason is None:
reason = "No reason provided"
response = await self.execute_kick(interaction, member, reason or "None provided")
await interaction.response.send_message(embed=response)
async def execute_kick(
self, interaction: discord.Interaction, member: discord.Member, reason: str | None = None
) -> discord.Embed:
try:
await member.kick(reason=reason)
embed = discord.Embed(
title=f"Kicked {member.display_name}!",
color=discord.Colour.gold(),
description=f"Reason: `{reason}`",
)
embed.set_footer(
text=f"Kicked by {interaction.user.display_name}",
icon_url=interaction.user.display_avatar.url,
)
await self.log_kick(interaction, member, reason)
except Exception as error:
await self.handle_kick_error(interaction, member, error)
logger.info(f"Successfully kicked {member.display_name}.")
async def log_kick(
self, interaction: discord.Interaction, member: discord.Member, reason: str
) -> None:
"""Sends a log message and informs about the kick operation."""
embed = EmbedCreator.create_infraction_embed(
title=f"{member.display_name} has been kicked.",
description=f"Reason: `{reason}`",
interaction=interaction,
)
embed.add_field(
name="Moderator",
value=f"{interaction.user.mention} ({interaction.user.id})",
inline=False,
)
embed.add_field(name="Member", value=f"{member.mention} ({member.id})", inline=False)
except discord.errors.Forbidden as error:
embed = discord.Embed(
title=f"Failed to kick {member.display_name}",
color=discord.Colour.red(),
description=f"Insufficient permissions. Error Info: `{error}`",
)
logger.error(f"Failed to kick {member.display_name}. Error: {error}")
await self.insert_infraction(
user_id=member.id,
moderator_id=interaction.user.id,
infraction_type=InfractionType.KICK,
infraction_reason=reason,
)
logger.info(f"Kicked {member.display_name} for: {reason}")
await interaction.response.send_message(embed=embed)
return embed
async def handle_kick_error(
self, interaction: discord.Interaction, member: discord.Member, error: Exception
) -> None:
"""Handles errors that occur during the kick operation."""
error_msg = f"Failed to kick {member.display_name}. Error: {error}"
logger.error(error_msg)
embed = EmbedCreator.create_error_embed(
title=f"Failed to kick {member.display_name}",
description=f"Error Info: `{error}`",
interaction=interaction,
)
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:

View file

@ -0,0 +1,68 @@
import discord
from discord import app_commands
from discord.ext import commands
from tux.utils.constants import Constants as CONST
from tux.utils.embeds import EmbedCreator
class ConfirmModal(discord.ui.Modal):
def __init__(self, *, title: str = "Submit an anonymous report", bot: commands.Bot) -> None:
super().__init__(title=title)
self.bot = bot
self.channel = CONST.LOG_CHANNELS["REPORT"]
short = discord.ui.TextInput( # type: ignore
style=discord.TextStyle.short,
label="Related user(s) or issue(s)",
required=True,
max_length=100,
placeholder="User IDs, usernames, or brief description",
)
long = discord.ui.TextInput( # type: ignore
style=discord.TextStyle.long,
label="Your report",
required=True,
max_length=4000,
placeholder="Please provide as much detail as possible",
)
async def on_submit(self, interaction: discord.Interaction) -> None:
embed = EmbedCreator.create_log_embed(
title=(f"Anonymous report for {self.short.value}"), # type: ignore
description=self.long.value, # type: ignore
)
channel = self.bot.get_channel(self.channel) or await self.bot.fetch_channel(self.channel)
webhook: discord.Webhook | None = None
if isinstance(channel, discord.TextChannel):
webhook = await channel.create_webhook(
name="Tux",
reason="Anonymous report webhook",
)
if webhook:
await webhook.send(embed=embed)
await webhook.delete(reason="Report sent")
await interaction.response.send_message(
"The report has been sent to the moderation team. Thank you for your help!",
ephemeral=True,
)
class Report(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="report", description="Report a user or issue anonymously")
async def report(self, interaction: discord.Interaction) -> None:
modal = ConfirmModal(bot=self.bot)
await interaction.response.send_modal(modal)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Report(bot))

View file

@ -0,0 +1,76 @@
import datetime
import discord
from discord import app_commands
from discord.ext import commands
from loguru import logger
class TimeOut(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="timeout", description="Timeout a user")
@app_commands.describe(
member="Which member to timeout",
days="Days of timeout",
hours="Hours of timeout",
minutes="Minutes of timeout",
seconds="Seconds of timeout",
reason="Reason to timeout member",
)
async def timeout(
self,
interaction: discord.Interaction,
member: discord.Member,
days: int = 0,
hours: int = 0,
minutes: int = 0,
seconds: int = 0,
reason: str | None = None,
) -> None:
logger.info(
f"{interaction.user} used the timeout command to timeout {member}",
)
duration = datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
try:
await member.timeout(duration, reason=reason)
embed = discord.Embed(
color=discord.Color.red(),
title=f"User {member.display_name} timed out",
description=f"Reason: {reason if reason else '`None provided`'}",
timestamp=interaction.created_at,
)
embed.add_field(
name="User",
value=f"<@{member.id}>",
inline=True,
)
embed.add_field(
name="Duration",
value=duration,
inline=True,
)
embed.set_footer(
text=f"Requested by {interaction.user.display_name}",
icon_url=interaction.user.display_avatar,
)
await interaction.response.send_message(embed=embed)
except (discord.errors.Forbidden, discord.errors.HTTPException) as e:
logger.error("")
embed_error = discord.Embed(
colour=discord.Colour.red(),
title=f"Failed to timeout {member.display_name}",
description=f"`Error info: {e}`",
timestamp=interaction.created_at,
)
embed_error.set_footer(
text=f"Requested by {interaction.user.display_name}",
icon_url=interaction.user.display_avatar,
)
await interaction.response.send_message(embed=embed_error)
return
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(TimeOut(bot))

View file

@ -3,64 +3,84 @@ from discord import app_commands
from discord.ext import commands
from loguru import logger
from prisma.models import Infractions
from tux.database.controllers import DatabaseController
from tux.utils.embeds import EmbedCreator
from tux.utils.enums import InfractionType
class Warn(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.db_controller = DatabaseController().infractions
async def insert_infraction(
self,
user_id: int,
moderator_id: int,
infraction_type: InfractionType,
infraction_reason: str,
) -> Infractions | None:
try:
return await self.db_controller.create_infraction(
user_id=user_id,
moderator_id=moderator_id,
infraction_type=infraction_type,
infraction_reason=infraction_reason,
)
except Exception as error:
logger.error(f"Failed to create infraction. Error: {error}")
@app_commands.checks.has_any_role("Admin", "Sr. Mod", "Mod", "Jr. Mod")
@app_commands.command(name="warn", description="Warns a member in the server for a reason.")
@app_commands.command(name="warn", description="Warns a member from the server.")
@app_commands.describe(member="Which member to warn", reason="Reason for warn")
async def warn(
self,
interaction: discord.Interaction,
member: discord.Member,
reason: str | None = "No reason",
self, interaction: discord.Interaction, member: discord.Member, reason: str | None = None
) -> None:
logger.info(f"{interaction.user} just warned user {member} for {reason}")
moderator: discord.Member | discord.User = interaction.user
if interaction.guild:
moderator = await interaction.guild.fetch_member(interaction.user.id)
response = await self.execute_warn(
interaction, moderator, member, reason or "None provided"
)
await interaction.response.send_message(embed=response)
async def execute_warn(
self,
interaction: discord.Interaction,
moderator: discord.Member | discord.User,
member: discord.Member,
reason: str,
) -> discord.Embed:
try:
# TODO Replace this with function that creates a warn
# await add_infraction(moderator.id, member.id, "warn", reason, datetime.now())
embed = discord.Embed(
title=f"Warned {member.display_name}!",
color=discord.Colour.gold(),
description=f"Reason: `{reason}`",
timestamp=interaction.created_at,
new_warn: Infractions | None = await self.insert_infraction(
user_id=member.id,
moderator_id=interaction.user.id,
infraction_type=InfractionType.WARN,
infraction_reason=reason or "None provided",
)
embed.set_footer(
text=f"Warned by {moderator.display_name}",
icon_url=moderator.display_avatar.url,
warn_id = new_warn.id if new_warn is not None else "Unknown"
embed = EmbedCreator.create_infraction_embed(
title=f"WARNING — ID: {warn_id}", interaction=interaction, description=""
)
embed.add_field(
name="Reason",
value=f"`{reason}`" if reason else "No reason provided",
inline=False,
)
embed.add_field(
name="By",
value=f"{interaction.user.mention} `({interaction.user.id})`",
inline=True,
)
embed.add_field(
name="To",
value=f"{member.mention} `({member.id})`",
inline=True,
)
logger.info(f"Warned {member.display_name} for: {reason}")
except Exception as error:
embed = discord.Embed(
embed = EmbedCreator.create_error_embed(
title=f"Failed to warn {member.display_name}",
color=discord.Colour.red(),
description=f"Unknown error. Error Info: `{error}`",
timestamp=interaction.created_at,
description=f"Error Info: `{error}`",
interaction=interaction,
)
logger.error(f"Failed to warn {member.display_name}. Error: {error}")
return embed
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:

View file

@ -1,38 +0,0 @@
import traceback
import discord
from discord import app_commands
from discord.ext import commands
class ReportModal(discord.ui.Modal, title="Report"):
report = discord.ui.TextInput( # type: ignore
label="Submit your anonymous report",
style=discord.TextStyle.long,
placeholder="Type your feedback here...",
required=True,
max_length=300,
)
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.send_message(
"Thanks for your report and helping keep our community safe!",
ephemeral=True,
)
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True)
traceback.print_exception(type(error), error, error.__traceback__)
class Report(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="report", description="Make an anonymous report")
async def report(self, interaction: discord.Interaction) -> None:
await interaction.response.send_modal(ReportModal())
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Report(bot))

View file

@ -29,7 +29,7 @@ class Tldr(commands.Cog):
async def tldr(self, interaction: discord.Interaction, command: str) -> None:
logger.info(f"{interaction.user} used the /tldr to show info about {command}")
tldr_page = self.get_tldr_page(command)
embed = EmbedCreator.create_default_embed(
embed = EmbedCreator.create_info_embed(
title=f"TLDR for {command}", description=tldr_page, interaction=interaction
)
await interaction.response.send_message(embed=embed)

178
tux/cogs/utility/tools.py Normal file
View file

@ -0,0 +1,178 @@
import io
from base64 import b64decode, b64encode
from typing import Any, cast
import cairosvg # type: ignore
import discord
import httpx
from discord import app_commands
from discord.ext import commands
from tux.utils.embeds import EmbedCreator
client = httpx.AsyncClient()
COLOR_FORMATS = {"HEX": "hex", "RGB": "rgb", "HSL": "hsl", "CMYK": "cmyk"}
class Tools(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.encodings = {
"base64": self.encode_base64,
# "md5": self.encode_md5,
# "sha256": self.encode_sha256,
# "sha512": self.encode_sha512,
}
self.decodings = {
"base64": self.decode_base64,
# "md5": self.decode_md5,
# "sha256": self.decode_sha256,
# "sha512": self.decode_sha512,
}
def encode_base64(self, input_string: str):
return b64encode(input_string.encode()).decode()
def decode_base64(self, input_string: str):
return b64decode(input_string.encode()).decode()
# def encode_md5(self, input_string: str):
# return hashlib.md5(input_string.encode()).hexdigest()
# def encode_sha256(self, input_string: str):
# return hashlib.sha256(input_string.encode()).hexdigest()
# def encode_sha512(self, input_string: str):
# return hashlib.sha512(input_string.encode()).hexdigest()
group = app_commands.Group(name="tools", description="Various tool commands.")
@group.command(name="colors", description="Converts a color to different formats.")
@app_commands.describe(color_format="Original color format to convert from")
@app_commands.choices(
color_format=[
app_commands.Choice[str](name=color_format, value=value)
for color_format, value in COLOR_FORMATS.items()
]
)
async def colors(
self,
interaction: discord.Interaction,
color_format: discord.app_commands.Choice[str],
color: str,
) -> None:
if color_format.value == "HEX" and color.startswith("#"):
color = color[1:]
api = f"https://www.thecolorapi.com/id?format=json&{color_format.value}={color}"
data: Any = await self.make_request(api)
content: bytes = await self.get_svg_content(data["image"]["named"])
png_bio: io.BytesIO = self.convert_svg_to_png(content)
embed = self.construct_embed(interaction, data)
await self.send_message(interaction, embed, png_bio)
async def make_request(self, api: str) -> Any:
return (await client.get(api)).json()
async def get_svg_content(self, svg_url: str) -> bytes:
return (await client.get(svg_url)).content
def convert_svg_to_png(self, content: bytes) -> io.BytesIO:
# Attempt conversion from SVG to PNG
png_content = cairosvg.svg2png(bytestring=content, dpi=96, scale=1, unsafe=False) # type: ignore
# Ensure the output is bytes; use cast to reassure type checkers
png_content = cast(bytes | None, png_content)
if png_content is None:
msg = "Failed to convert SVG to PNG"
raise ValueError(msg)
# Create BytesIO stream from the PNG content bytes
png_bio = io.BytesIO(png_content)
png_bio.seek(0)
return png_bio
def construct_embed(self, interaction: discord.Interaction, data: Any) -> discord.Embed:
embed = EmbedCreator.create_info_embed(
title="Color Converter",
description="Here is your color converted!",
interaction=interaction,
)
for color_format, value in COLOR_FORMATS.items():
embed.add_field(name=color_format, value=data[value]["value"])
embed.add_field(name="HSV", value=data["hsv"]["value"])
embed.add_field(name="XYZ", value=data["XYZ"]["value"])
embed.set_thumbnail(url="attachment://color.png")
return embed
async def send_message(
self, interaction: discord.Interaction, embed: discord.Embed, png_bio: io.BytesIO
) -> None:
await interaction.response.send_message(
embed=embed, file=discord.File(png_bio, "color.png")
)
@group.command(name="encode", description="Encodes a string to a specified format.")
@app_commands.describe(encoding="The encoding format to use", string="The string to encode")
@app_commands.choices(
encoding=[
app_commands.Choice[str](name="base64", value="base64"),
# app_commands.Choice[str](name="md5", value="md5"),
# app_commands.Choice[str](name="sha256", value="sha256"),
# app_commands.Choice[str](name="sha512", value="sha512"),
]
)
async def encode(
self,
interaction: discord.Interaction,
encoding: app_commands.Choice[str],
string: str,
) -> None:
title = f"{encoding.name.capitalize()} Encode"
try:
encode_func = self.encodings[encoding.value]
encoded_string = encode_func(string)
description = f"Encoded: {encoded_string}"
except KeyError:
description = "Invalid encoding selected!"
embed = EmbedCreator.create_info_embed(
title=title, description=description, interaction=interaction
)
await interaction.response.send_message(embed=embed)
@group.command(name="decode", description="Decodes a string from a specified format.")
@app_commands.describe(encoding="The decoding format to use", string="The string to decode")
@app_commands.choices(
encoding=[
app_commands.Choice[str](name="base64", value="base64"),
]
)
async def decode(
self,
interaction: discord.Interaction,
encoding: app_commands.Choice[str],
string: str,
) -> None:
title = f"{encoding.name.capitalize()} Decode"
try:
decode_func = self.decodings[encoding.value]
decoded_string = decode_func(string)
description = f"Decoded: {decoded_string}"
except KeyError:
description = "Invalid decoding selected!"
embed = EmbedCreator.create_info_embed(
title=title, description=description, interaction=interaction
)
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Tools(bot))

View file

@ -3,6 +3,7 @@ from .notes import NotesController
from .reminders import RemindersController
from .roles import RolesController
from .snippets import SnippetsController
from .user_roles import UserRolesController
from .users import UsersController
@ -14,3 +15,4 @@ class DatabaseController:
self.snippets = SnippetsController()
self.reminders = RemindersController()
self.roles = RolesController()
self.user_roles = UserRolesController()

View file

@ -1,14 +1,6 @@
from enum import Enum
from prisma.models import Infractions
from tux.database.client import db
class InfractionType(Enum):
BAN = "ban"
WARN = "warn"
KICK = "kick"
TIMEOUT = "timeout"
from tux.utils.enums import InfractionType
class InfractionsController:

View file

@ -0,0 +1,89 @@
from prisma.models import UserRoles
from tux.database.client import db
class UserRolesController:
def __init__(self) -> None:
self.table = db.userroles
async def get_all_user_roles(self) -> list[UserRoles]:
"""
Retrieves all user roles from the database.
Returns:
list[UserRoles]: A list of all user roles.
"""
return await self.table.find_many()
async def get_user_role_by_ids(self, user_id: int, role_id: int) -> UserRoles | None:
"""
Retrieves a user role from the database based on the specified user ID and role ID.
Args:
user_id (int): The ID of the user.
role_id (int): The ID of the role.
Returns:
UserRoles | None: The user role if found, None if the user role does not exist.
"""
return await self.table.find_first(where={"user_id": user_id, "role_id": role_id})
async def create_user_role(self, user_id: int, role_id: int) -> UserRoles:
"""
Creates a new user role with the specified user ID and role ID.
Args:
user_id (int), role_id (int)
Returns:
UserRoles: The newly created user role.
"""
return await self.table.create(data={"user_id": user_id, "role_id": role_id})
async def delete_user_role(self, user_id: int, role_id: int) -> None:
"""
Deletes a user role based on specified user ID and role ID.
"""
await self.table.delete(where={"user_id_role_id": {"user_id": user_id, "role_id": role_id}})
async def delete_user_roles(self, user_id: int) -> None:
"""
Deletes all user roles from the database based on the specified user ID.
"""
await self.table.delete_many(where={"user_id": user_id})
async def delete_role_users(self, role_id: int) -> None:
"""
Deletes all user roles from the database based on the specified role ID.
"""
await self.table.delete_many(where={"role_id": role_id})
async def delete_all_user_roles(self) -> None:
"""
Deletes all user roles from the database.
"""
await self.table.delete_many()
async def get_user_roles_by_user_id(self, user_id: int) -> list[UserRoles]:
"""
Retrieves all user roles from the database based on the specified user ID.
"""
return await self.table.find_many(where={"user_id": user_id})
async def get_user_roles_by_role_id(self, role_id: int) -> list[UserRoles]:
"""
Retrieves all user roles from the database based on the specified role ID.
"""
return await self.table.find_many(where={"role_id": role_id})
async def sync_user_roles(self, user_id: int, role_ids: list[int]) -> None:
"""
Synchronizes user roles in the database based on the specified user ID and role IDs.
"""
user_roles = await self.get_user_roles_by_user_id(user_id)
user_role_ids = [user_role.role_id for user_role in user_roles]
for role_id in role_ids:
if role_id not in user_role_ids:
await self.create_user_role(user_id=user_id, role_id=role_id)
for user_role in user_roles:
if user_role.role_id not in role_ids:
await self.delete_user_role(user_id=user_id, role_id=user_role.role_id)

View file

@ -13,17 +13,54 @@ class ActivityChanger:
return [
discord.Activity(
type=discord.ActivityType.watching, name=f"{self.get_member_count()} members"
),
discord.Streaming(name="fortnite gamer hourz", url="https://twitch.tv/urmom"),
discord.Activity(type=discord.ActivityType.watching, name="All Things Linux"),
discord.Activity(type=discord.ActivityType.playing, name="with fire"),
discord.Activity(type=discord.ActivityType.watching, name="linux tech tips"),
discord.Activity(type=discord.ActivityType.listening, name="mpd"),
discord.Activity(type=discord.ActivityType.watching, name="a vast field of grain"),
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.watching, name="All Things Linux"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.playing, name="with fire"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.watching, name="linux tech tips"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.listening, name="mpd"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.watching, name="a vast field of grain"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.playing,
name="i am calling about your car's extended warranty",
),
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.playing, name="SuperTuxKart"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.playing, name="supertux2"
), # submitted by lilliana
discord.Activity(
type=discord.ActivityType.watching, name="Linux install"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.watching, name="Brodie Robertson"
), # submitted by electron271
discord.Streaming(
name="SuperTuxKart", url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.listening, name="Terry Davis on YouTube"
), # submitted by kaizen
discord.Activity(
type=discord.ActivityType.playing, name="with Puffy"
), # submitted by kaizen
discord.Activity(
type=discord.ActivityType.watching, name="the stars"
), # submitted by electron271
discord.Activity(
type=discord.ActivityType.playing,
name="To see who submitted these, check tux/utils/activities.py on the repo (/info tux)",
), # submitted by electron271
]
def get_member_count(self):

View file

@ -34,8 +34,12 @@ class Constants:
# Channel constants
LOG_CHANNELS: Final[dict[str, int]] = {
# For general logging
"AUDIT": 1223690612822376529,
# For infractions, mod actions, etc.
"MOD": 1223690612822376529,
# For anonymous reports
"REPORT": 1223690612822376529,
}
# User ID Constants
@ -73,6 +77,8 @@ class Constants:
"WHITE": 0xFFFFFF,
# catppuccin yellow
"POLL": 0xF9E2AF,
# catppuccin crust
"INFRACTION": 0x11111B,
}
EMBED_STATE_ICONS: Final[dict[str, str]] = {
@ -83,6 +89,7 @@ class Constants:
"ERROR": "https://github.com/catppuccin/catppuccin/blob/main/assets/palette/circles/latte_red.png?raw=true",
"SUCCESS": "https://github.com/catppuccin/catppuccin/blob/main/assets/palette/circles/mocha_green.png?raw=true",
"POLL": "https://github.com/catppuccin/catppuccin/raw/main/assets/palette/circles/mocha_yellow.png?raw=true",
"INFRACTION": "https://github.com/catppuccin/catppuccin/raw/main/assets/palette/circles/mocha_crust.png?raw=true",
}
EMBED_SPECIAL_CHARS: Final[dict[str, str]] = {

View file

@ -22,24 +22,23 @@ class EmbedCreator:
ctx: commands.Context[commands.Bot] | None, interaction: discord.Interaction | None
) -> tuple[str, str | None]:
user: discord.User | discord.Member | None = None
latency = None
if ctx:
user = ctx.author
latency = round(ctx.bot.latency * 1000, 2)
elif interaction:
user = interaction.user
latency = round(interaction.client.latency * 1000, 2)
if isinstance(user, discord.User | discord.Member):
return (
f"Requested by {user.display_name}",
f"{user.name}@atl $ tux {latency}ms", # noqa: RUF001
str(user.avatar.url) if user.avatar else None,
)
return ("", None)
# @staticmethod
# def shell_terminal_format(user: str) -> str:
# return f"[{user}@tux ~]$"
@staticmethod
def add_field(embed: discord.Embed, name: str, value: str, inline: bool = True) -> None:
embed.add_field(name=name, value=value, inline=inline)
@ -63,7 +62,7 @@ class EmbedCreator:
embed = discord.Embed()
embed.color = CONST.EMBED_STATE_COLORS[state]
embed.color = discord.Colour(CONST.EMBED_STATE_COLORS[state])
embed.set_author(
name=state.capitalize() if state else "Info",
@ -85,7 +84,7 @@ class EmbedCreator:
interaction: discord.Interaction | None,
state: str,
title: str,
description: str,
description: str = "",
) -> discord.Embed:
embed = cls.base_embed(ctx, interaction, state)
embed.title = title
@ -162,3 +161,13 @@ class EmbedCreator:
interaction: discord.Interaction | None = None,
) -> discord.Embed:
return cls.create_embed(ctx, interaction, "LOG", title, description)
@classmethod
def create_infraction_embed(
cls,
title: str,
description: str,
ctx: commands.Context[commands.Bot] | None = None,
interaction: discord.Interaction | None = None,
) -> discord.Embed:
return cls.create_embed(ctx, interaction, "INFRACTION", title, description)

8
tux/utils/enums.py Normal file
View file

@ -0,0 +1,8 @@
from enum import Enum
class InfractionType(Enum):
BAN = "ban"
WARN = "warn"
KICK = "kick"
TIMEOUT = "timeout"