Compare commits
82
Commits
rewrite-nyeki
...
v4.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6843865da3 | ||
|
|
f882ed7fed | ||
|
|
1149dd5bb4 | ||
|
|
54217f1d69 | ||
|
|
8ff7e3af02 | ||
|
|
040b720755 | ||
|
|
91ae88d040 | ||
|
|
31bb63a77a | ||
|
|
2f99b32aea | ||
|
|
d3e192e3c3 | ||
|
|
f98fc37191 | ||
|
|
fcb1a786be | ||
|
|
5fe3878471 | ||
|
|
d53a161716 | ||
|
|
c0b919d33c | ||
|
|
a852cd9844 | ||
|
|
36f15615b6 | ||
|
|
f909a300ad | ||
|
|
da1971060a | ||
|
|
e1c8957fc8 | ||
|
|
43ab107102 | ||
|
|
14f8df2aba | ||
|
|
a67b4f6a3d | ||
|
|
455dc27743 | ||
|
|
d39b06eb12 | ||
|
|
8eaa3f6195 | ||
|
|
f6866e1a11 | ||
|
|
700b770248 | ||
|
|
ac81e3b286 | ||
|
|
323cf7f748 | ||
|
|
281542a5c2 | ||
|
|
77182d9633 | ||
|
|
e472936c51 | ||
|
|
12948dcc15 | ||
|
|
b9410bedd7 | ||
|
|
7eb2a99b69 | ||
|
|
fb7eba11bb | ||
|
|
208e0ddbf5 | ||
|
|
deb76941ea | ||
|
|
25b96cdc2a | ||
|
|
f4f842e3c8 | ||
|
|
6dde25aa72 | ||
|
|
8e0c591b21 | ||
|
|
fe53f2e5c2 | ||
|
|
3c87fdfa69 | ||
|
|
da10b7f194 | ||
|
|
53d61cfbbb | ||
|
|
eb6dcb4604 | ||
|
|
8432cb78fa | ||
|
|
faf9d9d580 | ||
|
|
f4a282447a | ||
|
|
2b4c3d0b8b | ||
|
|
1a446ae3bf | ||
|
|
55431d580e | ||
|
|
09ed3b870e | ||
|
|
634c8eff8c | ||
|
|
8173a508ad | ||
|
|
c284ede5b4 | ||
|
|
7b3fa9905a | ||
|
|
a14ae96a47 | ||
|
|
e35b200d03 | ||
|
|
285f1d4516 | ||
|
|
2030812742 | ||
|
|
e407068a9c | ||
|
|
7270c90b62 | ||
|
|
7119730668 | ||
|
|
fb660c162b | ||
|
|
6ba65eef71 | ||
|
|
d7b8138606 | ||
|
|
66584a8239 | ||
|
|
80ed00f796 | ||
|
|
fd71345cd0 | ||
|
|
7ed2c10448 | ||
|
|
717de3eec5 | ||
|
|
09b875da97 | ||
|
|
0cdbe63c55 | ||
|
|
ed717ef94b | ||
|
|
5806493ca8 | ||
|
|
39c455f617 | ||
|
|
d04af236d2 | ||
|
|
e8a76d5e77 | ||
|
|
91d5bf73c5 |
@@ -0,0 +1,19 @@
|
||||
/target
|
||||
/.venv
|
||||
/.ansible
|
||||
/.vscode
|
||||
/k8s
|
||||
/playbooks
|
||||
/backup
|
||||
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.python-version
|
||||
|
||||
ansible.cfg
|
||||
compose.yml
|
||||
inventory.ini
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
|
||||
.env
|
||||
@@ -1,3 +1,10 @@
|
||||
/target
|
||||
/.venv
|
||||
/.ansible
|
||||
/backup
|
||||
|
||||
.env
|
||||
|
||||
00-vault-pwd.txt
|
||||
|
||||
reset-ssh.sh
|
||||
@@ -0,0 +1,38 @@
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
image: rust:latest
|
||||
script:
|
||||
- rustup component add rustfmt clippy
|
||||
- cargo fmt --check
|
||||
- cargo clippy -- -D warnings
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: rust:latest
|
||||
script:
|
||||
- cargo build --release
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: rust:latest
|
||||
script:
|
||||
- cargo test --release
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"ansible.python.interpreterPath": "/home/nyeki/Documents/Repositories/payphone/payphone/.venv/bin/python"
|
||||
}
|
||||
Generated
+315
-1
@@ -363,7 +363,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -397,6 +401,27 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@@ -446,6 +471,36 @@ dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-redis"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0965b977f1244bc3783bb27cd79cfcff335a8341da18f79232d00504b18eb1a"
|
||||
dependencies = [
|
||||
"deadpool",
|
||||
"redis",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
@@ -455,6 +510,17 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -705,6 +771,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
@@ -840,12 +912,119 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
@@ -1024,6 +1203,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
@@ -1125,12 +1310,31 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1140,6 +1344,16 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
@@ -1217,14 +1431,17 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "payphone"
|
||||
version = "4.0.0"
|
||||
version = "4.0.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"clap",
|
||||
"colored",
|
||||
"csv",
|
||||
"deadpool-redis",
|
||||
"dotenvy",
|
||||
"include_dir",
|
||||
"itertools",
|
||||
"linkify",
|
||||
"log",
|
||||
"notify",
|
||||
@@ -1235,6 +1452,7 @@ dependencies = [
|
||||
"regex",
|
||||
"scc",
|
||||
"scylla",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
@@ -1272,6 +1490,15 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@@ -1349,6 +1576,27 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.32.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"futures-util",
|
||||
"itoa",
|
||||
"num-bigint",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"ryu",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.17"
|
||||
@@ -1918,6 +2166,16 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
@@ -2219,6 +2477,23 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -2669,6 +2944,12 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
@@ -2739,3 +3020,36 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
+6
-1
@@ -1,15 +1,19 @@
|
||||
[package]
|
||||
name = "payphone"
|
||||
version = "4.0.0"
|
||||
version = "4.0.8"
|
||||
edition = "2024"
|
||||
default-run = "payphone"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.88"
|
||||
axum = "0.8.4"
|
||||
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
||||
colored = "3.0.0"
|
||||
csv = "1.3.1"
|
||||
deadpool-redis = "0.22.0"
|
||||
dotenvy = "0.15.7"
|
||||
include_dir = "0.7.4"
|
||||
itertools = "0.14.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.27", features = ["serde"] }
|
||||
notify = "8.2.0"
|
||||
@@ -20,6 +24,7 @@ rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||
regex = "1.11.1"
|
||||
scc = "2.3.4"
|
||||
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
FROM rust:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm
|
||||
|
||||
COPY --from=builder /app/target/release/payphone /usr/local/bin/payphone
|
||||
COPY --from=builder /app/target/release/migrate /usr/local/bin/migrate
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
RUN update-ca-certificates
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/payphone" ]
|
||||
CMD [ "start" ]
|
||||
@@ -0,0 +1,5 @@
|
||||
[ssh_connection]
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
||||
|
||||
[defaults]
|
||||
timeout = 30
|
||||
@@ -0,0 +1,16 @@
|
||||
[init]
|
||||
p1 ansible_host=p1.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||
|
||||
[master]
|
||||
p2 ansible_host=p2.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||
p3 ansible_host=p3.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||
|
||||
[worker]
|
||||
p4 ansible_host=p4.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||
p5 ansible_host=p5.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||
p6 ansible_host=p6.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||
|
||||
[scylla]
|
||||
p4
|
||||
p5
|
||||
p6
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
apiVersion: scylla.scylladb.com/v1alpha1
|
||||
kind: NodeConfig
|
||||
metadata:
|
||||
name: scylladb-nodepool-1
|
||||
spec:
|
||||
localDiskSetup:
|
||||
loopDevices:
|
||||
- name: persistent-volumes
|
||||
imagePath: /var/lib/persistent-volumes.img
|
||||
size: 60Gi
|
||||
filesystems:
|
||||
- device: /dev/loops/persistent-volumes
|
||||
type: xfs
|
||||
mounts:
|
||||
- device: /dev/loops/persistent-volumes
|
||||
mountPoint: /var/lib/persistent-volumes
|
||||
unsupportedOptions:
|
||||
- prjquota
|
||||
placement:
|
||||
nodeSelector:
|
||||
scylla.scylladb.com/node-type: scylla
|
||||
tolerations:
|
||||
- effect: NoSchedule
|
||||
key: scylla-operator.scylladb.com/dedicated
|
||||
operator: Equal
|
||||
value: scyllaclusters
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: scylladb:csi-external-provisioner
|
||||
aggregationRule:
|
||||
clusterRoleSelectors:
|
||||
- matchLabels:
|
||||
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||
@@ -0,0 +1,102 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: scylladb:aggregate-to-csi-external-provisioner
|
||||
labels:
|
||||
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "persistentvolumes"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- "create"
|
||||
- "delete"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "persistentvolumeclaims"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- "update"
|
||||
- apiGroups:
|
||||
- "storage.k8s.io"
|
||||
resources:
|
||||
- "storageclasses"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "events"
|
||||
verbs:
|
||||
- "list"
|
||||
- "watch"
|
||||
- "create"
|
||||
- "update"
|
||||
- "patch"
|
||||
- apiGroups:
|
||||
- "snapshot.storage.k8s.io"
|
||||
resources:
|
||||
- "volumesnapshots"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- apiGroups:
|
||||
- "snapshot.storage.k8s.io"
|
||||
resources:
|
||||
- "volumesnapshotcontents"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- apiGroups:
|
||||
- "storage.k8s.io"
|
||||
resources:
|
||||
- "csinodes"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "nodes"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- apiGroups:
|
||||
- "storage.k8s.io"
|
||||
resources:
|
||||
- "csistoragecapacities"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "watch"
|
||||
- "create"
|
||||
- "update"
|
||||
- "patch"
|
||||
- "delete"
|
||||
# The GET permissions below are needed for walking up the ownership chain
|
||||
# for CSIStorageCapacity. They are sufficient for deployment via
|
||||
# StatefulSet (only needs to get Pod) and Deployment (needs to get
|
||||
# Pod and then ReplicaSet to find the Deployment).
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "pods"
|
||||
verbs:
|
||||
- "get"
|
||||
- apiGroups:
|
||||
- "apps"
|
||||
resources:
|
||||
- "replicasets"
|
||||
verbs:
|
||||
- "get"
|
||||
@@ -0,0 +1,15 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: scylladb:aggregate-to-csi-external-provisioner-openshift
|
||||
labels:
|
||||
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||
rules:
|
||||
- apiGroups:
|
||||
- security.openshift.io
|
||||
resourceNames:
|
||||
- privileged
|
||||
resources:
|
||||
- securitycontextconstraints
|
||||
verbs:
|
||||
- use
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: local-csi-driver
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: StorageClass
|
||||
apiVersion: storage.k8s.io/v1
|
||||
metadata:
|
||||
name: scylladb-local-xfs
|
||||
provisioner: local.csi.scylladb.com
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
parameters:
|
||||
csi.storage.k8s.io/fstype: xfs
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: CSIDriver
|
||||
metadata:
|
||||
name: local.csi.scylladb.com
|
||||
spec:
|
||||
attachRequired: false
|
||||
storageCapacity: true
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
namespace: local-csi-driver
|
||||
name: local-csi-driver
|
||||
@@ -0,0 +1,12 @@
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: scylladb:csi-external-provisioner
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: local-csi-driver
|
||||
namespace: local-csi-driver
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: scylladb:csi-external-provisioner
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -0,0 +1,125 @@
|
||||
kind: DaemonSet
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
namespace: local-csi-driver
|
||||
name: local-csi-driver
|
||||
labels:
|
||||
app.kubernetes.io/name: local-csi-driver
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: local-csi-driver
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: local-csi-driver
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
scylla.scylladb.com/node-type: scylla
|
||||
serviceAccountName: local-csi-driver
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
containers:
|
||||
- name: local-csi-driver
|
||||
securityContext:
|
||||
privileged: true
|
||||
image: docker.io/scylladb/local-csi-driver:0.5.0@sha256:46c1d2e9be8edf076d8dea7e82a3bb763d4bdda5adf43c794f617e9c94251079
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --listen=/csi/csi.sock
|
||||
- --node-name=$(NODE_NAME)
|
||||
- --volumes-dir=/var/lib/persistent-volumes
|
||||
- --v=2
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
volumeMounts:
|
||||
- name: kubelet-dir
|
||||
mountPath: /var/lib/kubelet
|
||||
mountPropagation: "Bidirectional"
|
||||
- name: plugin-dir
|
||||
mountPath: /csi
|
||||
- name: volumes-dir
|
||||
mountPath: /var/lib/persistent-volumes
|
||||
ports:
|
||||
- name: healthz
|
||||
containerPort: 9809
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: healthz
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 3
|
||||
periodSeconds: 2
|
||||
failureThreshold: 5
|
||||
- name: csi-driver-registrar
|
||||
image: registry.k8s.io/sig-storage/csi-node-driver-registrar@sha256:fdff3ee285341bc58033b6b2458a5d45fd90ec6922a8ba6ebdd49b0c41e2cd34
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --kubelet-registration-path=/var/lib/kubelet/plugins/local.csi.scylladb.com/csi.sock
|
||||
volumeMounts:
|
||||
- name: plugin-dir
|
||||
mountPath: /csi
|
||||
- name: registration-dir
|
||||
mountPath: /registration
|
||||
- name: liveness-probe
|
||||
image: registry.k8s.io/sig-storage/livenessprobe@sha256:cacee2b5c36dd59d4c7e8469c05c9e4ef53ecb2df9025fa8c10cdaf61bce62f0
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --health-port=9809
|
||||
- --v=2
|
||||
volumeMounts:
|
||||
- name: plugin-dir
|
||||
mountPath: /csi
|
||||
- name: csi-provisioner
|
||||
image: registry.k8s.io/sig-storage/csi-provisioner@sha256:ee3b525d5b89db99da3b8eb521d9cd90cb6e9ef0fbb651e98bb37be78d36b5b8
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --v=2
|
||||
- --node-deployment
|
||||
- --feature-gates=Topology=true
|
||||
- --immediate-topology=false
|
||||
- --enable-capacity
|
||||
- --capacity-ownerref-level=0
|
||||
- --capacity-poll-interval=30s
|
||||
- --default-fstype=xfs
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
volumeMounts:
|
||||
- name: plugin-dir
|
||||
mountPath: /csi
|
||||
volumes:
|
||||
- name: kubelet-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet
|
||||
type: Directory
|
||||
- name: plugin-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/plugins/local.csi.scylladb.com/
|
||||
type: DirectoryOrCreate
|
||||
- name: registration-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/plugins_registry/
|
||||
type: Directory
|
||||
- name: volumes-dir
|
||||
hostPath:
|
||||
path: /var/lib/persistent-volumes
|
||||
type: Directory
|
||||
@@ -0,0 +1,117 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: scylladb-config
|
||||
data:
|
||||
scylla.yaml: |
|
||||
authenticator: PasswordAuthenticator
|
||||
authorizer: CassandraAuthorizer
|
||||
---
|
||||
apiVersion: scylla.scylladb.com/v1
|
||||
kind: ScyllaCluster
|
||||
metadata:
|
||||
name: scylladb
|
||||
spec:
|
||||
repository: docker.io/scylladb/scylla
|
||||
version: 2025.1.5
|
||||
agentVersion: 3.5.1
|
||||
developerMode: false
|
||||
automaticOrphanedNodeCleanup: true
|
||||
sysctls:
|
||||
- fs.aio-max-nr=30000000
|
||||
datacenter:
|
||||
name: ge-dc1
|
||||
racks:
|
||||
- name: ge-dc1-rack1
|
||||
members: 1
|
||||
scyllaConfig: scylladb-config
|
||||
storage:
|
||||
capacity: 45Gi
|
||||
storageClassName: scylladb-local-xfs
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
placement:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: topology.kubernetes.io/zone
|
||||
operator: In
|
||||
values:
|
||||
- ge-dc1-rack1
|
||||
- key: scylla.scylladb.com/node-type
|
||||
operator: In
|
||||
values:
|
||||
- scylla
|
||||
tolerations:
|
||||
- key: scylla-operator.scylladb.com/dedicated
|
||||
operator: Equal
|
||||
value: scyllaclusters
|
||||
effect: NoSchedule
|
||||
- name: ge-dc1-rack2
|
||||
members: 1
|
||||
scyllaConfig: scylladb-config
|
||||
storage:
|
||||
capacity: 45Gi
|
||||
storageClassName: scylladb-local-xfs
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
placement:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: topology.kubernetes.io/zone
|
||||
operator: In
|
||||
values:
|
||||
- ge-dc1-rack2
|
||||
- key: scylla.scylladb.com/node-type
|
||||
operator: In
|
||||
values:
|
||||
- scylla
|
||||
tolerations:
|
||||
- key: scylla-operator.scylladb.com/dedicated
|
||||
operator: Equal
|
||||
value: scyllaclusters
|
||||
effect: NoSchedule
|
||||
- name: ge-dc1-rack3
|
||||
members: 1
|
||||
scyllaConfig: scylladb-config
|
||||
storage:
|
||||
capacity: 45Gi
|
||||
storageClassName: scylladb-local-xfs
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 8Gi
|
||||
placement:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: topology.kubernetes.io/zone
|
||||
operator: In
|
||||
values:
|
||||
- ge-dc1-rack3
|
||||
- key: scylla.scylladb.com/node-type
|
||||
operator: In
|
||||
values:
|
||||
- scylla
|
||||
tolerations:
|
||||
- key: scylla-operator.scylladb.com/dedicated
|
||||
operator: Equal
|
||||
value: scyllaclusters
|
||||
effect: NoSchedule
|
||||
@@ -0,0 +1,167 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: scylla-manager
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: scylla-manager-config
|
||||
namespace: scylla-manager
|
||||
data:
|
||||
scylla-manager.yaml: |-
|
||||
http: :5080
|
||||
logger:
|
||||
level: info
|
||||
database:
|
||||
hosts:
|
||||
- scylla-manager-cluster-manager-dc-manager-rack-0
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
namespace: scylla-manager
|
||||
name: scylla-manager-to-scylla-pod
|
||||
spec:
|
||||
policyTypes:
|
||||
- Ingress
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/managed-by: scylla-operator
|
||||
app.kubernetes.io/name: scylla
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: scylla-manager
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
name: scylla-manager
|
||||
namespace: scylla-manager
|
||||
spec:
|
||||
ports:
|
||||
- name: api
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 5080
|
||||
- name: metrics
|
||||
port: 5090
|
||||
protocol: TCP
|
||||
targetPort: 5090
|
||||
selector:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: scylla-manager
|
||||
namespace: scylla-manager
|
||||
labels:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: scylla-manager
|
||||
namespace: scylla-manager
|
||||
labels:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: scylla-manager
|
||||
app.kubernetes.io/instance: scylla-manager
|
||||
spec:
|
||||
serviceAccountName: scylla-manager
|
||||
containers:
|
||||
- name: scylla-manager
|
||||
image: docker.io/scylladb/scylla-manager:3.5.1@sha256:6986ecfc8c925c3d59b65bbcb9763d62f7591a00bb30242842aada115929e816
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /usr/bin/scylla-manager
|
||||
args:
|
||||
- --config-file=/mnt/etc/scylla-manager/scylla-manager.yaml
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
volumeMounts:
|
||||
- mountPath: /mnt/etc/scylla-manager
|
||||
name: scylla-manager-config
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/clusters
|
||||
port: 5080
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
volumes:
|
||||
- configMap:
|
||||
name: scylla-manager-config
|
||||
name: scylla-manager-config
|
||||
|
||||
---
|
||||
apiVersion: scylla.scylladb.com/v1
|
||||
kind: ScyllaCluster
|
||||
metadata:
|
||||
name: scylla-manager-cluster
|
||||
namespace: scylla-manager
|
||||
spec:
|
||||
version: 2025.1.5
|
||||
agentVersion: 3.5.1@sha256:d1b57d08b9949c8faad2048fdf4dc7c502dae81da856c3c6b3a77dd347d5c7fc
|
||||
repository: docker.io/scylladb/scylla
|
||||
agentRepository: docker.io/scylladb/scylla-manager-agent
|
||||
developerMode: true
|
||||
cpuset: true
|
||||
sysctls:
|
||||
- fs.aio-max-nr=30000000
|
||||
datacenter:
|
||||
name: manager-dc
|
||||
racks:
|
||||
- members: 1
|
||||
name: manager-rack
|
||||
placement:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scylla.scylladb.com/node-type
|
||||
operator: In
|
||||
values:
|
||||
- scylla
|
||||
tolerations:
|
||||
- effect: NoSchedule
|
||||
key: scylla-operator.scylladb.com/dedicated
|
||||
operator: Equal
|
||||
value: scyllaclusters
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 200Mi
|
||||
requests:
|
||||
cpu: 1
|
||||
memory: 200Mi
|
||||
storage:
|
||||
capacity: 5Gi
|
||||
storageClassName: scylladb-local-xfs
|
||||
---
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
uv run ansible-playbook -i inventory.ini playbooks/01-setup.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/02-copy-k8s-files.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/03-cert-manager.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/04-prometheus-operator.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/05-scylla-operator.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/06-scylla-cluster.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/07-scylla-manager.yml
|
||||
uv run ansible-playbook -i inventory.ini playbooks/08-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt
|
||||
@@ -0,0 +1,14 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
31353838363564633532353539313763643561396661313030346466653839663931323961663362
|
||||
6333376338336336346562643136316439393962356236640a653836643066346564663363633364
|
||||
30616634363430646335663339613333616334393037333065336664396338346135353239346261
|
||||
3366303231616166300a613566643231303066373266646665343637353364303939346464323633
|
||||
37623565643063353435376238326564353563653735393164616136373531356135666337353765
|
||||
39326463633965383463353335306432326433313132373461633666343934616638346666306339
|
||||
36313561363965663662313163663362333466663135363731643737386466643134396564353439
|
||||
32313936316432313433336235643337386430633434363764666439386234323933313465306638
|
||||
65396532396533303662326338376661343962613432393735356130613165626465656464343434
|
||||
63646265333064623735646161656236623366623265626664313564316665396331616631613536
|
||||
38646264656563353934383237626534663563666335666430326562303862616461343935393737
|
||||
62343637336563383864383732303861633634303061376666353231323331643538333633643762
|
||||
35623539303562643763336232353633666661626539356465383631336261363532
|
||||
@@ -0,0 +1,186 @@
|
||||
- name: Install K3s and Apply Security Configurations on All Nodes
|
||||
hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Install curl
|
||||
ansible.builtin.apt:
|
||||
name: curl
|
||||
state: present
|
||||
when: ansible_os_family == "Debian"
|
||||
|
||||
- name: Download k3s install script
|
||||
ansible.builtin.get_url:
|
||||
url: https://get.k3s.io
|
||||
dest: /tmp/k3s_install.sh
|
||||
mode: '0755'
|
||||
|
||||
- name: Install Python Kubernetes client
|
||||
ansible.builtin.apt:
|
||||
name: python3-kubernetes
|
||||
state: present
|
||||
|
||||
- name: Disable UFW
|
||||
ansible.builtin.command: ufw disable
|
||||
changed_when: false
|
||||
|
||||
- name: Disable password authentication for SSH
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^PasswordAuthentication'
|
||||
line: 'PasswordAuthentication no'
|
||||
state: present
|
||||
backup: true
|
||||
|
||||
- name: Disable root password login
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^PermitRootLogin'
|
||||
line: 'PermitRootLogin prohibit-password'
|
||||
state: present
|
||||
backup: true
|
||||
|
||||
- name: Restart SSH to apply config
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Install fail2ban
|
||||
ansible.builtin.apt:
|
||||
name: fail2ban
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Configure fail2ban for SSH
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/fail2ban/jail.local
|
||||
create: true
|
||||
block: |
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ssh
|
||||
logpath = /var/log/auth.log
|
||||
maxretry = 5
|
||||
bantime = 3600
|
||||
mode: '0644'
|
||||
|
||||
- name: Restart fail2ban
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
|
||||
- name: Set up K3s on Init Node
|
||||
hosts: init
|
||||
become: true
|
||||
tasks:
|
||||
- name: Run k3s Installer
|
||||
ansible.builtin.shell: |
|
||||
INSTALL_K3S_EXEC="server --tls-san {{ ansible_host }} --flannel-backend=wireguard-native --cluster-init" \
|
||||
sh /tmp/k3s_install.sh
|
||||
args:
|
||||
creates: /usr/local/bin/k3s
|
||||
|
||||
- name: Ensure K3s Service is Started and Enabled
|
||||
ansible.builtin.systemd:
|
||||
name: k3s
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Get Joining Token
|
||||
ansible.builtin.command: cat /var/lib/rancher/k3s/server/node-token
|
||||
register: k3s_token
|
||||
run_once: true
|
||||
tags: skip_ansible_lint
|
||||
changed_when: false
|
||||
|
||||
- name: Set up K3s on Master Nodes
|
||||
hosts: master
|
||||
become: true
|
||||
tasks:
|
||||
- name: Run k3s Installer
|
||||
ansible.builtin.shell: |
|
||||
INSTALL_K3S_EXEC="server --flannel-backend=wireguard-native --tls-san {{ ansible_host }}" \
|
||||
K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||
args:
|
||||
creates: /usr/local/bin/k3s
|
||||
|
||||
- name: Ensure k3s service is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: k3s
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Set up K3s on Worker Nodes
|
||||
hosts: worker
|
||||
become: true
|
||||
tasks:
|
||||
- name: Run k3s Installer
|
||||
ansible.builtin.shell: |
|
||||
INSTALL_K3S_EXEC="agent" K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||
args:
|
||||
creates: /usr/local/bin/k3s
|
||||
|
||||
- name: Ensure k3s service is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: k3s-agent
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Wait for All K3s Nodes to Become Ready
|
||||
hosts: init
|
||||
become: true
|
||||
tasks:
|
||||
- name: Wait until all nodes report Ready
|
||||
ansible.builtin.shell: |
|
||||
kubectl wait --for=condition=Ready node --all --timeout=10m
|
||||
register: wait_nodes
|
||||
retries: 5
|
||||
delay: 30
|
||||
until: wait_nodes.rc == 0
|
||||
changed_when: false
|
||||
|
||||
- name: Set Up Node Labels and Taints
|
||||
hosts: init
|
||||
become: true
|
||||
tasks:
|
||||
- name: Set Rack Tags to All Nodes
|
||||
loop: "{{ groups['all'] }}"
|
||||
kubernetes.core.k8s:
|
||||
api_version: v1
|
||||
kind: Node
|
||||
name: "{{ item }}"
|
||||
definition:
|
||||
metadata:
|
||||
labels:
|
||||
topology.kubernetes.io/zone: "{{ hostvars[item].rack }}"
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
state: present
|
||||
|
||||
- name: Set Scylla Tags to Scylla Nodes
|
||||
loop: "{{ groups['scylla'] }}"
|
||||
kubernetes.core.k8s:
|
||||
api_version: v1
|
||||
kind: Node
|
||||
name: "{{ item }}"
|
||||
definition:
|
||||
metadata:
|
||||
labels:
|
||||
scylla.scylladb.com/node-type: scylla
|
||||
spec:
|
||||
taints:
|
||||
- key: scylla-operator.scylladb.com/dedicated
|
||||
value: scyllaclusters
|
||||
effect: NoSchedule
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
state: present
|
||||
|
||||
- name: Create Containerd Socket Symlink
|
||||
hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Create Containerd Symlink
|
||||
ansible.builtin.command: |
|
||||
ln -s /run/k3s/containerd/containerd.sock /run/containerd/containerd.sock
|
||||
args:
|
||||
creates: /run/containerd/containerd.sock
|
||||
@@ -0,0 +1,19 @@
|
||||
- name: Copy K8s files to node
|
||||
hosts: init
|
||||
become: true
|
||||
gather_facts: false
|
||||
vars:
|
||||
k8s_files_src: "{{ playbook_dir }}/../k8s"
|
||||
k8s_files_dest: /tmp/k8s
|
||||
tasks:
|
||||
- name: Create destination directory for K8s files
|
||||
ansible.builtin.file:
|
||||
path: "{{ k8s_files_dest }}"
|
||||
state: directory
|
||||
mode: '0644'
|
||||
|
||||
- name: Copy K8s files to destination
|
||||
ansible.posix.synchronize:
|
||||
src: "{{ k8s_files_src }}/"
|
||||
dest: "{{ k8s_files_dest }}/"
|
||||
recursive: true
|
||||
@@ -0,0 +1,46 @@
|
||||
- name: Set up Cert-Manager
|
||||
hosts: init
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars:
|
||||
cert_manager_ns: cert-manager
|
||||
cert_manager_crds:
|
||||
- certificates.cert-manager.io
|
||||
- issuers.cert-manager.io
|
||||
cert_manager_deployments:
|
||||
- cert-manager
|
||||
- cert-manager-cainjector
|
||||
- cert-manager-webhook
|
||||
tasks:
|
||||
- name: Apply Cert-Manager YAML
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
src: "/tmp/k8s/scylla/01-cert-manager.yml"
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Wait for Cert-Manager CRDs
|
||||
ansible.builtin.shell: |
|
||||
kubectl wait --for condition=established --timeout=60s crd/{{ cert_manager_crds | join(' crd/') }}
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Cert-Manager deployments
|
||||
ansible.builtin.shell: |
|
||||
kubectl -n {{ cert_manager_ns }} rollout status --timeout=10m deployment.apps/{{ item }}
|
||||
loop: "{{ cert_manager_deployments }}"
|
||||
loop_control:
|
||||
loop_var: item
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Cert-Manager webhook CA secret
|
||||
ansible.builtin.shell: |
|
||||
for i in {1..30}; do
|
||||
kubectl -n {{ cert_manager_ns }} get secret/cert-manager-webhook-ca && break
|
||||
sleep 1
|
||||
done
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
@@ -0,0 +1,26 @@
|
||||
- name: Set up Prometheus operator
|
||||
hosts: init
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars:
|
||||
prometheus_crds:
|
||||
- prometheuses.monitoring.coreos.com
|
||||
- prometheusrules.monitoring.coreos.com
|
||||
- servicemonitors.monitoring.coreos.com
|
||||
tasks:
|
||||
- name: Apply Prometheus operator server-side
|
||||
ansible.builtin.command: |
|
||||
kubectl apply --server-side -f /tmp/k8s/scylla/02-prometheus-operator.yml -n prometheus-operator
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Prometheus operator CRDs
|
||||
ansible.builtin.shell: |
|
||||
kubectl wait --for='condition=established' crd/{{ prometheus_crds | join(' crd/') }}
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Prometheus operator deployment
|
||||
ansible.builtin.shell: |
|
||||
kubectl -n=prometheus-operator rollout status --timeout=10m deployment.apps/prometheus-operator
|
||||
changed_when: false
|
||||
@@ -0,0 +1,67 @@
|
||||
- name: Set up Scylla Operator, NodeConfig, and the Local CSI Driver
|
||||
hosts: init
|
||||
become: true
|
||||
vars:
|
||||
scylla_crds:
|
||||
- scyllaclusters.scylla.scylladb.com
|
||||
- nodeconfigs.scylla.scylladb.com
|
||||
- scyllaoperatorconfigs.scylla.scylladb.com
|
||||
- scylladbmonitorings.scylla.scylladb.com
|
||||
scylla_deployments:
|
||||
- scylla-operator
|
||||
- webhook-server
|
||||
tasks:
|
||||
- name: Apply Scylla Operator YAML
|
||||
ansible.builtin.command: |
|
||||
kubectl apply --server-side -f /tmp/k8s/scylla/03-scylla-operator.yml -n scylla-operator
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Scylla Operator CRDs
|
||||
ansible.builtin.shell: |
|
||||
kubectl wait --for='condition=established' crd/{{ scylla_crds | join(' crd/') }}
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Scylla Operator Deployments
|
||||
ansible.builtin.shell: |
|
||||
kubectl -n scylla-operator rollout status --timeout=10m deployment.apps/{{ item }}
|
||||
loop: "{{ scylla_deployments }}"
|
||||
loop_control:
|
||||
loop_var: item
|
||||
environment:
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
changed_when: false
|
||||
|
||||
- name: Apply Scylla NodeConfig YAML
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
src: "/tmp/k8s/scylla/04-scylla-node-config.yml"
|
||||
namespace: scylla-operator
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Wait for Scylla NodeConfig to be applied across all nodes
|
||||
ansible.builtin.command: |
|
||||
kubectl wait --for='condition=Reconciled' --timeout=10m nodeconfigs.scylla.scylladb.com/scylladb-nodepool-1
|
||||
changed_when: false
|
||||
|
||||
- name: Find Scylla CSI Driver K8s YAMLs on Remote Host
|
||||
ansible.builtin.find:
|
||||
paths: /tmp/k8s/scylla
|
||||
patterns: "05-*.yml"
|
||||
file_type: file
|
||||
register: scylla_csi_driver_files
|
||||
|
||||
- name: Apply all Scylla CSI Driver K8s YAMLs
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
src: "{{ item.path }}"
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
loop: "{{ scylla_csi_driver_files.files | sort(attribute='path') }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Wait for Local CSI Driver DaemonSet
|
||||
ansible.builtin.command: |
|
||||
kubectl -n=local-csi-driver rollout status --timeout=10m daemonset.apps/local-csi-driver
|
||||
changed_when: false
|
||||
@@ -0,0 +1,35 @@
|
||||
- name: Set Up Scylla Cluster
|
||||
hosts: init
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Create Scylla Namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: scylla
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Apply Scylla Cluster Config
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
src: /tmp/k8s/scylla/06-scylla-cluster.yml
|
||||
namespace: scylla
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Wait for Scylla Cluster Application
|
||||
ansible.builtin.command: |
|
||||
kubectl -n scylla rollout status statefulset/{{ item }} --timeout=10m
|
||||
loop:
|
||||
- scylladb-ge-dc1-ge-dc1-rack1
|
||||
- scylladb-ge-dc1-ge-dc1-rack2
|
||||
- scylladb-ge-dc1-ge-dc1-rack3
|
||||
register: rollout
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: rollout.rc == 0
|
||||
ignore_errors: true
|
||||
changed_when: false
|
||||
@@ -0,0 +1,14 @@
|
||||
- name: Set Up Scylla Manager
|
||||
hosts: init
|
||||
become: true
|
||||
tasks:
|
||||
- name: Apply Scylla Manager YAML
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
src: "/tmp/k8s/scylla/07-scylla-manager.yml"
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Wait for Scylla Manager Deployment
|
||||
ansible.builtin.command: |
|
||||
kubectl -n=scylla-manager rollout status --timeout=10m deployment.apps/scylla-manager
|
||||
changed_when: false
|
||||
@@ -0,0 +1,125 @@
|
||||
- name: Build and Push Payphone
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Get Cargo Project Metadata
|
||||
ansible.builtin.command: cargo metadata --no-deps --format-version 1
|
||||
register: cargo_metadata
|
||||
changed_when: false
|
||||
|
||||
- name: Parse version
|
||||
ansible.builtin.set_fact:
|
||||
app_version: "v{{ (cargo_metadata.stdout | from_json).packages[0].version }}"
|
||||
|
||||
- name: Build and Push the Docker image
|
||||
community.docker.docker_image:
|
||||
source: build
|
||||
build:
|
||||
path: "{{ playbook_dir }}/../"
|
||||
name: registry.nyeki.dev/payphone/payphone
|
||||
tag: "{{ app_version }}"
|
||||
push: true
|
||||
|
||||
- name: Deploy Payphone
|
||||
hosts: init
|
||||
become: true
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- 00-vault.yml
|
||||
tasks:
|
||||
- name: Create Payphone Namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: payphone
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Create Bot Token Secret
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
namespace: payphone
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: payphone-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
PAYPHONE_TOKEN: "{{ bot_token }}"
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Set Git credentials
|
||||
ansible.builtin.shell: >
|
||||
kubectl create secret docker-registry payphone-git-credentials \
|
||||
--docker-server=registry.nyeki.dev \
|
||||
--docker-username={{ gitlab_username }} \
|
||||
--docker-password={{ gitlab_pat }} \
|
||||
--docker-email={{ gitlab_email }} \
|
||||
-n payphone \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Apply Payphone Deployment
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
namespace: payphone
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: payphone
|
||||
labels:
|
||||
app: payphone
|
||||
spec:
|
||||
serviceName: payphone
|
||||
replicas: "{{ bot_shards }}"
|
||||
selector:
|
||||
matchLabels:
|
||||
app: payphone
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: payphone
|
||||
spec:
|
||||
containers:
|
||||
- name: payphone
|
||||
image: registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||
imagePullPolicy: "Always"
|
||||
args: [
|
||||
"start",
|
||||
"--shard",
|
||||
"$(SHARD_ID):$(SHARD_COUNT)",
|
||||
"--database-addr",
|
||||
"scylladb-client.scylla.svc.cluster.local",
|
||||
"--database-username",
|
||||
"cassandra",
|
||||
"--database-password",
|
||||
"cassandra"
|
||||
]
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: payphone-secrets
|
||||
env:
|
||||
- name: SHARD_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels['apps.kubernetes.io/pod-index']
|
||||
- name: SHARD_COUNT
|
||||
value: "{{ bot_shards | string }}"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "100Mi"
|
||||
imagePullSecrets:
|
||||
- name: payphone-git-credentials
|
||||
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
- name: Rollout New Version
|
||||
ansible.builtin.command:
|
||||
kubectl set image -n payphone sts/payphone payphone=registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||
changed_when: false
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "payphone"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"ansible>=11.9.0",
|
||||
"ansible-lint>=25.8.1",
|
||||
"requests>=2.32.4",
|
||||
]
|
||||
@@ -0,0 +1,432 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use payphone::{
|
||||
database::{
|
||||
self,
|
||||
models::{
|
||||
MessageData, Report as PayphoneReport, ReportResolution, Server as PayphoneServer,
|
||||
UserIncident, UserIncidentType,
|
||||
},
|
||||
},
|
||||
utils::{Bye, get_random_u64, u64_to_i64},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{sync::Semaphore, time::Instant};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
dir: String,
|
||||
|
||||
#[arg(long, default_value_t = String::from("127.0.0.1"))]
|
||||
database_addr: String,
|
||||
|
||||
#[arg(long)]
|
||||
database_username: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
database_password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Report {
|
||||
id: u64,
|
||||
message_id: u64,
|
||||
message_content: String,
|
||||
channel_id: u64,
|
||||
user_id: u64,
|
||||
created_at: String,
|
||||
resolved: String,
|
||||
resolved_at: Option<String>,
|
||||
resolved_by_id: Option<u64>,
|
||||
user_avatar_url: String,
|
||||
user_name: String,
|
||||
reply_user_name: Option<String>,
|
||||
reply_message_content: Option<String>,
|
||||
reply_user_avatar_url: Option<String>,
|
||||
reply_user_id: Option<u64>,
|
||||
reported_by_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct User {
|
||||
id: u64,
|
||||
is_masked: String,
|
||||
is_moderator: String,
|
||||
is_supporter_until: String,
|
||||
warns_count: u64,
|
||||
bans_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Ban {
|
||||
id: u64,
|
||||
user_id: u64,
|
||||
created_at: String,
|
||||
expires_at: Option<String>,
|
||||
created_by_id: u64,
|
||||
expired_by_id: Option<u64>,
|
||||
report_id: Option<u64>,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Server {
|
||||
id: u64,
|
||||
prefix: String,
|
||||
language: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Warn {
|
||||
id: u64,
|
||||
user_id: u64,
|
||||
reason: String,
|
||||
created_at: String,
|
||||
created_by_id: u64,
|
||||
report_id: u64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
println!("Testing DB connection...");
|
||||
|
||||
let db = Arc::new(
|
||||
database::init(
|
||||
args.database_addr,
|
||||
args.database_username,
|
||||
args.database_password,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to initialize database"),
|
||||
);
|
||||
|
||||
println!("Reading migration file {}/reports.csv", args.dir);
|
||||
|
||||
let mut rdr = csv::Reader::from_path(format!("{}/reports.csv", args.dir))
|
||||
.expect("Failed to open reports file");
|
||||
|
||||
let mut reports = Vec::new();
|
||||
|
||||
let format = time::format_description::parse(
|
||||
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond][offset_hour]",
|
||||
)
|
||||
.expect("Failed to parse format description");
|
||||
|
||||
for result in rdr.deserialize() {
|
||||
let report: Report = result.expect("Failed to deserialize report");
|
||||
reports.push(report);
|
||||
}
|
||||
|
||||
println!("Finished reading reports, found {} reports", reports.len());
|
||||
println!("Reading migration file {}/users.csv", args.dir);
|
||||
|
||||
let mut rdr = csv::Reader::from_path(format!("{}/users.csv", args.dir))
|
||||
.expect("Failed to open users file");
|
||||
|
||||
let mut users = HashMap::new();
|
||||
|
||||
for result in rdr.deserialize() {
|
||||
let user: User = result.expect("Failed to deserialize user");
|
||||
users.insert(user.id, user).bye();
|
||||
}
|
||||
|
||||
println!("Finished reading users, found {} users", users.len());
|
||||
println!("Reading migration file {}/bans.csv", args.dir);
|
||||
|
||||
let mut rdr =
|
||||
csv::Reader::from_path(format!("{}/bans.csv", args.dir)).expect("Failed to open bans file");
|
||||
|
||||
let mut bans = HashMap::new();
|
||||
|
||||
for result in rdr.deserialize() {
|
||||
let ban: Ban = result.expect("Failed to deserialize ban");
|
||||
bans.insert(ban.id, ban).bye();
|
||||
}
|
||||
|
||||
println!("Finished reading bans, found {} bans", bans.len());
|
||||
println!("Reading migration file {}/servers.csv", args.dir);
|
||||
|
||||
let mut rdr = csv::Reader::from_path(format!("{}/servers.csv", args.dir))
|
||||
.expect("Failed to open servers file");
|
||||
|
||||
let mut servers = HashMap::new();
|
||||
|
||||
for result in rdr.deserialize() {
|
||||
let server: Server = result.expect("Failed to deserialize server");
|
||||
servers.insert(server.id, server).bye();
|
||||
}
|
||||
|
||||
println!("Finished reading servers, found {} servers", servers.len());
|
||||
println!("Reading migration file {}/warns.csv", args.dir);
|
||||
|
||||
let mut rdr = csv::Reader::from_path(format!("{}/warns.csv", args.dir))
|
||||
.expect("Failed to open warns file");
|
||||
|
||||
let mut warns = HashMap::new();
|
||||
|
||||
for result in rdr.deserialize() {
|
||||
let warn: Warn = result.expect("Failed to deserialize warn");
|
||||
warns.insert(warn.id, warn).bye();
|
||||
}
|
||||
|
||||
println!("Finished reading warns, found {} warns", warns.len());
|
||||
println!("Starting migration...");
|
||||
println!("Inserting server configurations...");
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(50));
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for (i, server) in servers.values().enumerate() {
|
||||
let payphone_server = PayphoneServer {
|
||||
id: server.id,
|
||||
prefix: server.prefix.clone(),
|
||||
language: server.language.clone(),
|
||||
name: server.name.clone(),
|
||||
};
|
||||
|
||||
let task_db = db.clone();
|
||||
let task_semaphore = semaphore.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = task_semaphore.acquire().await;
|
||||
|
||||
let start = Instant::now();
|
||||
payphone_server
|
||||
.insert(&task_db)
|
||||
.await
|
||||
.expect("Failed to insert server");
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"({i}) Inserted server {} in {:?}",
|
||||
payphone_server.id, duration
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
let bans = Arc::new(bans);
|
||||
let warns = Arc::new(warns);
|
||||
let reports = Arc::new(reports);
|
||||
|
||||
for (i, report) in reports.iter().enumerate() {
|
||||
let task_db = db.clone();
|
||||
let task_semaphore = semaphore.clone();
|
||||
let task_report = report.clone();
|
||||
let task_bans = bans.clone();
|
||||
let task_warns = warns.clone();
|
||||
let task_format = format.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = task_semaphore.acquire().await;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let resolution = if task_report.resolved == "t" {
|
||||
if task_bans.iter().any(|(_id, ban)| {
|
||||
ban.report_id
|
||||
.map(|report_id| task_report.id == report_id)
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
Some(ReportResolution::Ban)
|
||||
} else if task_warns
|
||||
.iter()
|
||||
.any(|(_id, warn)| warn.report_id == task_report.id)
|
||||
{
|
||||
Some(ReportResolution::Warn)
|
||||
} else {
|
||||
Some(ReportResolution::Skip)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let payphone_report = PayphoneReport {
|
||||
id: task_report.id,
|
||||
user_id: task_report.user_id,
|
||||
message: MessageData {
|
||||
id: u64_to_i64(task_report.message_id),
|
||||
call_id: 0,
|
||||
author_id: u64_to_i64(task_report.user_id),
|
||||
author_name: task_report.user_name,
|
||||
author_avatar_url: task_report.user_avatar_url,
|
||||
author_mask: None,
|
||||
content: task_report.message_content,
|
||||
attachments: vec![],
|
||||
sticker: None,
|
||||
propagation_message_id: None,
|
||||
propagation_channel_id: None,
|
||||
replying_to_message_id: None,
|
||||
is_deleted: false,
|
||||
is_edited: false,
|
||||
is_censored: false,
|
||||
},
|
||||
created_at: OffsetDateTime::parse(&task_report.created_at, &task_format)
|
||||
.expect("Failed to parse created_at")
|
||||
.to_utc(),
|
||||
created_by_ids: task_report
|
||||
.reported_by_id
|
||||
.map(|id| vec![id])
|
||||
.unwrap_or_default(),
|
||||
replying_to: None,
|
||||
reviewed_at: task_report.resolved_at.as_ref().map(|s| {
|
||||
OffsetDateTime::parse(s, &task_format)
|
||||
.expect("Failed to parse resolved_at")
|
||||
.to_utc()
|
||||
}),
|
||||
resolution,
|
||||
reviewed_by_id: task_report.resolved_by_id,
|
||||
};
|
||||
|
||||
payphone_report
|
||||
.insert(&task_db)
|
||||
.await
|
||||
.expect("Failed to insert report");
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"({i}) Inserted report {} in {:?}",
|
||||
payphone_report.id, duration
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
for (i, ban) in bans.values().enumerate() {
|
||||
let task_semaphore = semaphore.clone();
|
||||
let task_format = format.clone();
|
||||
let task_db = db.clone();
|
||||
let task_reports = reports.clone();
|
||||
let task_ban = Arc::new(ban.clone());
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = task_semaphore.acquire().await;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let message = task_reports
|
||||
.iter()
|
||||
.find(|report| {
|
||||
task_ban
|
||||
.report_id
|
||||
.map(|id| id == report.id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|report| MessageData {
|
||||
id: u64_to_i64(report.message_id),
|
||||
call_id: 0,
|
||||
author_id: u64_to_i64(report.user_id),
|
||||
author_name: report.user_name.clone(),
|
||||
author_avatar_url: report.user_avatar_url.clone(),
|
||||
author_mask: None,
|
||||
content: report.message_content.clone(),
|
||||
attachments: vec![],
|
||||
sticker: None,
|
||||
propagation_message_id: None,
|
||||
propagation_channel_id: None,
|
||||
replying_to_message_id: None,
|
||||
is_deleted: false,
|
||||
is_edited: false,
|
||||
is_censored: false,
|
||||
});
|
||||
|
||||
let payphone_incident = UserIncident {
|
||||
incident_id: get_random_u64(),
|
||||
user_id: task_ban.user_id,
|
||||
report_id: task_ban.report_id,
|
||||
r#type: UserIncidentType::Ban,
|
||||
message,
|
||||
reason: task_ban.reason.clone(),
|
||||
created_at: OffsetDateTime::parse(&task_ban.created_at, &task_format)
|
||||
.expect("Failed to parse created_at")
|
||||
.to_utc(),
|
||||
created_by_id: Some(task_ban.created_by_id),
|
||||
duration: None,
|
||||
expires_at: task_ban.expires_at.as_ref().map(|expires_at| {
|
||||
OffsetDateTime::parse(expires_at, &task_format)
|
||||
.expect("Failed to parse expires_at")
|
||||
.to_utc()
|
||||
}),
|
||||
};
|
||||
|
||||
payphone_incident
|
||||
.insert(&task_db)
|
||||
.await
|
||||
.expect("Failed to insert ban");
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"({i}) Inserted incident (ban) {} in {:?}",
|
||||
payphone_incident.incident_id, duration
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
for (i, warn) in warns.values().enumerate() {
|
||||
let task_semaphore = semaphore.clone();
|
||||
let task_format = format.clone();
|
||||
let task_db = db.clone();
|
||||
let task_reports = reports.clone();
|
||||
let task_warn = Arc::new(warn.clone());
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = task_semaphore.acquire().await;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let message = task_reports
|
||||
.iter()
|
||||
.find(|report| report.id == task_warn.report_id)
|
||||
.map(|report| MessageData {
|
||||
id: u64_to_i64(report.message_id),
|
||||
call_id: 0,
|
||||
author_id: u64_to_i64(report.user_id),
|
||||
author_name: report.user_name.clone(),
|
||||
author_avatar_url: report.user_avatar_url.clone(),
|
||||
author_mask: None,
|
||||
content: report.message_content.clone(),
|
||||
attachments: vec![],
|
||||
sticker: None,
|
||||
propagation_message_id: None,
|
||||
propagation_channel_id: None,
|
||||
replying_to_message_id: None,
|
||||
is_deleted: false,
|
||||
is_edited: false,
|
||||
is_censored: false,
|
||||
});
|
||||
|
||||
let payphone_incident = UserIncident {
|
||||
incident_id: get_random_u64(),
|
||||
user_id: task_warn.user_id,
|
||||
report_id: Some(task_warn.report_id),
|
||||
r#type: UserIncidentType::Warning,
|
||||
message,
|
||||
reason: task_warn.reason.clone(),
|
||||
created_at: OffsetDateTime::parse(&task_warn.created_at, &task_format)
|
||||
.expect("Failed to parse created_at")
|
||||
.to_utc(),
|
||||
created_by_id: Some(task_warn.created_by_id),
|
||||
duration: None,
|
||||
expires_at: None,
|
||||
};
|
||||
|
||||
payphone_incident
|
||||
.insert(&task_db)
|
||||
.await
|
||||
.expect("Failed to insert warn");
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"({i}) Inserted incident (warn) {} in {:?}",
|
||||
payphone_incident.incident_id, duration
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
task.await.expect("Failed to join task");
|
||||
}
|
||||
}
|
||||
+85
-5
@@ -45,21 +45,28 @@
|
||||
//! ```shell
|
||||
//! $ cargo run -- start [TOKEN]
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! To run multiple shards, pass the `--shard` or `-s` argument to the `start` command with the
|
||||
//! shard ID and total amount of shards as `id:total`. For example, for a single-shard bot, you'd
|
||||
//! set it to `-s 0:1`. ID is zero-indexed (0 means first shard), total is one-indexed (0 means no
|
||||
//! shards).
|
||||
//!
|
||||
//! # Running the Documentation Server
|
||||
//!
|
||||
//!
|
||||
//! Payphone has a built-in documentation server that serves the project's documentation and
|
||||
//! refreshes it on changes. To start the documentation server, run the following command:
|
||||
//!
|
||||
//!
|
||||
//! ```shell
|
||||
//! $ cargo run -- docs --open
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! The `--open` flag is optional and will open the documentation in the default browser after
|
||||
//! starting the server. You can also use the `--bind` argument to specify the address to bind the
|
||||
//! server to, which defaults to `0.0.0.0:3000`.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::{fmt, net::SocketAddr, str::FromStr};
|
||||
|
||||
use twilight_gateway::ShardId;
|
||||
|
||||
/// Payphone's argument parser.
|
||||
#[derive(clap::Parser)]
|
||||
@@ -122,6 +129,54 @@ impl Into<log::LevelFilter> for LevelFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shard {
|
||||
pub id: u32,
|
||||
pub total: u32,
|
||||
}
|
||||
|
||||
impl Shard {
|
||||
fn new(id: u32, total: u32) -> Self {
|
||||
Shard { id, total }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Shard {
|
||||
fn default() -> Self {
|
||||
Shard::new(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Shard {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.split_once(":") {
|
||||
Some((id, total)) => {
|
||||
let id = id.parse().map_err(|_| "Invalid shard ID")?;
|
||||
let total = total.parse().map_err(|_| "Invalid shard total")?;
|
||||
|
||||
Ok(Shard { id, total })
|
||||
}
|
||||
None => Err("Invalid shard format, expected `id:total`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Shard {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.id, self.total)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<ShardId> for Shard {
|
||||
fn into(self) -> ShardId {
|
||||
ShardId::new(self.id, self.total)
|
||||
}
|
||||
}
|
||||
|
||||
/// The available commands for the Payphone CLI and their available arguments.
|
||||
#[derive(clap::Subcommand, Clone)]
|
||||
pub enum Commands {
|
||||
/// Starts the bot with the provided token.
|
||||
@@ -139,6 +194,31 @@ pub enum Commands {
|
||||
default_value_t = String::from("127.0.0.1:9042"))
|
||||
]
|
||||
database_addr: String,
|
||||
|
||||
/// The username to use when connecting to the database.
|
||||
#[arg(
|
||||
long,
|
||||
env = "PAYPHONE_DATABASE_USERNAME",
|
||||
value_name = "USERNAME"
|
||||
)]
|
||||
database_username: Option<String>,
|
||||
|
||||
/// The password to use when connecting to the database.
|
||||
#[arg(
|
||||
long,
|
||||
env = "PAYPHONE_DATABASE_PASSWORD",
|
||||
value_name = "PASSWORD"
|
||||
)]
|
||||
database_password: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
env = "PAYPHONE_SHARD",
|
||||
value_name = "ID:TOTAL",
|
||||
default_value_t = Shard::default())
|
||||
]
|
||||
shard: Shard,
|
||||
},
|
||||
|
||||
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
|
||||
|
||||
@@ -8,3 +8,6 @@ pub const COLOR_WHITE: u32 = 0xE2D9D0;
|
||||
|
||||
/// A red color used for error messages and moderator embeds.
|
||||
pub const COLOR_RED: u32 = 0xFF8787;
|
||||
|
||||
/// A yellow color used for mask notifications and warnings.
|
||||
pub const COLOR_YELLOW: u32 = 0xFFC868;
|
||||
|
||||
@@ -60,12 +60,11 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
|
||||
PRIMARY KEY (call_id, user_id, channel_id)
|
||||
);
|
||||
|
||||
-- TTL 30 days.
|
||||
CREATE TABLE IF NOT EXISTS payphone.messages (
|
||||
CREATE TYPE IF NOT EXISTS payphone.message (
|
||||
id BIGINT,
|
||||
call_id BIGINT,
|
||||
message_id BIGINT,
|
||||
author_id BIGINT,
|
||||
|
||||
author_id BIGINT,
|
||||
author_name TEXT,
|
||||
author_avatar_url TEXT,
|
||||
author_mask ASCII,
|
||||
@@ -77,39 +76,44 @@ CREATE TABLE IF NOT EXISTS payphone.messages (
|
||||
propagation_message_id BIGINT,
|
||||
propagation_channel_id BIGINT,
|
||||
|
||||
replying_to_id BIGINT,
|
||||
replying_to_message_id BIGINT,
|
||||
|
||||
is_deleted BOOLEAN,
|
||||
is_edited BOOLEAN,
|
||||
is_censored BOOLEAN,
|
||||
is_censored BOOLEAN
|
||||
);
|
||||
|
||||
-- TTL 30 days.
|
||||
CREATE TABLE IF NOT EXISTS payphone.messages (
|
||||
call_id BIGINT,
|
||||
message_id BIGINT,
|
||||
author_id BIGINT,
|
||||
propagation_message_id BIGINT,
|
||||
|
||||
data FROZEN<payphone.message>,
|
||||
|
||||
PRIMARY KEY (call_id, message_id)
|
||||
);
|
||||
) WITH default_time_to_live = 2592000; -- 30 days
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.messages_by_propagation_id AS
|
||||
SELECT call_id, message_id, propagation_message_id FROM payphone.messages
|
||||
WHERE call_id IS NOT NULL AND message_id IS NOT NULL AND propagation_message_id IS NOT NULL
|
||||
PRIMARY KEY (call_id, propagation_message_id, message_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.servers (
|
||||
server_id BIGINT PRIMARY KEY,
|
||||
|
||||
name TEXT,
|
||||
prefix TEXT,
|
||||
language TEXT,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||
report_id BIGINT,
|
||||
user_id BIGINT,
|
||||
|
||||
message_author_id BIGINT,
|
||||
message_author_name TEXT,
|
||||
message_author_avatar_url TEXT,
|
||||
message_author_mask ASCII,
|
||||
|
||||
message_id BIGINT,
|
||||
message_content TEXT,
|
||||
message_attachments LIST<TEXT>,
|
||||
message_sticker BIGINT,
|
||||
|
||||
message_replying_to_id BIGINT,
|
||||
message_replying_to_content TEXT,
|
||||
message_replying_to_attachments LIST<TEXT>,
|
||||
message_replying_to_sticker BIGINT,
|
||||
message FROZEN<payphone.message>,
|
||||
replying_to FROZEN<payphone.message>,
|
||||
|
||||
created_at TIMESTAMP,
|
||||
reviewed_at TIMESTAMP,
|
||||
@@ -119,28 +123,18 @@ CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||
reviewed_by_id BIGINT,
|
||||
resolution ASCII,
|
||||
|
||||
PRIMARY KEY (message_author_id, report_id)
|
||||
PRIMARY KEY (user_id, report_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
||||
user_id BIGINT,
|
||||
incident_id BIGINT,
|
||||
report_id BIGINT,
|
||||
|
||||
-- ban, warning
|
||||
type ASCII,
|
||||
|
||||
call_id BIGINT,
|
||||
report_id BIGINT,
|
||||
|
||||
message_id BIGINT,
|
||||
message_content TEXT,
|
||||
message_attachments LIST<TEXT>,
|
||||
message_sticker BIGINT,
|
||||
|
||||
message_replying_to_id BIGINT,
|
||||
message_replying_to_content TEXT,
|
||||
message_replying_to_attachments LIST<TEXT>,
|
||||
message_replying_to_sticker BIGINT,
|
||||
message FROZEN<payphone.message>,
|
||||
|
||||
reason TEXT,
|
||||
|
||||
@@ -164,4 +158,4 @@ CREATE TABLE IF NOT EXISTS payphone.server_incidents (
|
||||
created_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
duration ASCII
|
||||
);
|
||||
);
|
||||
|
||||
+21
-7
@@ -22,13 +22,27 @@ pub enum DatabaseError {
|
||||
|
||||
static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations");
|
||||
|
||||
pub async fn init(uri: impl ToString) -> Result<Session, DatabaseError> {
|
||||
let session = SessionBuilder::new()
|
||||
.known_node(uri.to_string())
|
||||
.build()
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
||||
.map_err(|_| DatabaseError::ConnectionFailed)?;
|
||||
pub async fn init(
|
||||
uri: impl ToString,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Session, DatabaseError> {
|
||||
let session = if let Some((username, password)) = username.zip(password) {
|
||||
SessionBuilder::new()
|
||||
.known_node(uri.to_string())
|
||||
.user(username, password)
|
||||
.build()
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
||||
.map_err(|_| DatabaseError::ConnectionFailed)?
|
||||
} else {
|
||||
SessionBuilder::new()
|
||||
.known_node(uri.to_string())
|
||||
.build()
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Could not connect to the database! {e:?}"))
|
||||
.map_err(|_| DatabaseError::ConnectionFailed)?
|
||||
};
|
||||
|
||||
execute_file(
|
||||
&session,
|
||||
|
||||
+716
-67
@@ -1,22 +1,27 @@
|
||||
use scylla::{
|
||||
DeserializeValue, SerializeValue,
|
||||
client::session::Session,
|
||||
deserialize::row::DeserializeRow,
|
||||
errors::{ExecutionError, SingleRowError},
|
||||
response::query_result::QueryRowsResult,
|
||||
statement::{
|
||||
Statement,
|
||||
batch::{Batch, BatchType},
|
||||
},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use time::{OffsetDateTime, UtcDateTime};
|
||||
use time::{Duration, OffsetDateTime, UtcDateTime, UtcOffset};
|
||||
use twilight_http::Client;
|
||||
use twilight_model::{
|
||||
guild::{Guild as TwilightGuild, GuildInfo as TwilightGuildInfo},
|
||||
id::Id,
|
||||
channel::Message as TwilightMessage, guild::Guild as TwilightGuild, id::Id,
|
||||
user::User as TwilightUser,
|
||||
};
|
||||
|
||||
use crate::{constants::DEFAULT_PREFIX, utils::get_random_u64};
|
||||
use crate::{
|
||||
constants::DEFAULT_PREFIX,
|
||||
groups::calls::DisplayProfile,
|
||||
utils::{get_random_u64, i64_to_u64, u64_to_i64},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DatabaseOrDiscordError {
|
||||
@@ -42,34 +47,6 @@ pub enum DatabaseError {
|
||||
DeserializationFailed,
|
||||
}
|
||||
|
||||
/// Copies the bytes from [`i64`] to [`u64`].
|
||||
///
|
||||
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `i` - The [`i64`] to convert.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`u64`] - The resulting [`u64`].
|
||||
fn i64_to_u64(i: i64) -> u64 {
|
||||
u64::from_ne_bytes(i.to_ne_bytes())
|
||||
}
|
||||
|
||||
/// Copies the bytes from [`u64`] to [`i64`].
|
||||
///
|
||||
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `u` - The [`u64`] to convert.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`i64`] - The resulting [`i64`].
|
||||
fn u64_to_i64(u: u64) -> i64 {
|
||||
i64::from_ne_bytes(u.to_ne_bytes())
|
||||
}
|
||||
|
||||
trait Object<'a> {
|
||||
type Row: DeserializeRow<'a, 'a>;
|
||||
|
||||
@@ -77,6 +54,27 @@ trait Object<'a> {
|
||||
fn to_row(value: Self) -> Self::Row;
|
||||
}
|
||||
|
||||
/// Returns the first row of the result set, if any.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `result` - The DB result.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Result<Option<T>, DatabaseError>`] - The first row of the result set, if any.
|
||||
async fn get_single_row_from_result<'a, T: Object<'a>>(
|
||||
result: &'a QueryRowsResult,
|
||||
) -> Result<Option<T>, DatabaseError> {
|
||||
match result.single_row::<T::Row>() {
|
||||
Ok(row) => Ok(Some(T::from_row(row))),
|
||||
Err(error) => match error {
|
||||
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||
Err(DatabaseError::DeserializationFailed)
|
||||
}
|
||||
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
@@ -132,6 +130,11 @@ impl<'a> Object<'a> for User {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NewUserIncident {
|
||||
pub counting_incidents: usize,
|
||||
pub is_first_incident: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
|
||||
let id = u64_to_i64(id);
|
||||
@@ -172,6 +175,76 @@ impl User {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||
session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
INSERT INTO payphone.users (
|
||||
user_id,
|
||||
name,
|
||||
email,
|
||||
is_masked,
|
||||
is_moderator,
|
||||
joined_at,
|
||||
last_seen_at,
|
||||
supporter_since,
|
||||
supporter_until
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn incident(
|
||||
&self,
|
||||
reason: String,
|
||||
message: Option<MessageData>,
|
||||
created_by_id: Option<u64>,
|
||||
session: &Session,
|
||||
) -> Result<NewUserIncident, DatabaseError> {
|
||||
let incidents = UserIncident::get_for_user(self.id, session).await?;
|
||||
|
||||
let counting_incidents = incidents
|
||||
.iter()
|
||||
.filter(|incident| incident.created_at + Duration::weeks(12) > UtcDateTime::now())
|
||||
.count();
|
||||
|
||||
let incident_type = if counting_incidents != 0 {
|
||||
UserIncidentType::Ban
|
||||
} else {
|
||||
UserIncidentType::Warning
|
||||
};
|
||||
|
||||
let duration = match counting_incidents {
|
||||
0 => None, // Warning
|
||||
1 => Some(Duration::days(1)), // First ban
|
||||
_ => None, // Permanent ban
|
||||
};
|
||||
|
||||
let incident = UserIncident {
|
||||
user_id: self.id,
|
||||
incident_id: get_random_u64(),
|
||||
report_id: None,
|
||||
r#type: incident_type,
|
||||
created_at: UtcDateTime::now(),
|
||||
expires_at: duration.map(|d| UtcDateTime::now() + d),
|
||||
message,
|
||||
reason,
|
||||
created_by_id,
|
||||
duration: duration.map(|_| "1 day".into()),
|
||||
};
|
||||
incident.insert(session).await?;
|
||||
|
||||
Ok(NewUserIncident {
|
||||
counting_incidents,
|
||||
is_first_incident: incidents.is_empty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TwilightUser> for User {
|
||||
@@ -195,21 +268,28 @@ pub struct Server {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub prefix: String,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for Server {
|
||||
type Row = (i64, String, String);
|
||||
type Row = (i64, String, String, String);
|
||||
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
id: i64_to_u64(row.0),
|
||||
name: row.1,
|
||||
prefix: row.2,
|
||||
language: row.3,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(u64_to_i64(value.id), value.name, value.prefix)
|
||||
(
|
||||
u64_to_i64(value.id),
|
||||
value.name,
|
||||
value.prefix,
|
||||
value.language,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +303,8 @@ impl Server {
|
||||
SELECT
|
||||
server_id,
|
||||
name,
|
||||
prefix
|
||||
prefix,
|
||||
language
|
||||
FROM
|
||||
payphone.servers
|
||||
WHERE
|
||||
@@ -248,6 +329,36 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_or_create_by_id(
|
||||
id: u64,
|
||||
session: &Session,
|
||||
http: &Client,
|
||||
) -> Result<Self, DatabaseOrDiscordError> {
|
||||
let server = Server::get_by_id(id, session)
|
||||
.await
|
||||
.map_err(DatabaseOrDiscordError::Database)?;
|
||||
|
||||
if let Some(server) = server {
|
||||
Ok(server)
|
||||
} else {
|
||||
let server: Server = http
|
||||
.guild(Id::new(id))
|
||||
.await
|
||||
.map_err(DatabaseOrDiscordError::Discord)?
|
||||
.model()
|
||||
.await
|
||||
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?
|
||||
.into();
|
||||
|
||||
server
|
||||
.insert(session)
|
||||
.await
|
||||
.map_err(DatabaseOrDiscordError::Database)?;
|
||||
|
||||
Ok(server)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||
session
|
||||
.query_unpaged(
|
||||
@@ -255,8 +366,9 @@ impl Server {
|
||||
INSERT INTO payphone.servers (
|
||||
server_id,
|
||||
name,
|
||||
prefix
|
||||
) VALUES (?, ?, ?) IF NOT EXISTS;
|
||||
prefix,
|
||||
language
|
||||
) VALUES (?, ?, ?, ?) IF NOT EXISTS;
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
)
|
||||
@@ -274,11 +386,13 @@ impl Server {
|
||||
INSERT INTO payphone.servers (
|
||||
server_id,
|
||||
name,
|
||||
prefix
|
||||
prefix,
|
||||
language
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
?
|
||||
?,
|
||||
?
|
||||
);
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
@@ -308,16 +422,7 @@ impl From<TwilightGuild> for Server {
|
||||
id: value.id.get(),
|
||||
name: value.name,
|
||||
prefix: DEFAULT_PREFIX.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TwilightGuildInfo> for Server {
|
||||
fn from(value: TwilightGuildInfo) -> Self {
|
||||
Self {
|
||||
id: value.id.get(),
|
||||
name: value.name,
|
||||
prefix: DEFAULT_PREFIX.into(),
|
||||
language: value.preferred_locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,15 +506,7 @@ impl Channel {
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
match result.single_row::<<Channel as Object>::Row>() {
|
||||
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||
Err(error) => match error {
|
||||
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||
Err(DatabaseError::DeserializationFailed)
|
||||
}
|
||||
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||
},
|
||||
}
|
||||
get_single_row_from_result(&result).await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_by_id(
|
||||
@@ -622,15 +719,7 @@ impl Call {
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
match result.single_row::<<Call as Object>::Row>() {
|
||||
Ok(row) => Ok(Some(Self::from_row(row))),
|
||||
Err(error) => match error {
|
||||
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||
Err(DatabaseError::DeserializationFailed)
|
||||
}
|
||||
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||
},
|
||||
}
|
||||
get_single_row_from_result(&result).await
|
||||
}
|
||||
|
||||
/// Returns the opposite party's channel ID.
|
||||
@@ -826,3 +915,563 @@ impl Call {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SerializeValue, DeserializeValue)]
|
||||
pub struct MessageData {
|
||||
pub id: i64,
|
||||
pub call_id: i64,
|
||||
|
||||
pub author_id: i64,
|
||||
pub author_name: String,
|
||||
pub author_avatar_url: String,
|
||||
pub author_mask: Option<String>,
|
||||
|
||||
pub content: String,
|
||||
pub attachments: Vec<String>,
|
||||
pub sticker: Option<i64>,
|
||||
|
||||
pub propagation_message_id: Option<i64>,
|
||||
pub propagation_channel_id: Option<i64>,
|
||||
|
||||
pub replying_to_message_id: Option<i64>,
|
||||
|
||||
pub is_deleted: bool,
|
||||
pub is_edited: bool,
|
||||
pub is_censored: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("The message was missing either the event's message, the user's profile, or the call ID")]
|
||||
pub struct MessageBuilderError;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MessageBuilder {
|
||||
message: Option<TwilightMessage>,
|
||||
profile: Option<DisplayProfile>,
|
||||
call_id: Option<u64>,
|
||||
is_censored: bool,
|
||||
propagation_message_id: Option<u64>,
|
||||
propagation_channel_id: Option<u64>,
|
||||
replying_to_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: None,
|
||||
profile: None,
|
||||
call_id: None,
|
||||
is_censored: false,
|
||||
propagation_message_id: None,
|
||||
propagation_channel_id: None,
|
||||
replying_to_message_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(mut self, message: TwilightMessage) -> Self {
|
||||
self.message = Some(message);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn profile(mut self, profile: DisplayProfile) -> Self {
|
||||
self.profile = Some(profile);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn call_id(mut self, call_id: u64) -> Self {
|
||||
self.call_id = Some(call_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn is_censored(mut self, is_censored: bool) -> Self {
|
||||
self.is_censored = is_censored;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn propagation_message_id(mut self, propagation_message_id: u64) -> Self {
|
||||
self.propagation_message_id = Some(propagation_message_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn propagation_channel_id(mut self, propagation_channel_id: u64) -> Self {
|
||||
self.propagation_channel_id = Some(propagation_channel_id);
|
||||
self
|
||||
}
|
||||
|
||||
// Argument is made optional here because it happens to be more convenient for where this is
|
||||
// used.
|
||||
pub fn replying_to_message_id(mut self, replying_to_id: Option<u64>) -> Self {
|
||||
self.replying_to_message_id = replying_to_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Message, MessageBuilderError> {
|
||||
let message = self.message.ok_or(MessageBuilderError)?;
|
||||
let profile = self.profile.ok_or(MessageBuilderError)?;
|
||||
let call_id = self.call_id.ok_or(MessageBuilderError)?;
|
||||
|
||||
let data = MessageData {
|
||||
id: u64_to_i64(message.id.get()),
|
||||
call_id: u64_to_i64(call_id),
|
||||
author_id: u64_to_i64(profile.id),
|
||||
author_name: profile.name.clone(),
|
||||
author_avatar_url: profile.avatar_url.clone(),
|
||||
author_mask: profile.mask,
|
||||
content: message.content,
|
||||
attachments: message.attachments.iter().map(|a| a.url.clone()).collect(),
|
||||
sticker: message
|
||||
.sticker_items
|
||||
.first()
|
||||
.map(|s| u64_to_i64(s.id.get())),
|
||||
propagation_message_id: self.propagation_message_id.map(u64_to_i64),
|
||||
propagation_channel_id: self.propagation_channel_id.map(u64_to_i64),
|
||||
replying_to_message_id: self.replying_to_message_id.map(u64_to_i64),
|
||||
|
||||
// You can't create a deleted message, this is always going to be false when
|
||||
// initializing
|
||||
is_deleted: false,
|
||||
is_edited: message.edited_timestamp.is_some(),
|
||||
is_censored: self.is_censored,
|
||||
};
|
||||
|
||||
Ok(Message {
|
||||
call_id,
|
||||
message_id: message.id.get(),
|
||||
author_id: profile.id,
|
||||
propagation_message_id: self.propagation_message_id,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub call_id: u64,
|
||||
pub message_id: u64,
|
||||
pub author_id: u64,
|
||||
pub propagation_message_id: Option<u64>,
|
||||
pub data: MessageData,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for Message {
|
||||
type Row = (i64, i64, i64, Option<i64>, MessageData);
|
||||
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
call_id: i64_to_u64(row.0),
|
||||
message_id: i64_to_u64(row.1),
|
||||
author_id: i64_to_u64(row.2),
|
||||
propagation_message_id: row.3.map(i64_to_u64),
|
||||
data: row.4,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
u64_to_i64(value.call_id),
|
||||
u64_to_i64(value.message_id),
|
||||
u64_to_i64(value.author_id),
|
||||
value.propagation_message_id.map(u64_to_i64),
|
||||
value.data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn build() -> MessageBuilder {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
|
||||
pub async fn get_by_call_and_message_id(
|
||||
call_id: u64,
|
||||
message_id: u64,
|
||||
session: &Session,
|
||||
) -> Result<Option<Self>, DatabaseError> {
|
||||
let call_id = u64_to_i64(call_id);
|
||||
let message_id_i64 = u64_to_i64(message_id);
|
||||
|
||||
let result = session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
SELECT
|
||||
call_id,
|
||||
message_id,
|
||||
author_id,
|
||||
propagation_message_id,
|
||||
data
|
||||
FROM
|
||||
payphone.messages
|
||||
WHERE
|
||||
call_id = ? AND message_id = ?
|
||||
LIMIT 1;
|
||||
"#,
|
||||
(call_id, message_id_i64),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
if result.rows_num() == 0 {
|
||||
let message_by_propagation_id =
|
||||
MessageByPropagationId::get_by_propagation_message_id(message_id, session).await?;
|
||||
|
||||
if let Some(message_by_propagation_id) = message_by_propagation_id {
|
||||
let result = session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
SELECT
|
||||
call_id,
|
||||
message_id,
|
||||
author_id,
|
||||
propagation_message_id,
|
||||
data
|
||||
FROM
|
||||
payphone.messages
|
||||
WHERE
|
||||
call_id = ? AND message_id = ?
|
||||
LIMIT 1;
|
||||
"#,
|
||||
(call_id, u64_to_i64(message_by_propagation_id.message_id)),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
return get_single_row_from_result(&result).await;
|
||||
}
|
||||
}
|
||||
|
||||
get_single_row_from_result(&result).await
|
||||
}
|
||||
|
||||
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||
session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
INSERT INTO payphone.messages (
|
||||
call_id,
|
||||
message_id,
|
||||
author_id,
|
||||
propagation_message_id,
|
||||
data
|
||||
) VALUES (?, ?, ?, ?, ?);
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageByPropagationId {
|
||||
pub call_id: u64,
|
||||
pub message_id: u64,
|
||||
pub propagation_message_id: u64,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for MessageByPropagationId {
|
||||
type Row = (i64, i64, i64);
|
||||
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
call_id: i64_to_u64(row.0),
|
||||
message_id: i64_to_u64(row.1),
|
||||
propagation_message_id: i64_to_u64(row.2),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
u64_to_i64(value.call_id),
|
||||
u64_to_i64(value.message_id),
|
||||
u64_to_i64(value.propagation_message_id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageByPropagationId {
|
||||
pub async fn get_by_propagation_message_id(
|
||||
propagation_message_id: u64,
|
||||
session: &Session,
|
||||
) -> Result<Option<Self>, DatabaseError> {
|
||||
let propagation_message_id = u64_to_i64(propagation_message_id);
|
||||
|
||||
let result = session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
SELECT
|
||||
call_id,
|
||||
message_id,
|
||||
propagation_message_id
|
||||
FROM
|
||||
payphone.messages_by_propagation_id
|
||||
WHERE
|
||||
propagation_message_id = ?
|
||||
LIMIT 1;
|
||||
"#,
|
||||
(propagation_message_id,),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
get_single_row_from_result(&result).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReportResolution {
|
||||
Ban,
|
||||
Warn,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Report {
|
||||
pub id: u64,
|
||||
pub user_id: u64,
|
||||
|
||||
pub message: MessageData,
|
||||
pub replying_to: Option<MessageData>,
|
||||
|
||||
pub created_at: UtcDateTime,
|
||||
pub reviewed_at: Option<UtcDateTime>,
|
||||
|
||||
pub created_by_ids: Vec<u64>,
|
||||
|
||||
pub reviewed_by_id: Option<u64>,
|
||||
pub resolution: Option<ReportResolution>,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for Report {
|
||||
type Row = (
|
||||
i64,
|
||||
i64,
|
||||
MessageData,
|
||||
Option<MessageData>,
|
||||
OffsetDateTime,
|
||||
Option<OffsetDateTime>,
|
||||
Vec<i64>,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
id: i64_to_u64(row.0),
|
||||
user_id: i64_to_u64(row.1),
|
||||
message: row.2,
|
||||
replying_to: row.3,
|
||||
created_at: row.4.to_utc(),
|
||||
reviewed_at: row.5.map(|i| i.to_utc()),
|
||||
created_by_ids: row.6.into_iter().map(i64_to_u64).collect(),
|
||||
reviewed_by_id: row.7.map(i64_to_u64),
|
||||
resolution: row.8.map(|r| match r.as_str() {
|
||||
"ban" => ReportResolution::Ban,
|
||||
"warn" => ReportResolution::Warn,
|
||||
"skip" => ReportResolution::Skip,
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
u64_to_i64(value.id),
|
||||
u64_to_i64(value.user_id),
|
||||
value.message,
|
||||
value.replying_to,
|
||||
value.created_at.to_offset(UtcOffset::UTC),
|
||||
value.reviewed_at.map(|i| i.to_offset(UtcOffset::UTC)),
|
||||
value.created_by_ids.into_iter().map(u64_to_i64).collect(),
|
||||
value.reviewed_by_id.map(u64_to_i64),
|
||||
value.resolution.map(|r| match r {
|
||||
ReportResolution::Ban => "ban".into(),
|
||||
ReportResolution::Warn => "warn".into(),
|
||||
ReportResolution::Skip => "skip".into(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||
session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
INSERT INTO payphone.reports (
|
||||
report_id,
|
||||
user_id,
|
||||
message,
|
||||
replying_to,
|
||||
created_at,
|
||||
reviewed_at,
|
||||
created_by_ids,
|
||||
reviewed_by_id,
|
||||
resolution
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum UserIncidentType {
|
||||
Warning,
|
||||
Ban,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserIncident {
|
||||
pub user_id: u64,
|
||||
pub incident_id: u64,
|
||||
pub report_id: Option<u64>,
|
||||
|
||||
pub r#type: UserIncidentType,
|
||||
|
||||
pub message: Option<MessageData>,
|
||||
|
||||
pub reason: String,
|
||||
|
||||
pub created_by_id: Option<u64>,
|
||||
|
||||
pub created_at: UtcDateTime,
|
||||
pub expires_at: Option<UtcDateTime>,
|
||||
pub duration: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for UserIncident {
|
||||
type Row = (
|
||||
i64,
|
||||
i64,
|
||||
Option<i64>,
|
||||
String,
|
||||
Option<MessageData>,
|
||||
String,
|
||||
Option<i64>,
|
||||
OffsetDateTime,
|
||||
Option<OffsetDateTime>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
user_id: i64_to_u64(row.0),
|
||||
incident_id: i64_to_u64(row.1),
|
||||
report_id: row.2.map(i64_to_u64),
|
||||
r#type: match row.3.as_str() {
|
||||
"warning" => UserIncidentType::Warning,
|
||||
"ban" => UserIncidentType::Ban,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
message: row.4,
|
||||
reason: row.5,
|
||||
created_by_id: row.6.map(i64_to_u64),
|
||||
created_at: row.7.to_utc(),
|
||||
expires_at: row.8.map(|i| i.to_utc()),
|
||||
duration: row.9,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
u64_to_i64(value.user_id),
|
||||
u64_to_i64(value.incident_id),
|
||||
value.report_id.map(u64_to_i64),
|
||||
match value.r#type {
|
||||
UserIncidentType::Warning => "warning".into(),
|
||||
UserIncidentType::Ban => "ban".into(),
|
||||
},
|
||||
value.message,
|
||||
value.reason,
|
||||
value.created_by_id.map(u64_to_i64),
|
||||
value.created_at.to_offset(UtcOffset::UTC),
|
||||
value.expires_at.map(|i| i.to_offset(UtcOffset::UTC)),
|
||||
value.duration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserIncident {
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.expires_at
|
||||
.map(|e| e > UtcDateTime::now())
|
||||
.unwrap_or(true)
|
||||
&& self.r#type == UserIncidentType::Ban
|
||||
}
|
||||
|
||||
pub async fn get_for_user(user_id: u64, session: &Session) -> Result<Vec<Self>, DatabaseError> {
|
||||
let user_id = u64_to_i64(user_id);
|
||||
|
||||
let result = session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
SELECT
|
||||
user_id,
|
||||
incident_id,
|
||||
report_id,
|
||||
type,
|
||||
message,
|
||||
reason,
|
||||
created_by_id,
|
||||
created_at,
|
||||
expires_at,
|
||||
duration
|
||||
FROM
|
||||
payphone.user_incidents
|
||||
WHERE
|
||||
user_id = ?;
|
||||
"#,
|
||||
(user_id,),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?
|
||||
.into_rows_result()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?;
|
||||
|
||||
Ok(result
|
||||
.rows::<<Self as Object>::Row>()
|
||||
.map_err(|_| DatabaseError::ResultNotRows)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| DatabaseError::DeserializationFailed)?
|
||||
.into_iter()
|
||||
.map(Self::from_row)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
|
||||
session
|
||||
.query_unpaged(
|
||||
r#"
|
||||
INSERT INTO payphone.user_incidents (
|
||||
user_id,
|
||||
incident_id,
|
||||
report_id,
|
||||
type,
|
||||
message,
|
||||
reason,
|
||||
created_by_id,
|
||||
created_at,
|
||||
expires_at,
|
||||
duration
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
Self::to_row(self.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
//! Utilities for handling Discord's image hashes.
|
||||
|
||||
use twilight_model::util::ImageHash;
|
||||
|
||||
/// Returns the URL of a user's avatar.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `hash` - The user's avatar hash (if available).
|
||||
/// * `user_id` - The user's ID.
|
||||
/// * `discriminator` - The user's discriminator.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The URL of the user's avatar.
|
||||
pub fn get_avatar_url(hash: Option<ImageHash>, user_id: u64, discriminator: u64) -> String {
|
||||
if let Some(hash) = hash {
|
||||
format!("https://cdn.discordapp.com/avatars/{user_id}/{hash}.webp?size=256")
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
//! A set of utilities to interact with Discord not included in the main framework.
|
||||
//!
|
||||
//! It currently includes a markdown escaper and a utility to get image URLs from Discord's image
|
||||
//! hashes.
|
||||
|
||||
pub mod images;
|
||||
pub mod sanitizing;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
//! Utilities for sanitizing Discord messages (removing markdown from user input).
|
||||
|
||||
/// Escapes Discord markdown in a string.
|
||||
pub trait SanitizeDiscordMarkdown {
|
||||
/// Escapes Discord markdown in a string.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `hard` - If `false`, it'll only escape mentions and pings. Otherwise, it'll also escape
|
||||
/// generic markdown like headers and code blocks.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The sanitized string.
|
||||
fn sanitize(&self, hard: bool) -> String;
|
||||
|
||||
/// Sanitizes mentions in a string.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The sanitized string.
|
||||
fn sanitize_mentions(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T> SanitizeDiscordMarkdown for T
|
||||
@@ -14,7 +31,8 @@ where
|
||||
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' => {
|
||||
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' | '[' | ']' | '/'
|
||||
| '(' | ')' => {
|
||||
escaped.push('\\');
|
||||
escaped.push(ch);
|
||||
}
|
||||
@@ -28,7 +46,13 @@ where
|
||||
};
|
||||
|
||||
// Now block ping attempts — break mentions
|
||||
escaped
|
||||
escaped.sanitize_mentions()
|
||||
}
|
||||
|
||||
fn sanitize_mentions(&self) -> String {
|
||||
let input = self.to_string();
|
||||
|
||||
input
|
||||
.replace("@everyone", "@\u{200B}everyone")
|
||||
.replace("@here", "@\u{200B}here")
|
||||
.replace("<@", "<@\u{200B}") // user mentions
|
||||
|
||||
+53
-34
@@ -13,20 +13,38 @@ use crate::{
|
||||
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
|
||||
OnServerUpdateHandler,
|
||||
},
|
||||
interactions::{components::Component, registry::ComponentRegistry},
|
||||
interactions::{
|
||||
components::Component, registry::ComponentRegistry, runner::InteractionRunner,
|
||||
},
|
||||
runtime::Runtime,
|
||||
},
|
||||
utils::Bye,
|
||||
};
|
||||
|
||||
/// Errors occurred while attempting to run the bot.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeError {
|
||||
#[error("The application's details could not be fetched")]
|
||||
ApplicationFetchingFailed,
|
||||
}
|
||||
|
||||
/// Getter trait for command prefixes.
|
||||
#[async_trait]
|
||||
pub trait IntoPrefixes<T> {
|
||||
pub trait PrefixGetter<T> {
|
||||
/// Get the command prefixes for the given server and user IDs.
|
||||
///
|
||||
/// The getter must never fail. In case an internal process does, it must fall back to default
|
||||
/// prefixes.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `runtime` - The application [`Runtime`].
|
||||
/// * `server_id` - The server ID in which the command, interaction, or event is running.
|
||||
/// * `user_id` - The user ID who triggered the command, interaction, or event.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Vec<String>`] - A list of command prefixes. When matching for commands, all these
|
||||
/// prefixes should be checked against, as they're all valid. In case one should be picked, the
|
||||
/// first one will always be the safest choice.
|
||||
async fn get(
|
||||
&self,
|
||||
runtime: &Runtime<T>,
|
||||
@@ -36,63 +54,44 @@ pub trait IntoPrefixes<T> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for String {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
impl<T> PrefixGetter<T> for String {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for &str {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
impl<T> PrefixGetter<T> for &str {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> IntoPrefixes<S> for [T; N]
|
||||
impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> PrefixGetter<S> for [T; N]
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<S>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
async fn get(&self, _: &Runtime<S>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
self.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F, Fut, T> IntoPrefixes<T> for F
|
||||
impl<F, Fut, T> PrefixGetter<T> for F
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = Vec<String>> + Send,
|
||||
{
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
(self)().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bot<T: Send + Sync = ()> {
|
||||
shard: ShardId,
|
||||
state: Arc<T>,
|
||||
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
|
||||
prefixes: Arc<dyn PrefixGetter<T> + Send + Sync>,
|
||||
intents: Intents,
|
||||
commands: Vec<Arc<dyn Command<T>>>,
|
||||
components: Vec<Arc<dyn Component<T>>>,
|
||||
@@ -108,8 +107,9 @@ pub struct Bot<T: Send + Sync = ()> {
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync> Bot<T> {
|
||||
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
|
||||
pub fn new(state: T, prefixes: impl PrefixGetter<T> + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
shard: ShardId::new(0, 1),
|
||||
state: Arc::new(state),
|
||||
prefixes: Arc::new(prefixes),
|
||||
intents: Intents::empty(),
|
||||
@@ -117,7 +117,7 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
components: Vec::new(),
|
||||
on_ready_handlers: Vec::new(),
|
||||
on_reconnect_handlers: Vec::new(),
|
||||
on_interaction_handlers: Vec::new(),
|
||||
on_interaction_handlers: vec![Arc::new(InteractionRunner)],
|
||||
on_message_handlers: vec![Arc::new(CommandRunner)],
|
||||
on_message_edit_handlers: Vec::new(),
|
||||
on_message_delete_handlers: Vec::new(),
|
||||
@@ -127,6 +127,11 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shard(mut self, shard: ShardId) -> Self {
|
||||
self.shard = shard;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn intent(mut self, intent: Intents) -> Self {
|
||||
self.intents.insert(intent);
|
||||
self
|
||||
@@ -152,6 +157,7 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn on_interaction(mut self, handler: impl OnInteractionHandler<T> + 'static) -> Self {
|
||||
self.on_interaction_handlers.push(Arc::new(handler));
|
||||
self
|
||||
@@ -162,11 +168,13 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn on_message_edit(mut self, handler: impl OnMessageEditHandler<T> + 'static) -> Self {
|
||||
self.on_message_edit_handlers.push(Arc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn on_message_delete(mut self, handler: impl OnMessageDeleteHandler<T> + 'static) -> Self {
|
||||
self.on_message_delete_handlers.push(Arc::new(handler));
|
||||
self
|
||||
@@ -220,7 +228,7 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
commands: bot.commands.clone(),
|
||||
});
|
||||
|
||||
let mut gateway = Shard::new(ShardId::new(0, 1), token.to_string(), bot.intents);
|
||||
let mut gateway = Shard::new(bot.shard, token.to_string(), bot.intents);
|
||||
|
||||
loop {
|
||||
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
|
||||
@@ -308,6 +316,17 @@ impl<T: 'static + Send + Sync> Bot<T> {
|
||||
.bye();
|
||||
}
|
||||
}
|
||||
Event::GuildUpdate(guild) => {
|
||||
for handler in &task_bot.on_server_update_handlers {
|
||||
handler
|
||||
.on_server_update(&guild, task_rt.clone())
|
||||
.await
|
||||
.inspect_err(
|
||||
|e| log::error!("An error occurred while handling on server update for {handler:?}! {e:?}")
|
||||
)
|
||||
.bye();
|
||||
}
|
||||
}
|
||||
Event::GuildDelete(guild) => {
|
||||
for handler in &task_bot.on_server_delete_handlers {
|
||||
handler
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
pub struct RedisCache {
|
||||
|
||||
}
|
||||
@@ -31,6 +31,10 @@ impl FromArguments for String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the entire remaining string as a single argument. If no string is remaining, it
|
||||
/// returns an empty string.
|
||||
///
|
||||
/// Parsing this argument type never fails.
|
||||
pub struct GreedyString(pub String);
|
||||
|
||||
impl FromArguments for GreedyString {
|
||||
|
||||
@@ -82,7 +82,7 @@ where
|
||||
|
||||
for (index, component) in self.components {
|
||||
if index != current_index {
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
current_row = ActionRow::new(&[]);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ where
|
||||
current_row.add(custom_id.clone(), component.clone());
|
||||
}
|
||||
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn content(mut self, content: impl ToString) -> Self {
|
||||
self.content = Some(content.to_string());
|
||||
|
||||
@@ -194,7 +195,7 @@ where
|
||||
|
||||
for (index, component) in self.components {
|
||||
if index != current_index {
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
current_row = ActionRow::new(&[]);
|
||||
}
|
||||
@@ -213,7 +214,7 @@ where
|
||||
current_row.add(custom_id.clone(), component.clone());
|
||||
}
|
||||
|
||||
if current_row.len() > 0 {
|
||||
if !current_row.is_empty() {
|
||||
rows.push(current_row.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
use twilight_http::response::DeserializeBodyError;
|
||||
|
||||
use crate::framework::commands::{arguments::Argument, context::CommandContext};
|
||||
use crate::framework::commands::{
|
||||
arguments::Argument, context::CommandContext, permissions::Permission,
|
||||
};
|
||||
|
||||
pub mod arguments;
|
||||
pub mod context;
|
||||
pub mod messages;
|
||||
pub mod permissions;
|
||||
pub mod runner;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -20,6 +24,15 @@ pub enum CommandError {
|
||||
|
||||
#[error("A database operation failed due to an inconsistent state")]
|
||||
DatabaseInconsistentState,
|
||||
|
||||
#[error("The command was used in DMs, but it can only be used in servers")]
|
||||
ServerCommandRunInDMs,
|
||||
|
||||
#[error(transparent)]
|
||||
DiscordAPIFailed(#[from] twilight_http::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
DiscordDeserializationFailed(#[from] DeserializeBodyError),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -34,6 +47,9 @@ pub trait Command<T>: Send + Sync + Debug {
|
||||
fn arguments(&self) -> &'static [Argument] {
|
||||
&[]
|
||||
}
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<T> + Send + Sync + 'static>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the command name + all its aliases.
|
||||
///
|
||||
@@ -66,7 +82,10 @@ pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option<CommandPa
|
||||
.to_ascii_lowercase()
|
||||
.starts_with(&prefix.to_ascii_lowercase())
|
||||
{
|
||||
let message = message_trimmed_start.split_at(prefix.len()).1;
|
||||
let message = message_trimmed_start
|
||||
.split_at(prefix.len())
|
||||
.1
|
||||
.trim_ascii_start();
|
||||
|
||||
match message.split_once(" ") {
|
||||
Some((name, args)) => Some(CommandParts::new(name, args)),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::framework::commands::{CommandError, context::CommandContext};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Permission<S: Send + Sync + 'static>: Debug {
|
||||
async fn check(&self, ctx: &CommandContext<S>) -> Result<bool, CommandError>;
|
||||
}
|
||||
@@ -23,17 +23,27 @@ impl<T: Send + Sync + 'static> OnMessageHandler<T> for CommandRunner {
|
||||
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
|
||||
.await;
|
||||
|
||||
for prefix in prefixes {
|
||||
if let Some(parts) = get_command_parts(&prefix, &event.content) {
|
||||
for prefix in &prefixes {
|
||||
if let Some(parts) = get_command_parts(prefix, &event.content) {
|
||||
let command = rt.commands.iter().find(|cmd| {
|
||||
cmd.identifiers()
|
||||
.contains(&parts.name.to_ascii_lowercase().as_str())
|
||||
});
|
||||
|
||||
if let Some(command) = command {
|
||||
command
|
||||
.execute(CommandContext::new(event, rt.clone(), &prefix), parts.args)
|
||||
.await?;
|
||||
let ctx = CommandContext::new(event, rt.clone(), &prefixes[0]);
|
||||
|
||||
for permission in command.permissions() {
|
||||
if !permission
|
||||
.check(&ctx)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Permission check failed: {e}"))?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
command.execute(ctx, parts.args).await?;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ impl<T> ActionRow<T> {
|
||||
self.components.push((id, component));
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.components.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.components.is_empty()
|
||||
}
|
||||
|
||||
@@ -39,24 +39,6 @@ impl<T> InteractionContext<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn think(&self) -> Result<(), InteractionError> {
|
||||
self.rt
|
||||
.http
|
||||
.interaction(self.rt.app.id)
|
||||
.create_response(
|
||||
self.event.id,
|
||||
&self.event.token,
|
||||
&InteractionResponse {
|
||||
data: None,
|
||||
kind: InteractionResponseType::DeferredChannelMessageWithSource,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| InteractionError::DeferringFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn original_message(&self) -> Option<OriginalMessage<T>> {
|
||||
self.event.message.as_ref().map(|_| OriginalMessage {
|
||||
interaction_id: self.event.id,
|
||||
|
||||
@@ -3,24 +3,20 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use twilight_model::gateway::payload::incoming::InteractionCreate;
|
||||
|
||||
use crate::{
|
||||
framework::{
|
||||
events::traits::{EventResult, OnInteractionHandler},
|
||||
runtime::Runtime,
|
||||
},
|
||||
state::State,
|
||||
use crate::framework::{
|
||||
events::traits::{EventResult, OnInteractionHandler},
|
||||
runtime::Runtime,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InteractionRunner;
|
||||
|
||||
#[async_trait]
|
||||
impl OnInteractionHandler<State> for InteractionRunner {
|
||||
async fn on_interaction(
|
||||
&self,
|
||||
event: &InteractionCreate,
|
||||
rt: Arc<Runtime<State>>,
|
||||
) -> EventResult {
|
||||
impl<T> OnInteractionHandler<T> for InteractionRunner
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
||||
Ok(rt.registry.execute(event, rt.clone()).await?)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-37
@@ -1,35 +1,35 @@
|
||||
//! A framework for building Discord bots, built with Twilight.
|
||||
//!
|
||||
//!
|
||||
//! Since Twilight is a low-level library, it can be hard to build a bot with it. Payphone's
|
||||
//! framework provides a high-level interface for building Discord bots, with a focus on modularity
|
||||
//! and ease of use. The framework is designed to be extensible, allowing you to add new features
|
||||
//! and commands without modifying the core codebase. It also provides a set of utilities for
|
||||
//! common tasks, such as sending messages, handling events, and managing state.
|
||||
//!
|
||||
//!
|
||||
//! # Creating a Bot
|
||||
//!
|
||||
//!
|
||||
//! Creating a bot with Payphone is as simple as creating a new instance of the `Bot` struct and
|
||||
//! running it. The `Bot` struct is the main entry point for the framework and provides methods for
|
||||
//! running the bot, handling events, and managing state.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! use crate::framework::bot::Bot;
|
||||
//! use crate::state::State;
|
||||
//!
|
||||
//!
|
||||
//! // Your app's state.
|
||||
//! let state = State::new();
|
||||
//!
|
||||
//!
|
||||
//! // Create a new bot instance with the state and a list of prefixes.
|
||||
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
||||
//!
|
||||
//!
|
||||
//! bot.run(&token).await.unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! ### Prefixes
|
||||
//!
|
||||
//!
|
||||
//! The framework supports multiple prefixes per command and allows you to set them dynamically
|
||||
//! based on server and user IDs.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! async fn get_prefixes(
|
||||
//! rt: &Runtime<State>,
|
||||
@@ -37,15 +37,15 @@
|
||||
//! user_id: Option<u64>
|
||||
//! ) -> Vec<String> {
|
||||
//! // The runtime contains your state at [`Runtime::state`].
|
||||
//!
|
||||
//!
|
||||
//! vec!["p.".to_string()]
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! let bot: Bot<State> = Bot::new(state, get_prefixes);
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! Otherwise, you can just pass a static list of prefixes as shown in the previous example.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! // All of these are valid ways to create a bot with a prefix.
|
||||
//! let bot: Bot<State> = Bot::new(state, ["p."]);
|
||||
@@ -53,77 +53,77 @@
|
||||
//! let bot: Bot<State> = Bot::new(state, "p.".to_string());
|
||||
//! let bot: Bot<State> = Bot::new(state, ["p.".to_string()]);
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! Setting no prefix will make the bot raise errors when receiving messages.
|
||||
//!
|
||||
//!
|
||||
//! ## Adding Commands
|
||||
//!
|
||||
//!
|
||||
//! Creating a command is quite simple. You just need to implement the `Command` trait and register
|
||||
//! it with the bot.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! use crate::framework::commands::{Command, CommandError, context::CommandContext};
|
||||
//!
|
||||
//!
|
||||
//! pub struct MyCommand;
|
||||
//!
|
||||
//!
|
||||
//! #[async_trait::async_trait]
|
||||
//! impl Command<State> for MyCommand {
|
||||
//! fn name(&self) -> &'static str {
|
||||
//! "my_command"
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! async fn execute(&self, ctx: CommandContext<'_, T>, _args: &str) -> Result<(), CommandError> {
|
||||
//! // Your command logic here.
|
||||
//! ctx.send()
|
||||
//! .content("Hello, world!")
|
||||
//! .await
|
||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
//!
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! Then you just need to register the command with the bot:
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! let bot = Bot::new(state, ["p."]).command(MyCommand);
|
||||
//!
|
||||
//!
|
||||
//! bot.run(&token).await.unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! Log into your Discord account and invite the bot to your server. Then you can test the command
|
||||
//! by sending `p.my_command` in a channel the bot has access to.
|
||||
//!
|
||||
//!
|
||||
//! ### Configuring Commands Further
|
||||
//!
|
||||
//!
|
||||
//! Commands can be configured with additional metadata, such as aliases, descriptions, and
|
||||
//! arguments. The following is a full example of a command with all these features:
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! use crate::framework::commands::{
|
||||
//! Command, CommandError, context::CommandContext, arguments::NeedyGreedyString
|
||||
//! };
|
||||
//! use crate::state::State;
|
||||
//!
|
||||
//!
|
||||
//! pub struct HelloCommand;
|
||||
//!
|
||||
//!
|
||||
//! #[async_trait::async_trait]
|
||||
//! impl Command<State> for HelloCommand {
|
||||
//! fn name(&self) -> &'static str {
|
||||
//! "hello"
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! /// A list of alternative names for the command.
|
||||
//! fn aliases(&self) -> &'static [&'static str] {
|
||||
//! &["hola", "hey", "hi", "konnichiwa"]
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! /// A description of the command and its functionality.
|
||||
//! fn description(&self) -> &'static str {
|
||||
//! "This command responds with a greeting."
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! /// A list of arguments the command accepts. These are solely for documentation purposes,
|
||||
//! /// they won't affect the parsing you do in the command's `execute` method.
|
||||
//! fn arguments(&self) -> &'static [Argument] {
|
||||
@@ -135,7 +135,7 @@
|
||||
//! },
|
||||
//! ]
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! async fn execute(
|
||||
//! &self,
|
||||
//! ctx: CommandContext<'_, T>,
|
||||
@@ -146,18 +146,19 @@
|
||||
//! args
|
||||
//! )
|
||||
//! .unwrap_or(NeedyGreedyString("???".to_string()));
|
||||
//!
|
||||
//!
|
||||
//! ctx.send()
|
||||
//! .content(format!("Hello, {}!", name.0))
|
||||
//! .await
|
||||
//! .map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
//!
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod bot;
|
||||
pub mod cache;
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub mod interactions;
|
||||
|
||||
@@ -1,26 +1,76 @@
|
||||
//! Runtime app state of the shard.
|
||||
//!
|
||||
//! The [`Runtime`] is an abstraction over the application's state and provides access to various
|
||||
//! components, such as the HTTP client, application information, and command registry. This was
|
||||
//! done because there's a lot of data the application depends on to execute, and passing all those
|
||||
//! values around individually was cumbersome.
|
||||
//!
|
||||
//! The [`Runtime`] includes the following data:
|
||||
//!
|
||||
//! - The HTTP API client.
|
||||
//! - The application's details, including the user profile.
|
||||
//! - The component registry.
|
||||
//! - The prefix getter.
|
||||
//! - The list of available commands.
|
||||
//! - The app state.
|
||||
//!
|
||||
//! The [`Runtime`] is always passed around as [`Arc<Runtime>`], so it can be easily shared between
|
||||
//! different parts of the application.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_http::Client;
|
||||
use twilight_model::oauth::Application;
|
||||
|
||||
use crate::framework::{
|
||||
bot::IntoPrefixes, commands::Command, interactions::registry::ComponentRegistry,
|
||||
bot::PrefixGetter, commands::Command, interactions::registry::ComponentRegistry,
|
||||
};
|
||||
|
||||
/// The application's runtime tools.
|
||||
pub struct Runtime<T> {
|
||||
/// The HTTP client used for making requests to the HTTP API.
|
||||
pub http: Client,
|
||||
|
||||
/// Details about the application.
|
||||
pub app: Application,
|
||||
|
||||
/// The component registry.
|
||||
pub registry: ComponentRegistry<T>,
|
||||
pub prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync + 'static>,
|
||||
|
||||
/// The command prefixes getter.
|
||||
pub prefixes: Arc<dyn PrefixGetter<T> + Send + Sync + 'static>,
|
||||
|
||||
/// The commands available in the application.
|
||||
pub commands: Vec<Arc<dyn Command<T>>>,
|
||||
|
||||
/// The application state.
|
||||
pub state: Arc<T>,
|
||||
}
|
||||
|
||||
impl<T> Runtime<T> {
|
||||
/// Get the command prefixes set for the server and user ID.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `server_id` - The ID of the server in which the command, interaction, or event is
|
||||
/// running. When this is [`None`] it means the prefix should be returned for the user's DM
|
||||
/// channel.
|
||||
/// * `user_id` - The user who triggered the event.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Vec<String>`] - A list of prefixes, all which should be taken into account. It's safe to
|
||||
/// default to the first item of the list when a single prefix should be used (e.g. to display
|
||||
/// it to the user).
|
||||
pub async fn get_prefixes(&self, server_id: Option<u64>, user_id: Option<u64>) -> Vec<String> {
|
||||
self.prefixes.clone().get(self, server_id, user_id).await
|
||||
}
|
||||
|
||||
/// Get a command by its name or alias.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `name` - The command's name or alias, without prefix.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Option<Arc<dyn Command<T>>>`] - The command if found, otherwise `None`.
|
||||
pub fn get_command(&self, name: &str) -> Option<Arc<dyn Command<T>>> {
|
||||
self.commands
|
||||
.iter()
|
||||
|
||||
@@ -26,6 +26,7 @@ impl Button<State> for HelpGoBackButton {
|
||||
.component(HelpWebsiteLinkButton, 1)
|
||||
.component(HelpSupportLinkButton, 1)
|
||||
.component(HelpDonateLinkButton, 1)
|
||||
.component(CodeOfConductLinkButton, 1)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}"))
|
||||
.map_err(
|
||||
@@ -77,3 +78,17 @@ impl Button<State> for HelpDonateLinkButton {
|
||||
Some("https://ko-fi.com/payphonedev")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CodeOfConductLinkButton;
|
||||
|
||||
#[async_trait]
|
||||
impl Button<State> for CodeOfConductLinkButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Code of Conduct")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://payphone.lat/code-of-conduct")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ use twilight_model::id::Id;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel},
|
||||
framework::commands::{Command, CommandError, context::CommandContext},
|
||||
discord::sanitizing::SanitizeDiscordMarkdown,
|
||||
framework::commands::{
|
||||
Command, CommandError, context::CommandContext, permissions::Permission,
|
||||
},
|
||||
groups::calls::{get_display_profile, permissions::NotBannedPermission},
|
||||
state::State,
|
||||
utils::Bye,
|
||||
};
|
||||
@@ -36,6 +40,10 @@ impl Command<State> for CallCommand {
|
||||
&["c"]
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<State> + Send + Sync + 'static>> {
|
||||
vec![Box::new(NotBannedPermission)]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
@@ -109,6 +117,112 @@ impl Command<State> for CallCommand {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SkipCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for SkipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"skip"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &'static [&'static str] {
|
||||
&["s", "next"]
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<State> + Send + Sync + 'static>> {
|
||||
vec![Box::new(NotBannedPermission)]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
_args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let channel = Channel::get_or_create_by_id(
|
||||
ctx.event.channel_id.get(),
|
||||
&ctx.rt.state.db,
|
||||
&ctx.rt.http,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to get or create channel: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
if channel.call_id.is_none() {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if channel.is_searching {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"☎️ You're already searching for a call! Use `{}hangup` to cancel the search.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let call = Call::get_by_id(channel.call_id.unwrap(), &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
.ok_or(CommandError::DatabaseInconsistentState)?;
|
||||
|
||||
call.hangup(&ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
let other_channel_id = call.other_party_channel_id(ctx.event.channel_id.get());
|
||||
|
||||
ctx.rt
|
||||
.http
|
||||
.create_message(Id::new(other_channel_id))
|
||||
.content("☎️ The other party hung up the phone.")
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to send message to other party: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
let call = Call::start(&channel, &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to start (skipped) call: {e:?}");
|
||||
})
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
if let Some(call) = call {
|
||||
ctx.send().content("☎️ Call connected. Say hi!").await.bye();
|
||||
ctx.rt
|
||||
.http
|
||||
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
||||
.content("☎️ Call connected. Say hi!")
|
||||
.await
|
||||
.bye();
|
||||
} else {
|
||||
ctx.send()
|
||||
.content("☎️ Skipped! Searching for a call...")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HangupCommand;
|
||||
|
||||
@@ -122,6 +236,10 @@ impl Command<State> for HangupCommand {
|
||||
&["h", "hang"]
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<State> + Send + Sync + 'static>> {
|
||||
vec![Box::new(NotBannedPermission)]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
@@ -202,3 +320,101 @@ impl Command<State> for HangupCommand {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FriendRequestCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for FriendRequestCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"friendreq"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &'static [&'static str] {
|
||||
&["f", "fr", "friendrequest"]
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Shows your username to the other party and asks them to send you a friend request on Discord."
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<Box<dyn Permission<State> + Send + Sync + 'static>> {
|
||||
vec![Box::new(NotBannedPermission)]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
_args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get channel: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
let Some(channel) = channel else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let call = if let Some(call_id) = channel.call_id {
|
||||
Call::get_by_id(call_id, &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?
|
||||
} else {
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"❌ You're not on a call! Use `{}call` to start one.",
|
||||
ctx.prefix
|
||||
))
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(call) = call else {
|
||||
ctx.send()
|
||||
.content("❌ Oops! We could not find that call.")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
return Err(CommandError::DatabaseInconsistentState);
|
||||
};
|
||||
|
||||
let other_channel_id = call.other_party_channel_id(channel.id);
|
||||
|
||||
let profile = get_display_profile(ctx.event);
|
||||
|
||||
ctx
|
||||
.rt
|
||||
.http
|
||||
.create_message(
|
||||
Id::new(other_channel_id)
|
||||
)
|
||||
.content(
|
||||
&format!(
|
||||
"📬 **{}** wants you to send them a friend request on Discord! Their username is `{}`.",
|
||||
profile.name.sanitize(false),
|
||||
ctx.event.author.name.sanitize(false)
|
||||
)
|
||||
)
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
ctx.send()
|
||||
.content("📬 Asked the other party to send you a friend request!")
|
||||
.await
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use crate::{framework::interactions::components::Button, state::State};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CodeOfConductButton;
|
||||
|
||||
impl Button<State> for CodeOfConductButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Code of Conduct")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://payphone.lat/code-of-conduct")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NeedHelpButton;
|
||||
|
||||
impl Button<State> for NeedHelpButton {
|
||||
fn label(&self) -> Option<&'static str> {
|
||||
Some("Need Help?")
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<&'static str> {
|
||||
Some("https://discord.gg/9daSsrfTmE")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_model::{channel::message::Embed, oauth::Application};
|
||||
use twilight_util::builder::embed::{
|
||||
EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::{COLOR_RED, COLOR_YELLOW},
|
||||
database::models::Message,
|
||||
discord::images,
|
||||
groups::calls::moderation::Rule,
|
||||
utils::ellipse,
|
||||
};
|
||||
|
||||
pub fn first_warning_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Warning")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. Read the code of conduct to prevent getting banned.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_YELLOW)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("This is a warning because it's your first offense. Breaking the rules again will result in a ban."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn repeat_warning_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Long Time no See!")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. It's not your first offense, yet you've gone for a long time with a good record so we're making this a warning instead. In case you've forgotten the rules, consider this a reminder to re-read them.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_YELLOW)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in a ban."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn first_ban_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Banned")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your first offense, and as such, you are being issued a ban. This ban will last for 24 hours, next one will be permanent.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_RED)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
))
|
||||
.unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("Breaking the rules again will result in being permanently banned."))
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn final_ban_embed(
|
||||
app: &Application,
|
||||
message: &Message,
|
||||
rules_broken: Vec<Arc<dyn Rule>>,
|
||||
) -> Embed {
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(app.name.clone())
|
||||
.icon_url(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
)
|
||||
.title("Permanently Banned")
|
||||
.description("You broke [Payphone's code of conduct](https://payphone.lat/code-of-conduct) during a call. This is your final offense, and as such, you are being issued a permanent ban.")
|
||||
.field(EmbedFieldBuilder::new("What message broke the rules?", ellipse(message.data.content.clone(), 500)))
|
||||
.field(EmbedFieldBuilder::new(
|
||||
"What rules did you break?",
|
||||
rules_broken
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
.color(COLOR_RED)
|
||||
.thumbnail(
|
||||
ImageSource::url(
|
||||
images::get_avatar_url(
|
||||
app.icon,
|
||||
app.bot.as_ref().unwrap().id.get(),
|
||||
app.bot.as_ref().unwrap().discriminator as u64,
|
||||
)
|
||||
).unwrap()
|
||||
)
|
||||
.footer(EmbedFooterBuilder::new("You broke the rules 3 times."))
|
||||
.build()
|
||||
}
|
||||
+343
-51
@@ -1,25 +1,91 @@
|
||||
use std::sync::Arc;
|
||||
use std::{error::Error, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
|
||||
use twilight_validate::request::webhook_username;
|
||||
use itertools::Itertools;
|
||||
use twilight_model::{
|
||||
gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate},
|
||||
id::Id,
|
||||
};
|
||||
use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource};
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel},
|
||||
discord::images,
|
||||
constants::COLOR_WHITE,
|
||||
database::models::{Call, Channel, Message, User, UserIncident},
|
||||
discord::sanitizing::SanitizeDiscordMarkdown,
|
||||
framework::{
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
events::traits::{
|
||||
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
|
||||
},
|
||||
interactions::components::ActionRow,
|
||||
runtime::Runtime,
|
||||
},
|
||||
groups::calls::moderation::escape_message,
|
||||
groups::calls::{
|
||||
components::{CodeOfConductButton, NeedHelpButton},
|
||||
embeds, get_display_profile,
|
||||
moderation::{Action, Rule, escape_message},
|
||||
},
|
||||
state::State,
|
||||
utils::{ellipse, i64_to_u64},
|
||||
};
|
||||
|
||||
async fn penalize_user(
|
||||
user: User,
|
||||
message: Message,
|
||||
broken_rules: Vec<Arc<dyn Rule>>,
|
||||
rt: &Runtime<State>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let reason = format!(
|
||||
"You broke the following rules during a call:\n{}",
|
||||
broken_rules
|
||||
.iter()
|
||||
.map(|rule| format!("- {}", rule.name()))
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
let new_incident = user
|
||||
.incident(
|
||||
reason.clone(),
|
||||
Some(message.data.clone()),
|
||||
Some(user.id),
|
||||
&rt.state.db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let dm_channel = rt
|
||||
.http
|
||||
.create_private_channel(Id::new(user.id))
|
||||
.await?
|
||||
.model()
|
||||
.await?;
|
||||
|
||||
let embed = if new_incident.is_first_incident {
|
||||
embeds::first_warning_embed(&rt.app, &message, broken_rules)
|
||||
} else if new_incident.counting_incidents == 0 {
|
||||
embeds::repeat_warning_embed(&rt.app, &message, broken_rules)
|
||||
} else if new_incident.counting_incidents == 1 {
|
||||
embeds::first_ban_embed(&rt.app, &message, broken_rules)
|
||||
} else {
|
||||
embeds::final_ban_embed(&rt.app, &message, broken_rules)
|
||||
};
|
||||
|
||||
rt.http
|
||||
.create_message(dm_channel.id)
|
||||
.embeds(&[embed])
|
||||
.components(&[ActionRow::new(&[
|
||||
(None, Arc::new(CodeOfConductButton)),
|
||||
(None, Arc::new(NeedHelpButton)),
|
||||
])
|
||||
.build()])
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MessageRedirector;
|
||||
pub struct CallMessageHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMessageHandler<State> for MessageRedirector {
|
||||
impl OnMessageHandler<State> for CallMessageHandler {
|
||||
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
if event.author.bot || event.author.system.unwrap_or(false) {
|
||||
return Ok(());
|
||||
@@ -75,6 +141,16 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let user_incidents = UserIncident::get_for_user(event.author.id.get(), &rt.state.db)
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|incident| incident.is_active())
|
||||
.count();
|
||||
|
||||
if user_incidents > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(opposite_channel) = Channel::get_by_id(
|
||||
call.other_party_channel_id(event.channel_id.get()),
|
||||
&rt.state.db,
|
||||
@@ -99,59 +175,275 @@ impl OnMessageHandler<State> for MessageRedirector {
|
||||
|
||||
let profile = get_display_profile(event);
|
||||
|
||||
rt.http
|
||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||
.allowed_mentions(None)
|
||||
.username(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&escape_message(event))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||
let (content, rules) = escape_message(event);
|
||||
|
||||
let message: Message;
|
||||
|
||||
let replying_to = if let Some(reference) = &event.reference {
|
||||
if let Some(replying_to_message_id) = reference.message_id {
|
||||
Message::get_by_call_and_message_id(
|
||||
call.id,
|
||||
opposite_channel.id
|
||||
replying_to_message_id.get(),
|
||||
&rt.state.db,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let replying_to_id = replying_to.as_ref().map(|i| i.message_id);
|
||||
|
||||
if rules
|
||||
.iter()
|
||||
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||
{
|
||||
rt.http
|
||||
.create_message(event.channel_id)
|
||||
.content(
|
||||
"❌ Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
|
||||
)
|
||||
.await?;
|
||||
|
||||
rt.http
|
||||
.create_message(Id::new(opposite_channel.id))
|
||||
.content(&format!(
|
||||
"❌ **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.",
|
||||
profile.name.sanitize(false)
|
||||
))
|
||||
.await?;
|
||||
|
||||
message = Message::build()
|
||||
.call_id(call.id)
|
||||
.message(event.0.clone())
|
||||
.profile(profile.clone())
|
||||
.is_censored(!rules.is_empty())
|
||||
.replying_to_message_id(replying_to_id)
|
||||
.build()
|
||||
.expect(
|
||||
"The message builder was missing either the call ID, the message, or the profile",
|
||||
);
|
||||
})?;
|
||||
} else {
|
||||
let mut embeds = vec![];
|
||||
|
||||
if let Some(replying_to) = replying_to {
|
||||
embeds.push(
|
||||
EmbedBuilder::new()
|
||||
.author(
|
||||
EmbedAuthorBuilder::new(replying_to.data.author_name).icon_url(
|
||||
ImageSource::url(replying_to.data.author_avatar_url).unwrap(),
|
||||
),
|
||||
)
|
||||
.color(COLOR_WHITE)
|
||||
.description(ellipse(replying_to.data.content, 100))
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
let message_sent = rt
|
||||
.http
|
||||
.execute_webhook(Id::new(webhook_id), &webhook_token)
|
||||
.allowed_mentions(None)
|
||||
.username(&profile.name)
|
||||
.avatar_url(&profile.avatar_url)
|
||||
.content(&content.sanitize_mentions())
|
||||
.embeds(&embeds)
|
||||
.wait()
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to send message to webhook for call {} in channel {}: {e:?}",
|
||||
call.id,
|
||||
opposite_channel.id
|
||||
);
|
||||
})?
|
||||
.model()
|
||||
.await?;
|
||||
|
||||
message = Message::build()
|
||||
.call_id(call.id)
|
||||
.message(event.0.clone())
|
||||
.profile(profile.clone())
|
||||
.is_censored(!rules.is_empty())
|
||||
.replying_to_message_id(replying_to_id)
|
||||
.propagation_message_id(message_sent.id.get())
|
||||
.propagation_channel_id(opposite_channel.id)
|
||||
.build()
|
||||
.expect(
|
||||
"The message builder was missing either the call ID, the message, or the profile",
|
||||
);
|
||||
}
|
||||
|
||||
message.insert(&rt.state.db).await?;
|
||||
|
||||
if rules
|
||||
.iter()
|
||||
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||
{
|
||||
let user = User::get_by_id(event.author.id.get(), &rt.state.db).await?;
|
||||
|
||||
let user = if let Some(user) = user {
|
||||
user
|
||||
} else {
|
||||
let user: User = event.author.clone().into();
|
||||
|
||||
user.insert(&rt.state.db).await?;
|
||||
|
||||
user
|
||||
};
|
||||
|
||||
penalize_user(user, message, rules, &rt).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayProfile {
|
||||
pub name: String,
|
||||
pub avatar_url: String,
|
||||
#[derive(Debug)]
|
||||
pub struct CallMessageEditHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMessageEditHandler<State> for CallMessageEditHandler {
|
||||
async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc<Runtime<State>>) -> EventResult {
|
||||
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let call = if let Some(call_id) = channel.call_id {
|
||||
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
|
||||
if !call.is_active() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(mut message) =
|
||||
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
|
||||
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(propagation_message_id) = message.data.propagation_message_id {
|
||||
let (content, rules) = escape_message(event);
|
||||
|
||||
if rules
|
||||
.iter()
|
||||
.any(|rule| rule.action() == &Action::BlockAndPenalty)
|
||||
{
|
||||
rt.http
|
||||
.create_message(event.channel_id)
|
||||
.content(
|
||||
"❌ Your message's edit has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
|
||||
)
|
||||
.reply(event.id)
|
||||
.await?;
|
||||
|
||||
let user = User::get_by_id(event.author.id.get(), &rt.state.db).await?;
|
||||
|
||||
if let Some(user) = user {
|
||||
penalize_user(user, message, rules, &rt).await?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some((webhook_id, webhook_token)) = opposite_channel
|
||||
.webhook_id
|
||||
.zip(opposite_channel.webhook_token)
|
||||
{
|
||||
rt.http
|
||||
.update_webhook_message(
|
||||
Id::new(webhook_id),
|
||||
&webhook_token,
|
||||
Id::new(i64_to_u64(propagation_message_id)),
|
||||
)
|
||||
.content(Some(&content))
|
||||
.await?;
|
||||
|
||||
message.data.content = content;
|
||||
message.data.is_edited = true;
|
||||
message.data.is_censored = !rules.is_empty();
|
||||
|
||||
message.insert(&rt.state.db).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
||||
let member = event.member.clone();
|
||||
#[derive(Debug)]
|
||||
pub struct CallMessageDeleteHandler;
|
||||
|
||||
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
||||
if webhook_username(nick).is_err() {
|
||||
"Invalid Nickname".to_string()
|
||||
#[async_trait]
|
||||
impl OnMessageDeleteHandler<State> for CallMessageDeleteHandler {
|
||||
async fn on_message_delete(
|
||||
&self,
|
||||
event: &MessageDelete,
|
||||
rt: Arc<Runtime<State>>,
|
||||
) -> EventResult {
|
||||
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let call = if let Some(call_id) = channel.call_id {
|
||||
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
|
||||
if !call.is_active() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
nick.clone()
|
||||
}
|
||||
} else if let Some(name) = &event.author.global_name {
|
||||
if webhook_username(name).is_err() {
|
||||
"Invalid Global Name".to_string()
|
||||
} else {
|
||||
name.clone()
|
||||
}
|
||||
} else if webhook_username(&event.author.name).is_err() {
|
||||
"Invalid Username".to_string()
|
||||
} else {
|
||||
event.author.name.clone()
|
||||
};
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let avatar_hash = member
|
||||
.and_then(|inner| inner.avatar)
|
||||
.or(event.author.avatar);
|
||||
let avatar_url = images::get_avatar_url(
|
||||
avatar_hash,
|
||||
event.author.id.get(),
|
||||
event.author.discriminator as u64,
|
||||
);
|
||||
let Some(mut message) =
|
||||
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
DisplayProfile { name, avatar_url }
|
||||
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
|
||||
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(propagation_message_id) = message.data.propagation_message_id {
|
||||
if let Some((webhook_id, webhook_token)) = opposite_channel
|
||||
.webhook_id
|
||||
.zip(opposite_channel.webhook_token)
|
||||
{
|
||||
rt.http
|
||||
.delete_webhook_message(
|
||||
Id::new(webhook_id),
|
||||
&webhook_token,
|
||||
Id::new(i64_to_u64(propagation_message_id)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
message.data.is_deleted = true;
|
||||
|
||||
message.insert(&rt.state.db).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,4 +85,12 @@ faping
|
||||
masturbate
|
||||
masturbating
|
||||
masterbating
|
||||
masterbaiting
|
||||
masterbaiting
|
||||
horny
|
||||
hrny
|
||||
whore
|
||||
whre
|
||||
tickle your pickle
|
||||
tickle yr pickle
|
||||
tickle his pickle
|
||||
tickle my pickle
|
||||
@@ -44,4 +44,6 @@ get the fuck out
|
||||
pedo
|
||||
pdf
|
||||
pdfile
|
||||
blackie
|
||||
blackie
|
||||
motherfucker
|
||||
mf
|
||||
@@ -8,4 +8,9 @@ fuh
|
||||
fuck you
|
||||
f u
|
||||
f you
|
||||
fuck u
|
||||
fuck u
|
||||
damn
|
||||
dmn
|
||||
shit
|
||||
sht
|
||||
ass
|
||||
@@ -0,0 +1 @@
|
||||
lkrkfejnkjrngkjngrkjgenkgejnrkjgnknj
|
||||
@@ -1,3 +1,59 @@
|
||||
//! Call commands and events.
|
||||
|
||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||
use twilight_validate::request::webhook_username;
|
||||
|
||||
use crate::discord::images;
|
||||
|
||||
pub mod commands;
|
||||
pub(super) mod components;
|
||||
pub(super) mod embeds;
|
||||
pub mod events;
|
||||
pub(super) mod moderation;
|
||||
pub(super) mod permissions;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DisplayProfile {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub avatar_url: String,
|
||||
pub mask: Option<String>,
|
||||
}
|
||||
|
||||
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
|
||||
let member = event.member.clone();
|
||||
|
||||
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
|
||||
if webhook_username(nick).is_err() {
|
||||
"Invalid Nickname".to_string()
|
||||
} else {
|
||||
nick.clone()
|
||||
}
|
||||
} else if let Some(name) = &event.author.global_name {
|
||||
if webhook_username(name).is_err() {
|
||||
"Invalid Global Name".to_string()
|
||||
} else {
|
||||
name.clone()
|
||||
}
|
||||
} else if webhook_username(&event.author.name).is_err() {
|
||||
"Invalid Username".to_string()
|
||||
} else {
|
||||
event.author.name.clone()
|
||||
};
|
||||
|
||||
let avatar_hash = member
|
||||
.and_then(|inner| inner.avatar)
|
||||
.or(event.author.avatar);
|
||||
let avatar_url = images::get_avatar_url(
|
||||
avatar_hash,
|
||||
event.author.id.get(),
|
||||
event.author.discriminator as u64,
|
||||
);
|
||||
|
||||
DisplayProfile {
|
||||
id: event.author.id.get(),
|
||||
name,
|
||||
avatar_url,
|
||||
mask: None,
|
||||
}
|
||||
}
|
||||
|
||||
+399
-46
@@ -13,26 +13,97 @@
|
||||
//! | 5 | Suggestive language | "slap your ass", "sexy" | Block immediately + penalty |
|
||||
//! | 6 | Advertising | "discord.gg/payphone" | Block immediately + penalty |
|
||||
//! | 7 | Sensitive topics and triggering content | "blood", "knife", "suicide method" | Block immediately + penalty |
|
||||
//! | 8 | Non-english chats | | Block immediately + penalty |
|
||||
//! | 8 | Non-english chats | | Soft censor + warn |
|
||||
//! | 9 | E-dating | "let me hit" | Block immediately + penalty |
|
||||
//! | 10 | Mild swear words | "damn", "shit", "ass" | Soft censor + warn |
|
||||
//! | 11 | Casual insults and teasing | "stupid", "dumb" | Soft censor + warn |
|
||||
//! | 12 | Links | "https://google.com/" | Soft censor + warn |
|
||||
//! | 13 | Spam and flooding | | Soft censor + warn |
|
||||
//! | 14 | Normal conversations | "hey", "lol", "cool" | Allowed freely |
|
||||
//!
|
||||
//! # Rule-breaking Detection
|
||||
//!
|
||||
//! Rule-breaking detection is implemented individually per rule via the [`Rule`] trait. This trait
|
||||
//! defines a set of methods for defining and detecting rules, together with a set of utilities
|
||||
//! used by these detectors to increase their effectiveness.
|
||||
//!
|
||||
//! Some rules, like the advertising and the no links ones, implement link searching via
|
||||
//! [`linkify`], a crate to precisely identify and extracts links and another types of PII from
|
||||
//! strings.
|
||||
//!
|
||||
//! The rest of the rules mostly implement the same normalization algorithm, which normalizes text
|
||||
//! into a consistent text and whitespaces format that can be easily parsed searching for blocked
|
||||
//! keywords.
|
||||
//!
|
||||
//! The normalization process is made of the following steps:
|
||||
//!
|
||||
//! 1. Replace common leet code with their textual representation (e.g. 3 -> E, 4 -> A).
|
||||
//! 2. Pass the message through [`unidecode`], which normalizes all messages into their English
|
||||
//! ASCII representation. This also unaccents all text, so `café` becomes `cafe`.
|
||||
//! 3. Remove non-alphanumeric characters remaining.
|
||||
//! 4. Pass the message through each of the rules implementations' detection algorithms.
|
||||
//!
|
||||
//! Each rule can have its own list of blocked keywords, which you can find in text files under
|
||||
//! `src/groups/calls/lists/*.txt`. Each new line in those files represents a different keyword or
|
||||
//! phrase that is blocked, except for comments which are preceeded with `//` at the start of the
|
||||
//! line.
|
||||
//!
|
||||
//! Once that file is parsed, keywords are converted into a regex pattern that can be used to
|
||||
//! accurately detect blocked keywords. The regex is generated as follows:
|
||||
//!
|
||||
//! 1. For each keyword, remove all whitespaces and split it into characters.
|
||||
//! 2. To each character, append a `+\s*`, except for the last character which only gets a `+`
|
||||
//! appended.
|
||||
//! 3. After joining those characters and their suffixes together, put them between `(\b` and
|
||||
//! `\b)`.
|
||||
//! 4. Chain all parenthesized sub-regexes with `|`.
|
||||
//!
|
||||
//! This generates a quite effective regex, which for `hello` and `good bye` would look as follows:
|
||||
//!
|
||||
//! ```regex
|
||||
//! (\bh+\s*e+\s*l+\s*l+\s*o+\b)|(\bg+\s*o+\s*o+\s*d+\s*\b\s*b+\s*y+\s*e+\b)
|
||||
//! ```
|
||||
//!
|
||||
//! That regex can be matched against the previously normalized message to find all instances of
|
||||
//! censored keywords and phrases.
|
||||
//!
|
||||
//! Note that in some cases, people may attempt to *remove* letters or write variations of the word
|
||||
//! to bypass the censorship. In these cases, the lists should try to contain as many alternatives
|
||||
//! as possible. E.g. for "shit", the list could also include "sht".
|
||||
//!
|
||||
//! This algorithm effectively blocks a lot of attempts to bypass censorship. For example, for the
|
||||
//! word "shit", a malicious user could attempt to say:
|
||||
//!
|
||||
//! - "shit"
|
||||
//! - "sh7"
|
||||
//! - "sh1t"
|
||||
//! - "sshhiitt"
|
||||
//! - "s'h.ii i1 i1i!1.t t?"
|
||||
//! - "Sh.í1.7"
|
||||
//!
|
||||
//! All of which would be detected.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use linkify::LinkFinder;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use twilight_model::gateway::payload::incoming::MessageCreate;
|
||||
use twilight_model::channel::Message;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unidecode::unidecode;
|
||||
|
||||
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
||||
|
||||
pub fn escape_message(event: &MessageCreate) -> String {
|
||||
/// Escapes a call message by converting all mentions into user-readable ones, breaking @everyone
|
||||
/// and @here pings, and censoring by rules.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `event`: The incoming message event to escape.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`(String, Vec<Arc<dyn Rule>>)`] - The escaped and censored message, and a vector of the rules
|
||||
/// broken.
|
||||
pub fn escape_message(event: &Message) -> (String, Vec<Arc<dyn Rule>>) {
|
||||
let mut message = event.content.to_string();
|
||||
|
||||
for mention in &event.mentions {
|
||||
@@ -53,34 +124,42 @@ pub fn escape_message(event: &MessageCreate) -> String {
|
||||
);
|
||||
}
|
||||
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.url_must_have_scheme(false);
|
||||
|
||||
for link in finder.links(&message.clone()) {
|
||||
let url = link.as_str();
|
||||
|
||||
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||
}
|
||||
|
||||
let (censored, rules) = moderate_message(&message);
|
||||
|
||||
log::debug!("Escaped and normalized message: {censored:?}");
|
||||
|
||||
censored
|
||||
(censored, rules)
|
||||
}
|
||||
|
||||
/// A normalized message and its mappings to the original message's individual grapheme clusters.
|
||||
#[derive(Debug)]
|
||||
pub struct NormalizedMessage {
|
||||
/// The normalized message content.
|
||||
message: String,
|
||||
|
||||
/// Mappings from normalized start indices to original start indices (indices are grapheme
|
||||
/// indices).
|
||||
/// Mappings from normalized grapheme indices to the original message's grapheme indices
|
||||
/// (indices are grapheme indices). Each item in the vector corresponds to its character in the
|
||||
/// normalized string by index.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// h3lll0 normalized to hello would have a mapping of `vec![0, 1, 2, 3, 5]`. The extra L was
|
||||
/// removed in the normalization so it doesn't appear in the indexes list.
|
||||
mappings: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Generates a normalized message out of the original message string.
|
||||
///
|
||||
/// TODO: Replace * in the message with wildcards to match censored words.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `message`: The original message to normalize.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`NormalizedMessage`] - The normalized message and its mappings to the original string.
|
||||
pub fn normalize(message: String) -> NormalizedMessage {
|
||||
let replacements: HashMap<char, char> = [
|
||||
('$', 's'),
|
||||
('@', 'a'),
|
||||
('0', 'o'),
|
||||
('1', 'i'),
|
||||
@@ -108,6 +187,7 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
||||
ch = replacement.to_string();
|
||||
}
|
||||
|
||||
// Each character is unidecoded individually to be able to keep track of what went where.
|
||||
let unidecoded = unidecode(&ch);
|
||||
|
||||
for char in unidecoded.chars() {
|
||||
@@ -127,6 +207,23 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the regex described in the [`self`] module.
|
||||
///
|
||||
/// 1. Splits the phrase into individual characters.
|
||||
/// 2. Joins them with `+\s*`, which matches the previous character any amount of time and any
|
||||
/// whitespaces after it.
|
||||
/// 3. Appends `+` to the last character, which matches it any amount of times.
|
||||
/// 4. Wraps the whole regex in `\b` word boundaries.
|
||||
///
|
||||
/// For example:
|
||||
/// "hello" -> `\bh+\s*e+\s*l+\s*l+\s*o+\b`
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `phrase` - The phrase to generate the regex for.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The generated regex for the phrase. It does not return a [`Regex`] instance so
|
||||
/// that it can be easily merged with other words in [`regex_from_phrases_list`].
|
||||
fn regex_from_phrase(phrase: &str) -> String {
|
||||
let mut pattern = String::new();
|
||||
|
||||
@@ -143,6 +240,13 @@ fn regex_from_phrase(phrase: &str) -> String {
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Merges multiple [`regex_from_phrase`]-generated regexes into a single regex.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `phrases` - A slice of phrases to generate the regex for.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Regex`] - The merged regex for the phrases.
|
||||
fn regex_from_phrases_list(phrases: &[&str]) -> Regex {
|
||||
let mut pattern = String::new();
|
||||
|
||||
@@ -157,34 +261,70 @@ fn regex_from_phrases_list(phrases: &[&str]) -> Regex {
|
||||
Regex::new(&pattern).expect("Failed to compile regex")
|
||||
}
|
||||
|
||||
pub fn analyze_message(message: &str) -> Vec<(Vec<String>, Arc<dyn Rule>)> {
|
||||
/// The results of the analysis of a rule, including all detections (on the original message first,
|
||||
/// on the normalized message later) and the associated rule.
|
||||
type RuleAnalysis = ((Vec<String>, Vec<String>), Arc<dyn Rule>);
|
||||
|
||||
/// Scans all rules on the message and returns the results for each rule which gave detections.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `original` - The original message without normalization.
|
||||
/// * `normalized` - The normalized version of the message.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Vec<RuleAnalysis>`] - The results of the analysis, including all detections (on the original
|
||||
/// message first, on the normalized message later) and their associated rules.
|
||||
pub fn analyze_message(original: &str, normalized: &str) -> Vec<RuleAnalysis> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for rule in RULES.iter() {
|
||||
let detections = rule.detect(message);
|
||||
if !detections.is_empty() {
|
||||
results.push((detections, rule.clone()));
|
||||
let (original_detections, normalized_detections) = rule.detect(original, normalized);
|
||||
if !normalized_detections.is_empty() || !original_detections.is_empty() {
|
||||
results.push(((original_detections, normalized_detections), rule.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Replaces all the found rule breakings in a message with `[censored]`.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `original` - The original message without normalization.
|
||||
/// * `normalized` - The normalized version of the message.
|
||||
/// * `analysis` - The results of the analysis, including all detections and their associated
|
||||
/// rules. This is generated by [`analyze_message`].
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The censored version of the original message.
|
||||
pub fn censor_message(
|
||||
original: &str,
|
||||
normalized: NormalizedMessage,
|
||||
analysis: &[(Vec<String>, Arc<dyn Rule>)],
|
||||
analysis: &[RuleAnalysis],
|
||||
) -> String {
|
||||
let mut censored = original.to_string();
|
||||
|
||||
// Collect all ranges to censor
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for (findings, _) in analysis {
|
||||
for finding in findings {
|
||||
for ((original_findings, normalized_findings), _) in analysis {
|
||||
for finding in normalized_findings {
|
||||
for (index, _) in normalized.message.match_indices(finding) {
|
||||
let start_index = normalized.mappings.get(index).cloned().unwrap_or(0);
|
||||
let end_index = normalized.mappings.get(index + finding.len()).cloned().unwrap_or(original.len());
|
||||
let end_index = normalized
|
||||
.mappings
|
||||
.get(index + finding.len())
|
||||
.cloned()
|
||||
.unwrap_or(original.len());
|
||||
|
||||
ranges.push((start_index, end_index));
|
||||
}
|
||||
}
|
||||
|
||||
for finding in original_findings {
|
||||
for (index, _) in original.match_indices(finding) {
|
||||
let start_index = index;
|
||||
let end_index = index + finding.len();
|
||||
|
||||
ranges.push((start_index, end_index));
|
||||
}
|
||||
@@ -203,27 +343,69 @@ pub fn censor_message(
|
||||
censored
|
||||
}
|
||||
|
||||
/// Normalizes, analyzes, and censors the original message, returning all rules broken in the
|
||||
/// message.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `original` - The original message.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`(String, Vec<Arc<dyn Rule>>)`] - The censored message and the rules that were broken.
|
||||
pub fn moderate_message(original: &str) -> (String, Vec<Arc<dyn Rule>>) {
|
||||
let normalized = normalize(original.to_string());
|
||||
|
||||
let analysis = analyze_message(&normalized.message);
|
||||
let analysis = analyze_message(original, &normalized.message);
|
||||
let censored = censor_message(original, normalized, &analysis);
|
||||
|
||||
(censored, analysis.iter().map(|(_, r)| r.clone()).collect())
|
||||
}
|
||||
|
||||
/// The action to be taken when a rule is broken.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Action {
|
||||
/// The message is not allowed to go through, the user must be penalized.
|
||||
BlockAndPenalty,
|
||||
|
||||
/// The message's blocked phrases are censored with `[censored]` but the message goes through.
|
||||
/// If the user repeats this multiple times, they may be warned about this.
|
||||
///
|
||||
/// TODO: Collect information about this type of action in the DB to be able to warn the user.
|
||||
SoftCensorAndWarn,
|
||||
}
|
||||
|
||||
/// Trait implemented by all rules.
|
||||
#[allow(dead_code)]
|
||||
pub trait Rule: Send + Sync {
|
||||
/// The rule's human-readable name.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// The action that must be taken when the rule is broken.
|
||||
fn action(&self) -> &'static Action;
|
||||
fn detect(&self, message: &str) -> Vec<String>;
|
||||
|
||||
/// Detects rule violations in the given message.
|
||||
///
|
||||
/// It takes both the original and normalized version of the message because some rules may
|
||||
/// need the unchanged version to detect strict patterns (e.g. URLs) while some others will
|
||||
/// need the normalized version to detect banned keywords and phrases.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `original` - The original message.
|
||||
/// * `normalized` - The normalized version of the message.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`(Vec<String>, Vec<String>)`] - The list of detected rule violations in the original
|
||||
/// (first vector) and normalized (second vector) message.
|
||||
fn detect(&self, original: &str, normalized: &str) -> (Vec<String>, Vec<String>);
|
||||
}
|
||||
|
||||
/// Parses a file containing phrases to be detected by rules.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `file_content` - The content of the file to parse.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`Vec<&str>`] - The list of phrases found in the file.
|
||||
fn parse_phrases_file(file_content: &str) -> Vec<&str> {
|
||||
file_content
|
||||
.lines()
|
||||
@@ -260,11 +442,14 @@ impl Rule for HateSpeechAndSlursRule {
|
||||
&Action::BlockAndPenalty
|
||||
}
|
||||
|
||||
fn detect(&self, message: &str) -> Vec<String> {
|
||||
self.0
|
||||
.find_iter(message)
|
||||
.map(|m| message[m.start()..m.end()].to_string())
|
||||
.collect()
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,11 +475,14 @@ impl Rule for ThreatsAndSelfHarmRule {
|
||||
&Action::BlockAndPenalty
|
||||
}
|
||||
|
||||
fn detect(&self, message: &str) -> Vec<String> {
|
||||
self.0
|
||||
.find_iter(message)
|
||||
.map(|m| message[m.start()..m.end()].to_string())
|
||||
.collect()
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +508,14 @@ impl Rule for ExplicitSexualLanguageRule {
|
||||
"Explicit Sexual Language"
|
||||
}
|
||||
|
||||
fn detect(&self, message: &str) -> Vec<String> {
|
||||
self.0
|
||||
.find_iter(message)
|
||||
.map(|m| message[m.start()..m.end()].to_string())
|
||||
.collect()
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,19 +540,181 @@ impl Rule for SuggestiveLanguageRule {
|
||||
"Suggestive Language"
|
||||
}
|
||||
|
||||
fn detect(&self, message: &str) -> Vec<String> {
|
||||
self.0
|
||||
.find_iter(message)
|
||||
.map(|m| message[m.start()..m.end()].to_string())
|
||||
.collect()
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AdvertisingRule;
|
||||
|
||||
impl Rule for AdvertisingRule {
|
||||
fn name(&self) -> &'static str {
|
||||
"Advertising"
|
||||
}
|
||||
|
||||
fn action(&self) -> &'static Action {
|
||||
&Action::BlockAndPenalty
|
||||
}
|
||||
|
||||
fn detect(&self, original: &str, _normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
let mut detections = Vec::new();
|
||||
|
||||
for link in LinkFinder::new()
|
||||
.url_must_have_scheme(false)
|
||||
.links(original)
|
||||
{
|
||||
let link_str = link.as_str();
|
||||
|
||||
if link_str.contains("discord.gg") || link_str.contains("discord.com") {
|
||||
detections.push(link_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
(detections, vec![])
|
||||
}
|
||||
}
|
||||
|
||||
struct SensitiveTopicsAndTriggeringContentRule(Regex);
|
||||
|
||||
impl SensitiveTopicsAndTriggeringContentRule {
|
||||
fn new() -> Self {
|
||||
let phrases: Vec<&str> = parse_phrases_file(include_str!(
|
||||
"lists/sensitive-topics-and-triggering-content.txt"
|
||||
));
|
||||
|
||||
let regex = regex_from_phrases_list(&phrases);
|
||||
|
||||
Self(regex)
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for SensitiveTopicsAndTriggeringContentRule {
|
||||
fn name(&self) -> &'static str {
|
||||
"Sensitive Topics and Triggering Content"
|
||||
}
|
||||
|
||||
fn action(&self) -> &'static Action {
|
||||
&Action::BlockAndPenalty
|
||||
}
|
||||
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MildSwearWordsRule(Regex);
|
||||
|
||||
impl MildSwearWordsRule {
|
||||
fn new() -> Self {
|
||||
let phrases: Vec<&str> = parse_phrases_file(include_str!("lists/mild-swear-words.txt"));
|
||||
|
||||
let regex = regex_from_phrases_list(&phrases);
|
||||
|
||||
Self(regex)
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for MildSwearWordsRule {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mild Swear Words"
|
||||
}
|
||||
|
||||
fn action(&self) -> &'static Action {
|
||||
&Action::SoftCensorAndWarn
|
||||
}
|
||||
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CasualInsultsAndTeasing(Regex);
|
||||
|
||||
impl CasualInsultsAndTeasing {
|
||||
fn new() -> Self {
|
||||
let phrases: Vec<&str> =
|
||||
parse_phrases_file(include_str!("lists/casual-insults-and-teasing.txt"));
|
||||
|
||||
let regex = regex_from_phrases_list(&phrases);
|
||||
|
||||
Self(regex)
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for CasualInsultsAndTeasing {
|
||||
fn name(&self) -> &'static str {
|
||||
"Casual Insults and Teasing"
|
||||
}
|
||||
|
||||
fn action(&self) -> &'static Action {
|
||||
&Action::SoftCensorAndWarn
|
||||
}
|
||||
|
||||
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
(
|
||||
vec![],
|
||||
self.0
|
||||
.find_iter(normalized)
|
||||
.map(|m| normalized[m.start()..m.end()].to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct LinksRule;
|
||||
|
||||
impl Rule for LinksRule {
|
||||
fn name(&self) -> &'static str {
|
||||
"Links"
|
||||
}
|
||||
|
||||
fn action(&self) -> &'static Action {
|
||||
&Action::SoftCensorAndWarn
|
||||
}
|
||||
|
||||
fn detect(&self, original: &str, _normalized: &str) -> (Vec<String>, Vec<String>) {
|
||||
let mut detections = Vec::new();
|
||||
|
||||
for link in LinkFinder::new()
|
||||
.url_must_have_scheme(false)
|
||||
.links(original)
|
||||
{
|
||||
detections.push(link.as_str().to_string());
|
||||
}
|
||||
|
||||
(detections, vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of moderation rules.
|
||||
pub static RULES: Lazy<Vec<Arc<dyn Rule>>> = Lazy::new(|| {
|
||||
vec![
|
||||
Arc::new(HateSpeechAndSlursRule::new()),
|
||||
Arc::new(ThreatsAndSelfHarmRule::new()),
|
||||
Arc::new(SuggestiveLanguageRule::new()),
|
||||
Arc::new(ExplicitSexualLanguageRule::new()),
|
||||
Arc::new(SuggestiveLanguageRule::new()),
|
||||
Arc::new(AdvertisingRule),
|
||||
Arc::new(SensitiveTopicsAndTriggeringContentRule::new()),
|
||||
Arc::new(MildSwearWordsRule::new()),
|
||||
Arc::new(CasualInsultsAndTeasing::new()),
|
||||
Arc::new(LinksRule),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
database::models::UserIncident,
|
||||
framework::commands::{CommandError, context::CommandContext, permissions::Permission},
|
||||
groups::calls::components::{CodeOfConductButton, NeedHelpButton},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotBannedPermission;
|
||||
|
||||
#[async_trait]
|
||||
impl Permission<State> for NotBannedPermission {
|
||||
async fn check(&self, ctx: &CommandContext<State>) -> Result<bool, CommandError> {
|
||||
let incidents = UserIncident::get_for_user(ctx.event.author.id.get(), &ctx.rt.state.db)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get user incidents: {e}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
if incidents.iter().any(|i| i.is_active()) {
|
||||
ctx.send()
|
||||
.content("❌ You've been banned from Payphone.")
|
||||
.component(CodeOfConductButton, 1)
|
||||
.component(NeedHelpButton, 1)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to send message: {e}");
|
||||
})
|
||||
.map_err(|_| CommandError::MessageSendingFailed)?;
|
||||
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Event handling for the gateway sub-module.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -11,6 +13,7 @@ use crate::{
|
||||
state::State,
|
||||
};
|
||||
|
||||
/// Logs a ready message when the bot is ready.
|
||||
#[derive(Debug)]
|
||||
pub struct ReadyHandler;
|
||||
|
||||
@@ -27,6 +30,7 @@ impl OnReadyHandler<State> for ReadyHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs a reconnection message when the gateway asks for a reconnection.
|
||||
#[derive(Debug)]
|
||||
pub struct ReconnectHandler;
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
//! Logging gateway events.
|
||||
|
||||
pub mod events;
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
//! Payphone-specific code. All features like calls and moderation go here.
|
||||
//!
|
||||
//! Sub-modules in this module usually follow the same structure:
|
||||
//!
|
||||
//! - A `mod.rs` file exporting the public API of the sub-module.
|
||||
//! - A `commands.rs` file containing the command definitions.
|
||||
//! - A `embeds.rs` file containing embed-generating functions for those commands.
|
||||
//! - A `events.rs` file containing event handlers related to the features the sub-module covers.
|
||||
//! - Any additional modules the sub-module may specifically require.
|
||||
//!
|
||||
//! An example of extra modules not listed above is the [`calls::moderation`] module, which
|
||||
//! includes the tools for parsing messages and detecting rule breakings in them.
|
||||
//!
|
||||
//! > *Note that not all sub-modules have commands, nor they all have event listeners, etc. Each of
|
||||
//! > them only defines the modules they need.*
|
||||
|
||||
pub mod about;
|
||||
pub mod calls;
|
||||
pub mod gateway;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
constants::DEFAULT_PREFIX,
|
||||
database::models::Server,
|
||||
discord::sanitizing::SanitizeDiscordMarkdown,
|
||||
framework::commands::{
|
||||
Command, CommandError,
|
||||
arguments::{Argument, GreedyString, ParseArguments},
|
||||
context::CommandContext,
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrefixCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command<State> for PrefixCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"prefix"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &'static [&'static str] {
|
||||
&["p"]
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Sets the command prefix for the server."
|
||||
}
|
||||
|
||||
fn arguments(&self) -> &'static [Argument] {
|
||||
&[Argument {
|
||||
name: "prefix",
|
||||
is_optional: true,
|
||||
is_greedy: true,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: CommandContext<'_, State>,
|
||||
args: &str,
|
||||
) -> Result<(), CommandError> {
|
||||
let prefix: GreedyString = ParseArguments::parse(args).unwrap();
|
||||
let mut prefix = prefix.trim();
|
||||
|
||||
let mut server = Server::get_or_create_by_id(
|
||||
ctx.event
|
||||
.guild_id
|
||||
.ok_or(CommandError::ServerCommandRunInDMs)?
|
||||
.get(),
|
||||
&ctx.rt.state.db,
|
||||
&ctx.rt.http,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Failed to get or create server: {e}"))
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
if prefix.is_empty() {
|
||||
prefix = DEFAULT_PREFIX;
|
||||
}
|
||||
|
||||
server.prefix = prefix.to_string();
|
||||
server
|
||||
.upsert(&ctx.rt.state.db)
|
||||
.await
|
||||
.map_err(|_| CommandError::DatabaseQueryFailed)?;
|
||||
|
||||
ctx.send()
|
||||
.content(format!(
|
||||
"✅ Prefix updated! Run `{}help` to see the new prefix.",
|
||||
prefix.sanitize(false)
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to send prefix update message: {e}");
|
||||
CommandError::MessageSendingFailed
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! The event handlers for the servers sub-module.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -14,6 +16,7 @@ use crate::{
|
||||
state::State,
|
||||
};
|
||||
|
||||
/// Adds servers to the DB when they get created.
|
||||
#[derive(Debug)]
|
||||
pub struct ServerCreateHandler;
|
||||
|
||||
@@ -43,6 +46,7 @@ impl OnServerCreateHandler<State> for ServerCreateHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates DB server data when a server gets updated.
|
||||
#[derive(Debug)]
|
||||
pub struct ServerUpdateHandler;
|
||||
|
||||
@@ -74,6 +78,7 @@ impl OnServerUpdateHandler<State> for ServerUpdateHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes servers from the DB when they get deleted or payphone is kicked.
|
||||
#[derive(Debug)]
|
||||
pub struct ServerDeleteHandler;
|
||||
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
//! Syncing servers with the database and server configuration commands.
|
||||
//!
|
||||
//! This sub-module is responsible for registering servers in the database, updating them when they
|
||||
//! update, and removing them when they get deleted or Payphone is kicked.
|
||||
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub mod permissions;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
framework::commands::{CommandError, context::CommandContext, permissions::Permission},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GuildOnlyPermission;
|
||||
|
||||
#[async_trait]
|
||||
impl Permission<State> for GuildOnlyPermission {
|
||||
async fn check(&self, ctx: &CommandContext<State>) -> Result<bool, CommandError> {
|
||||
Ok(ctx.event.guild_id.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerModeratorPermission;
|
||||
|
||||
#[async_trait]
|
||||
impl Permission<State> for ServerModeratorPermission {
|
||||
async fn check(&self, ctx: &CommandContext<State>) -> Result<bool, CommandError> {
|
||||
let member = ctx
|
||||
.rt
|
||||
.http
|
||||
.guild_member(ctx.event.guild_id.unwrap(), ctx.event.author.id)
|
||||
.await
|
||||
.map_err(CommandError::DiscordAPIFailed)?
|
||||
.model()
|
||||
.await
|
||||
.map_err(CommandError::DiscordDeserializationFailed)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod cli;
|
||||
pub mod constants;
|
||||
pub mod database;
|
||||
pub mod discord;
|
||||
pub mod framework;
|
||||
pub mod groups;
|
||||
pub mod logging;
|
||||
pub mod state;
|
||||
pub mod utils;
|
||||
+10
-10
@@ -1,16 +1,16 @@
|
||||
//! Log handling and formatting of the console's output.
|
||||
//!
|
||||
//!
|
||||
//! # Logging
|
||||
//!
|
||||
//!
|
||||
//! Payphone uses the [`log`] crate for logging, and the [`colored`] crate for formatting the
|
||||
//! output.
|
||||
//!
|
||||
//!
|
||||
//! Any logs that are not coming from the `payphone` crate are ignored.
|
||||
//!
|
||||
//!
|
||||
//! ## Logging Levels
|
||||
//!
|
||||
//!
|
||||
//! There are 5 different logging levels:
|
||||
//!
|
||||
//!
|
||||
//! - `trace`: The most verbose level, used for debugging purposes.
|
||||
//! - `debug`: Events that are useful to be displayed during development, but not in production.
|
||||
//! - `info`: Used for general information about the bot's state and milestones.
|
||||
@@ -19,9 +19,9 @@
|
||||
//! - `error`: Used for errors, when something goes wrong and the bot cannot continue running.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//!
|
||||
//! Logging is quite simple as it works like any [`println!`] calls.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! log::trace!("This is a trace message.");
|
||||
//! log::debug!("This is a debug message.");
|
||||
@@ -29,9 +29,9 @@
|
||||
//! log::warn!("This is a warning message.");
|
||||
//! log::error!("This is an error message.");
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! Logging supports formatting, so you can use it like any other [`println!`] call:
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! let value = 42;
|
||||
//! log::info!("The value is: {value}");
|
||||
|
||||
+74
-10
@@ -107,24 +107,33 @@ use std::{
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{Router, response::Redirect, routing};
|
||||
use clap::Parser;
|
||||
use notify::{Event, EventKind, Watcher};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::services::ServeDir;
|
||||
use twilight_gateway::Intents;
|
||||
use twilight_gateway::{Intents, ShardId};
|
||||
|
||||
use crate::{
|
||||
cli::{Args, Commands},
|
||||
framework::bot::Bot,
|
||||
constants::DEFAULT_PREFIX,
|
||||
database::models::Server,
|
||||
framework::{
|
||||
bot::{Bot, PrefixGetter},
|
||||
runtime::Runtime,
|
||||
},
|
||||
groups::{
|
||||
about::{commands::HelpCommand, components::HelpGoBackButton},
|
||||
calls::{
|
||||
commands::{CallCommand, HangupCommand},
|
||||
events::MessageRedirector,
|
||||
commands::{CallCommand, FriendRequestCommand, HangupCommand, SkipCommand},
|
||||
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler},
|
||||
},
|
||||
gateway::events::{ReadyHandler, ReconnectHandler},
|
||||
servers::events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
|
||||
servers::{
|
||||
commands::PrefixCommand,
|
||||
events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
|
||||
},
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
@@ -139,6 +148,10 @@ mod logging;
|
||||
mod state;
|
||||
mod utils;
|
||||
|
||||
/// Payphone's main entrypoint function.
|
||||
///
|
||||
/// Parses ENV variables and CLI arguments, sets up the logger, and runs [`start`] or [`docs`]
|
||||
/// depending on the command.
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
@@ -152,8 +165,18 @@ pub async fn main() {
|
||||
Commands::Start {
|
||||
token,
|
||||
database_addr,
|
||||
database_username,
|
||||
database_password,
|
||||
shard,
|
||||
} => {
|
||||
start(token, database_addr).await;
|
||||
start(
|
||||
token,
|
||||
database_addr,
|
||||
database_username,
|
||||
database_password,
|
||||
shard.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Commands::Docs { bind, open } => {
|
||||
docs(bind, open).await;
|
||||
@@ -162,14 +185,45 @@ pub async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn start(token: String, database_addr: String) {
|
||||
struct ServerPrefixOrPayphoneMention;
|
||||
|
||||
#[async_trait]
|
||||
impl PrefixGetter<State> for ServerPrefixOrPayphoneMention {
|
||||
async fn get(
|
||||
&self,
|
||||
rt: &Runtime<State>,
|
||||
server_id: Option<u64>,
|
||||
_user_id: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
let mention = format!("<@{}>", rt.app.bot.as_ref().unwrap().id);
|
||||
|
||||
let default_prefixes = vec![DEFAULT_PREFIX.to_string(), mention.clone()];
|
||||
|
||||
if let Some(server_id) = server_id {
|
||||
if let Ok(Some(server)) = Server::get_by_id(server_id, &rt.state.db).await {
|
||||
return vec![server.prefix, mention];
|
||||
}
|
||||
}
|
||||
|
||||
default_prefixes
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a Payphone shard.
|
||||
async fn start(
|
||||
token: String,
|
||||
database_addr: String,
|
||||
database_username: Option<String>,
|
||||
database_password: Option<String>,
|
||||
shard: ShardId,
|
||||
) {
|
||||
log::info!("Welcome to Payphone, Nyeki!");
|
||||
log::info!(
|
||||
"Payphone is running on version v{}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
let db = database::init(database_addr)
|
||||
let db = database::init(database_addr, database_username, database_password)
|
||||
.await
|
||||
.expect("Could not connect to the database!");
|
||||
|
||||
@@ -177,17 +231,23 @@ async fn start(token: String, database_addr: String) {
|
||||
|
||||
let state = State { db };
|
||||
|
||||
let bot = Bot::new(state, ["p."])
|
||||
let bot = Bot::new(state, ServerPrefixOrPayphoneMention)
|
||||
.shard(shard)
|
||||
.intent(Intents::GUILDS)
|
||||
.intent(Intents::GUILD_MESSAGES)
|
||||
.intent(Intents::MESSAGE_CONTENT)
|
||||
.command(HelpCommand)
|
||||
.command(CallCommand::new())
|
||||
.command(SkipCommand)
|
||||
.command(HangupCommand)
|
||||
.command(FriendRequestCommand)
|
||||
.command(PrefixCommand)
|
||||
.component(HelpGoBackButton)
|
||||
.on_ready(ReadyHandler)
|
||||
.on_reconnect(ReconnectHandler)
|
||||
.on_message(MessageRedirector)
|
||||
.on_message(CallMessageHandler)
|
||||
.on_message_edit(CallMessageEditHandler)
|
||||
.on_message_delete(CallMessageDeleteHandler)
|
||||
.on_server_create(ServerCreateHandler)
|
||||
.on_server_update(ServerUpdateHandler)
|
||||
.on_server_delete(ServerDeleteHandler);
|
||||
@@ -195,6 +255,10 @@ async fn start(token: String, database_addr: String) {
|
||||
bot.run(&token).await.expect("Could not run bot!");
|
||||
}
|
||||
|
||||
/// Boots up a documentation server.
|
||||
///
|
||||
/// It compiles the crate's documentation and recompiles it automatically when the source code
|
||||
/// changes.
|
||||
async fn docs(bind: SocketAddr, open_on_start: bool) {
|
||||
log::info!("Compiling documentation...");
|
||||
|
||||
|
||||
+4
-4
@@ -1,18 +1,18 @@
|
||||
//! Application state at runtime including shared resources.
|
||||
//!
|
||||
//!
|
||||
//! Currently, it only contains the database connection, but it can be extended in the future to
|
||||
//! include other resources that need to be shared across the application, such as caches or
|
||||
//! configuration settings.
|
||||
//!
|
||||
//!
|
||||
//! It is shared around the application using an `Arc`, allowing it to be cloned and passed around
|
||||
//! without needing to worry about ownership or lifetimes. This is particularly useful in an
|
||||
//! asynchronous context.
|
||||
//!
|
||||
//!
|
||||
//! ```
|
||||
//! let state = State {
|
||||
//! db: database::init("127.0.0.1:9042").await?,
|
||||
//! };
|
||||
//!
|
||||
//!
|
||||
//! fn handler_which_needs_state(state: Arc<State>) {
|
||||
//! // Use the state here
|
||||
//! }
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
//! A set of generic utilities used across the codebase.
|
||||
//!
|
||||
//! It currently includes [`ToTitleCase`], [`get_random_u64`], and [`Bye`].
|
||||
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
/// Converts a string to title case.
|
||||
///
|
||||
/// It removes all duplicate whitespaces and capitalizes the first letter of every word.
|
||||
pub trait ToTitleCase {
|
||||
fn to_title_case(&self) -> String;
|
||||
}
|
||||
@@ -38,6 +45,8 @@ pub fn get_random_u64() -> u64 {
|
||||
rng.random()
|
||||
}
|
||||
|
||||
/// Consumes any value. Used to normalize return values when the output of a function doesn't
|
||||
/// matter yet all functions being called return different types.
|
||||
pub trait Bye: Sized {
|
||||
/// Whatever the value is, it consumes it and returns a unit value. Used to remove warnings
|
||||
/// about unused results.
|
||||
@@ -45,3 +54,49 @@ pub trait Bye: Sized {
|
||||
}
|
||||
|
||||
impl<T> Bye for T {}
|
||||
|
||||
/// Trims a string to a maximum length, adding an ellipsis if it exceeds the limit.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `string` - The string to trim with ellipse.
|
||||
/// * `max_length` - The max length the string may have.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`String`] - The trimmed string with an ellipsis if it exceeded the limit.
|
||||
pub fn ellipse(string: String, max_length: usize) -> String {
|
||||
if string.len() <= max_length {
|
||||
string
|
||||
} else {
|
||||
let len = max_length.saturating_sub(3);
|
||||
|
||||
format!("{}...", &string[..len])
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the bytes from [`i64`] to [`u64`].
|
||||
///
|
||||
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `i` - The [`i64`] to convert.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`u64`] - The resulting [`u64`].
|
||||
pub fn i64_to_u64(i: i64) -> u64 {
|
||||
u64::from_ne_bytes(i.to_ne_bytes())
|
||||
}
|
||||
|
||||
/// Copies the bytes from [`u64`] to [`i64`].
|
||||
///
|
||||
/// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact,
|
||||
/// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `u` - The [`u64`] to convert.
|
||||
///
|
||||
/// Returns:
|
||||
/// [`i64`] - The resulting [`i64`].
|
||||
pub fn u64_to_i64(u: u64) -> i64 {
|
||||
i64::from_ne_bytes(u.to_ne_bytes())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "ansible"
|
||||
version = "11.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansible-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/b3/01564da36375f35907c2ec6626d68f4d59e39e566c4b3f874f01d0d2ca19/ansible-11.9.0.tar.gz", hash = "sha256:528ca5a408f11cf1fea00daea7570e68d40e167be38b90c119a7cb45729e4921", size = 44589149, upload-time = "2025-08-12T16:03:31.912Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/fd/093dfe1f7f9f1058c0efa10b685f6049b676aa2c0ecd6f99c8664cafad6a/ansible-11.9.0-py3-none-any.whl", hash = "sha256:79b087ef38105b93e0e092e7013a0f840e154a6a8ce9b5fddd1b47593adc542a", size = 56340779, upload-time = "2025-08-12T16:03:26.18Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible-compat"
|
||||
version = "25.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansible-core" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "subprocess-tee" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/96/d5bcbd652dbcbf8611b32457d67b10e41e5e596e34460c25511ccfa6fbf1/ansible_compat-25.8.1.tar.gz", hash = "sha256:f29ab81264b3f04fb94ac2d7f68fe73efc2eb3cbe4ecf00f9b4742e77c41756f", size = 90142, upload-time = "2025-08-14T15:59:57.215Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/29/c73b6d6d5ecdaf51ee4210e6fbf95de0a24b87f2b5df83155ac0c753513e/ansible_compat-25.8.1-py3-none-any.whl", hash = "sha256:5bb843838414b75efff24dae5ff5b332cd503a596143d471980243cec4b46c9c", size = 27523, upload-time = "2025-08-14T15:59:55.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible-core"
|
||||
version = "2.18.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "resolvelib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/78/8b8680eaf7b1990a8d4c26f25cdf2b2eabaae764a3d8e5299b1d61c7c385/ansible_core-2.18.8.tar.gz", hash = "sha256:b0766215a96a47ce39933d27e1e996ca2beb54cf1b3907c742d35c913b1f78cd", size = 3088636, upload-time = "2025-08-11T19:03:12.495Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/59/aa2c1918224b054c9a0d93812c0b446fa8ddba155dc96c855189fae9c82b/ansible_core-2.18.8-py3-none-any.whl", hash = "sha256:b60bc23b2f11fd0559a79d10ac152b52f58a18ca1b7be0a620dfe87f34ced689", size = 2218673, upload-time = "2025-08-11T19:03:04.75Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansible-lint"
|
||||
version = "25.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansible-compat" },
|
||||
{ name = "ansible-core" },
|
||||
{ name = "black" },
|
||||
{ name = "filelock" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "referencing" },
|
||||
{ name = "ruamel-yaml" },
|
||||
{ name = "subprocess-tee" },
|
||||
{ name = "wcmatch" },
|
||||
{ name = "yamllint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/cc/43d66a73bf34b5e93702c71f432aedaa45e2b32c8fcee825120af0171ae1/ansible_lint-25.8.1.tar.gz", hash = "sha256:04059e3cf5b644e796e21d445004621d1e2454e3aeb68d1eea0a49f650ef3219", size = 578090, upload-time = "2025-08-14T00:44:04.229Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/29/02c9ba1258d97a0ceee47e89b852810d8c83c3294dd60f32cc0ad759fa08/ansible_lint-25.8.1-py3-none-any.whl", hash = "sha256:ce23c83212d21ccef9053fabeafbbe63488ab11f85e1ed2a94b2523f479d1372", size = 327282, upload-time = "2025-08-14T00:44:02.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bracex"
|
||||
version = "2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "45.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "payphone"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "ansible" },
|
||||
{ name = "ansible-lint" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "ansible", specifier = ">=11.9.0" },
|
||||
{ name = "ansible-lint", specifier = ">=25.8.1" },
|
||||
{ name = "requests", specifier = ">=2.32.4" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.36.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolvelib"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml"
|
||||
version = "0.18.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511, upload-time = "2025-06-09T08:51:09.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570, upload-time = "2025-06-09T08:51:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml-clib"
|
||||
version = "0.2.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload-time = "2024-10-20T10:12:55.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload-time = "2024-10-20T10:12:57.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload-time = "2024-10-20T10:12:58.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload-time = "2024-10-20T10:13:00.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload-time = "2024-10-21T11:26:46.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload-time = "2024-10-21T11:26:47.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload-time = "2024-12-11T19:58:17.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload-time = "2024-10-20T10:13:01.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload-time = "2024-10-20T10:13:02.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload-time = "2024-10-20T10:13:04.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload-time = "2024-10-20T10:13:05.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload-time = "2024-10-20T10:13:07.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload-time = "2024-10-20T10:13:08.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload-time = "2024-10-21T11:26:48.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload-time = "2024-10-21T11:26:50.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload-time = "2024-12-11T19:58:18.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload-time = "2024-10-20T10:13:09.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subprocess-tee"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/22/991efbf35bc811dfe7edcd749253f0931d2d4838cf55176132633e1c82a7/subprocess_tee-0.4.2.tar.gz", hash = "sha256:91b2b4da3aae9a7088d84acaf2ea0abee3f4fd9c0d2eae69a9b9122a71476590", size = 14951, upload-time = "2024-06-17T19:51:51.249Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/ab/e3a3be062cd544b2803760ff707dee38f0b1cb5685b2446de0ec19be28d9/subprocess_tee-0.4.2-py3-none-any.whl", hash = "sha256:21942e976715af4a19a526918adb03a8a27a8edab959f2d075b777e3d78f532d", size = 5249, upload-time = "2024-06-17T19:51:15.949Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcmatch"
|
||||
version = "10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bracex" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yamllint"
|
||||
version = "1.37.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pathspec" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/f2/cd8b7584a48ee83f0bc94f8a32fea38734cefcdc6f7324c4d3bfc699457b/yamllint-1.37.1.tar.gz", hash = "sha256:81f7c0c5559becc8049470d86046b36e96113637bcbe4753ecef06977c00245d", size = 141613, upload-time = "2025-05-04T08:25:54.355Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/b9/be7a4cfdf47e03785f657f94daea8123e838d817be76c684298305bd789f/yamllint-1.37.1-py3-none-any.whl", hash = "sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583", size = 68813, upload-time = "2025-05-04T08:25:52.552Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user