6 Commits
89 changed files with 812 additions and 128992 deletions
-19
View File
@@ -1,19 +0,0 @@
/target
/.venv
/.ansible
/.vscode
/k8s
/playbooks
/backup
.gitignore
.gitlab-ci.yml
.python-version
ansible.cfg
compose.yml
inventory.ini
pyproject.toml
uv.lock
.env
-7
View File
@@ -1,10 +1,3 @@
/target
/.venv
/.ansible
/backup
.env
00-vault-pwd.txt
reset-ssh.sh
-38
View File
@@ -1,38 +0,0 @@
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/"
-1
View File
@@ -1 +0,0 @@
3.12
-4
View File
@@ -1,4 +0,0 @@
{
"editor.formatOnSave": true,
"ansible.python.interpreterPath": "/home/nyeki/Documents/Repositories/payphone/payphone/.venv/bin/python"
}
Generated
+1 -315
View File
@@ -363,11 +363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
@@ -401,27 +397,6 @@ 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"
@@ -471,36 +446,6 @@ 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"
@@ -510,17 +455,6 @@ 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"
@@ -771,12 +705,6 @@ 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"
@@ -912,119 +840,12 @@ 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"
@@ -1203,12 +1024,6 @@ 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"
@@ -1310,31 +1125,12 @@ 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"
@@ -1344,16 +1140,6 @@ 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"
@@ -1431,17 +1217,14 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "payphone"
version = "4.0.8"
version = "4.0.0"
dependencies = [
"async-trait",
"axum",
"clap",
"colored",
"csv",
"deadpool-redis",
"dotenvy",
"include_dir",
"itertools",
"linkify",
"log",
"notify",
@@ -1452,7 +1235,6 @@ dependencies = [
"regex",
"scc",
"scylla",
"serde",
"thiserror 2.0.12",
"time",
"tokio",
@@ -1490,15 +1272,6 @@ 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"
@@ -1576,27 +1349,6 @@ 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"
@@ -2166,16 +1918,6 @@ 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"
@@ -2477,23 +2219,6 @@ 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"
@@ -2944,12 +2669,6 @@ 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"
@@ -3020,36 +2739,3 @@ 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",
]
+1 -6
View File
@@ -1,19 +1,15 @@
[package]
name = "payphone"
version = "4.0.8"
version = "4.0.0"
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"
@@ -24,7 +20,6 @@ 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
View File
@@ -1,20 +0,0 @@
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" ]
-5
View File
@@ -1,5 +0,0 @@
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
[defaults]
timeout = 30
-16
View File
@@ -1,16 +0,0 @@
[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
-26
View File
@@ -1,26 +0,0 @@
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
@@ -1,8 +0,0 @@
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"
@@ -1,102 +0,0 @@
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"
@@ -1,15 +0,0 @@
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
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: local-csi-driver
@@ -1,8 +0,0 @@
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
-7
View File
@@ -1,7 +0,0 @@
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
name: local.csi.scylladb.com
spec:
attachRequired: false
storageCapacity: true
@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: local-csi-driver
name: local-csi-driver
@@ -1,12 +0,0 @@
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
-125
View File
@@ -1,125 +0,0 @@
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
-117
View File
@@ -1,117 +0,0 @@
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
-167
View File
@@ -1,167 +0,0 @@
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
---
-11
View File
@@ -1,11 +0,0 @@
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
-14
View File
@@ -1,14 +0,0 @@
$ANSIBLE_VAULT;1.1;AES256
31353838363564633532353539313763643561396661313030346466653839663931323961663362
6333376338336336346562643136316439393962356236640a653836643066346564663363633364
30616634363430646335663339613333616334393037333065336664396338346135353239346261
3366303231616166300a613566643231303066373266646665343637353364303939346464323633
37623565643063353435376238326564353563653735393164616136373531356135666337353765
39326463633965383463353335306432326433313132373461633666343934616638346666306339
36313561363965663662313163663362333466663135363731643737386466643134396564353439
32313936316432313433336235643337386430633434363764666439386234323933313465306638
65396532396533303662326338376661343962613432393735356130613165626465656464343434
63646265333064623735646161656236623366623265626664313564316665396331616631613536
38646264656563353934383237626534663563666335666430326562303862616461343935393737
62343637336563383864383732303861633634303061376666353231323331643538333633643762
35623539303562643763336232353633666661626539356465383631336261363532
-186
View File
@@ -1,186 +0,0 @@
- 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
-19
View File
@@ -1,19 +0,0 @@
- 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
-46
View File
@@ -1,46 +0,0 @@
- 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
-26
View File
@@ -1,26 +0,0 @@
- 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
-67
View File
@@ -1,67 +0,0 @@
- 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
-35
View File
@@ -1,35 +0,0 @@
- 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
-14
View File
@@ -1,14 +0,0 @@
- 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
-125
View File
@@ -1,125 +0,0 @@
- 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
-11
View File
@@ -1,11 +0,0 @@
[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",
]
-432
View File
@@ -1,432 +0,0 @@
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");
}
}
+1 -81
View File
@@ -46,11 +46,6 @@
//! $ 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
@@ -64,9 +59,7 @@
//! 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::{fmt, net::SocketAddr, str::FromStr};
use twilight_gateway::ShardId;
use std::net::SocketAddr;
/// Payphone's argument parser.
#[derive(clap::Parser)]
@@ -129,54 +122,6 @@ 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.
@@ -194,31 +139,6 @@ 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
View File
View File
-3
View File
@@ -8,6 +8,3 @@ 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;
+29 -37
View File
@@ -6,6 +6,28 @@ CREATE KEYSPACE IF NOT EXISTS payphone
'enabled': false
};
CREATE TYPE IF NOT EXISTS payphone.message (
id BIGINT,
author_id BIGINT,
author_name TEXT,
author_avatar_url TEXT,
author_mask ASCII,
content TEXT,
attachments LIST<TEXT>,
sticker BIGINT,
propagation_message_id BIGINT,
propagation_channel_id BIGINT,
replying_to_id,
is_deleted BOOLEAN,
is_edited BOOLEAN,
is_censored BOOLEAN,
);
CREATE TABLE IF NOT EXISTS payphone.users (
user_id BIGINT PRIMARY KEY,
@@ -60,57 +82,24 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
PRIMARY KEY (call_id, user_id, channel_id)
);
CREATE TYPE IF NOT EXISTS payphone.message (
id BIGINT,
call_id BIGINT,
author_id BIGINT,
author_name TEXT,
author_avatar_url TEXT,
author_mask ASCII,
content TEXT,
attachments LIST<TEXT>,
sticker BIGINT,
propagation_message_id BIGINT,
propagation_channel_id BIGINT,
replying_to_message_id BIGINT,
is_deleted BOOLEAN,
is_edited 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 FROZEN<payphone.message>,
replying_to FROZEN<payphone.message>,
@@ -123,18 +112,21 @@ CREATE TABLE IF NOT EXISTS payphone.reports (
reviewed_by_id BIGINT,
resolution ASCII,
PRIMARY KEY (user_id, report_id)
PRIMARY KEY (message_author_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 FROZEN<payphone.message>,
replying_to FROZEN<payphone.message>,
reason TEXT,
@@ -158,4 +150,4 @@ CREATE TABLE IF NOT EXISTS payphone.server_incidents (
created_at TIMESTAMP,
expires_at TIMESTAMP,
duration ASCII
);
);
+7 -21
View File
@@ -22,27 +22,13 @@ pub enum DatabaseError {
static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations");
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)?
};
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)?;
execute_file(
&session,
+279 -715
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,16 +1,5 @@
//! 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")
-5
View File
@@ -1,7 +1,2 @@
//! 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;
+2 -26
View File
@@ -1,22 +1,5 @@
//! 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
@@ -31,8 +14,7 @@ where
for ch in input.chars() {
match ch {
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' | '[' | ']' | '/'
| '(' | ')' => {
'\\' | '*' | '_' | '~' | '`' | '>' | '<' | '|' | '#' => {
escaped.push('\\');
escaped.push(ch);
}
@@ -46,13 +28,7 @@ where
};
// Now block ping attempts — break mentions
escaped.sanitize_mentions()
}
fn sanitize_mentions(&self) -> String {
let input = self.to_string();
input
escaped
.replace("@everyone", "@\u{200B}everyone")
.replace("@here", "@\u{200B}here")
.replace("<@", "<@\u{200B}") // user mentions
+40 -67
View File
@@ -1,50 +1,32 @@
use std::{fmt::Debug, sync::Arc};
use crate::framework::{
commands::{Command, runner::CommandRunner},
events::traits::{
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
OnServerUpdateHandler,
},
interactions::{
components::Component, message_command::MessageCommand,
message_command_registry::MessageCommandRegistry,
message_command_runner::MessageCommandRunner, registry::ComponentRegistry,
},
runtime::Runtime,
};
use crate::utils::Bye;
use async_trait::async_trait;
use std::{fmt::Debug, sync::Arc};
use thiserror::Error;
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
use twilight_http::Client;
use crate::{
framework::{
commands::{Command, runner::CommandRunner},
events::traits::{
OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler,
OnServerUpdateHandler,
},
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 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.
pub trait IntoPrefixes<T> {
async fn get(
&self,
runtime: &Runtime<T>,
@@ -54,21 +36,21 @@ pub trait PrefixGetter<T> {
}
#[async_trait]
impl<T> PrefixGetter<T> for String {
impl<T> IntoPrefixes<T> for String {
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
vec![self.to_string()]
}
}
#[async_trait]
impl<T> PrefixGetter<T> for &str {
impl<T> IntoPrefixes<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> PrefixGetter<S> for [T; N]
impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> IntoPrefixes<S> for [T; N]
where
T: ToString,
{
@@ -78,7 +60,7 @@ where
}
#[async_trait]
impl<F, Fut, T> PrefixGetter<T> for F
impl<F, Fut, T> IntoPrefixes<T> for F
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = Vec<String>> + Send,
@@ -88,14 +70,15 @@ where
}
}
pub struct Bot<T: Send + Sync = ()> {
shard: ShardId,
pub struct Bot<T: Send + Sync + Debug = ()> {
state: Arc<T>,
prefixes: Arc<dyn PrefixGetter<T> + Send + Sync>,
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
intents: Intents,
commands: Vec<Arc<dyn Command<T>>>,
components: Vec<Arc<dyn Component<T>>>,
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
message_commands: Vec<Arc<dyn MessageCommand<T>>>,
message_command_registry: Arc<MessageCommandRegistry<T>>,
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T>>>,
@@ -106,18 +89,22 @@ pub struct Bot<T: Send + Sync = ()> {
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T>>>,
}
impl<T: 'static + Send + Sync> Bot<T> {
pub fn new(state: T, prefixes: impl PrefixGetter<T> + Send + Sync + 'static) -> Self {
impl<T: 'static + Send + Sync + Debug> Bot<T> {
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
let message_command_registry = Arc::new(MessageCommandRegistry::new());
Self {
shard: ShardId::new(0, 1),
state: Arc::new(state),
prefixes: Arc::new(prefixes),
intents: Intents::empty(),
commands: Vec::new(),
components: Vec::new(),
message_commands: Vec::new(),
message_command_registry: message_command_registry.clone(),
on_ready_handlers: Vec::new(),
on_reconnect_handlers: Vec::new(),
on_interaction_handlers: vec![Arc::new(InteractionRunner)],
on_interaction_handlers: vec![Arc::new(MessageCommandRunner {
registry: message_command_registry,
})],
on_message_handlers: vec![Arc::new(CommandRunner)],
on_message_edit_handlers: Vec::new(),
on_message_delete_handlers: Vec::new(),
@@ -126,9 +113,11 @@ impl<T: 'static + Send + Sync> Bot<T> {
on_server_delete_handlers: Vec::new(),
}
}
pub fn shard(mut self, shard: ShardId) -> Self {
self.shard = shard;
pub fn message_command(mut self, command: impl MessageCommand<T> + 'static) -> Self {
let arc_command = Arc::new(command);
self.message_command_registry.register(arc_command.name(), arc_command.clone());
self.message_commands.push(arc_command);
self
}
@@ -157,7 +146,6 @@ 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
@@ -168,13 +156,11 @@ 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
@@ -206,11 +192,10 @@ impl<T: 'static + Send + Sync> Bot<T> {
.await
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
let registry = ComponentRegistry::new();
let registry = ComponentRegistry::new();
for component in &self.components {
let component_id = component.custom_id();
registry.register(
component_id.expect("The component registered by default must have a custom ID"),
component.clone(),
@@ -228,13 +213,12 @@ impl<T: 'static + Send + Sync> Bot<T> {
commands: bot.commands.clone(),
});
let mut gateway = Shard::new(bot.shard, token.to_string(), bot.intents);
let mut gateway = Shard::new(ShardId::new(0, 1), token.to_string(), bot.intents);
loop {
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
let task_rt = runtime.clone();
let task_bot = bot.clone();
tokio::spawn(async move {
match event {
Event::Ready(event) => {
@@ -316,17 +300,6 @@ 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
-3
View File
@@ -1,3 +0,0 @@
pub struct RedisCache {
}
-4
View File
@@ -31,10 +31,6 @@ 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 {
+4 -5
View File
@@ -82,7 +82,7 @@ where
for (index, component) in self.components {
if index != current_index {
if !current_row.is_empty() {
if current_row.len() > 0 {
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.is_empty() {
if current_row.len() > 0 {
rows.push(current_row.build());
}
@@ -161,7 +161,6 @@ where
}
}
#[allow(dead_code)]
pub fn content(mut self, content: impl ToString) -> Self {
self.content = Some(content.to_string());
@@ -195,7 +194,7 @@ where
for (index, component) in self.components {
if index != current_index {
if !current_row.is_empty() {
if current_row.len() > 0 {
rows.push(current_row.build());
current_row = ActionRow::new(&[]);
}
@@ -214,7 +213,7 @@ where
current_row.add(custom_id.clone(), component.clone());
}
if !current_row.is_empty() {
if current_row.len() > 0 {
rows.push(current_row.build());
}
+2 -21
View File
@@ -2,16 +2,12 @@ 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, permissions::Permission,
};
use crate::framework::commands::{arguments::Argument, context::CommandContext};
pub mod arguments;
pub mod context;
pub mod messages;
pub mod permissions;
pub mod runner;
#[derive(Debug, Error)]
@@ -24,15 +20,6 @@ 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]
@@ -47,9 +34,6 @@ 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.
///
@@ -82,10 +66,7 @@ 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
.trim_ascii_start();
let message = message_trimmed_start.split_at(prefix.len()).1;
match message.split_once(" ") {
Some((name, args)) => Some(CommandParts::new(name, args)),
-10
View File
@@ -1,10 +0,0 @@
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>;
}
+5 -15
View File
@@ -23,27 +23,17 @@ 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 {
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?;
command
.execute(CommandContext::new(event, rt.clone(), &prefix), parts.args)
.await?;
break;
}
+4
View File
@@ -25,6 +25,10 @@ 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()
}
+24 -1
View File
@@ -1,5 +1,4 @@
use std::sync::Arc;
use twilight_model::{
gateway::payload::incoming::InteractionCreate,
http::interaction::{InteractionResponse, InteractionResponseType},
@@ -7,6 +6,7 @@ use twilight_model::{
};
use crate::framework::{
interactions::messages::InteractionResponder,
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
};
@@ -17,6 +17,11 @@ pub struct InteractionContext<T> {
}
impl<T> InteractionContext<T> {
pub fn send(
self: &Arc<Self>,
) -> InteractionResponder<T> {
InteractionResponder::new(self.clone())
}
pub fn new(event: InteractionCreate, rt: Arc<Runtime<T>>, prefix: String) -> Self {
Self { event, rt, prefix }
}
@@ -39,6 +44,24 @@ 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,
@@ -0,0 +1,19 @@
use crate::framework::interactions::{InteractionError, context::InteractionContext};
use async_trait::async_trait;
use std::fmt::Debug;
use twilight_model::gateway::payload::incoming::InteractionCreate;
#[async_trait]
pub trait MessageCommand<T>: Send + Sync + Debug {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str {
"A Payphone context menu command."
}
async fn execute(
&self,
ctx: InteractionContext<T>,
target_message_id: u64,
) -> Result<(), InteractionError>;
}
@@ -0,0 +1,84 @@
// to be later integrated into interaction registry...currently component registry
use crate::framework::{
interactions::{
InteractionError, context::InteractionContext, message_command::MessageCommand,
},
runtime::Runtime,
};
use scc::HashMap;
use std::sync::Arc;
use twilight_model::{
application::command::CommandType, application::interaction::InteractionData,
gateway::payload::incoming::InteractionCreate,
};
#[derive(Default, Debug)]
pub struct MessageCommandRegistry<T> {
inner: HashMap<String, Arc<dyn MessageCommand<T>>>,
}
impl<T> MessageCommandRegistry<T>
where
T: Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: Default::default(),
}
}
pub fn register(&self, name: impl ToString, command: Arc<dyn MessageCommand<T>>) {
self.inner.upsert(name.to_string(), command);
}
pub fn unregister(&self, name: impl ToString) {
self.inner.remove(&name.to_string());
}
pub async fn execute(
&self,
event: &InteractionCreate,
rt: Arc<Runtime<T>>,
) -> Result<(), InteractionError> {
if let Some(data) = &event.data {
if let InteractionData::ApplicationCommand(data) = data {
if data.kind == CommandType::Message {
if let Some(command) = self.inner.get_async(&data.name).await {
let prefixes = rt
.get_prefixes(
event.guild_id.map(|id| id.get()),
event.author_id().map(|id| id.get()),
)
.await;
let prefix = prefixes
.first()
.ok_or(InteractionError::PrefixesVectorEmpty)?;
// Message commands have a resolved target_id
if let Some(target_id) = data.target_id {
command
.execute(
InteractionContext::new(
event.clone(),
rt.clone(),
prefix.to_string(),
),
target_id.get(),
)
.await
} else {
Err(InteractionError::InteractionNotFound)
}
} else {
Err(InteractionError::InteractionNotFound)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
}
}
@@ -0,0 +1,30 @@
use crate::framework::interactions::InteractionError::InteractionTypeUnsent;
use async_trait::async_trait;
use std::sync::Arc;
use twilight_model::gateway::payload::incoming::InteractionCreate;
use crate::framework::{
events::traits::{EventResult, OnInteractionHandler},
interactions::message_command_registry::MessageCommandRegistry,
runtime::Runtime,
};
#[derive(Debug)]
pub struct MessageCommandRunner<T> {
pub registry: Arc<MessageCommandRegistry<T>>,
}
#[async_trait]
impl<T: Send + Sync + std::fmt::Debug + 'static> OnInteractionHandler<T>
for MessageCommandRunner<T>
{
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult {
if let Err(e) = self.registry.execute(event, rt).await {
// Only log errors that aren't "not a message command"
if !matches!(e, InteractionTypeUnsent) {
log::error!("Message command dispatch error: {e:?}");
}
}
Ok(())
}
}
+40
View File
@@ -0,0 +1,40 @@
use std::sync::Arc;
use twilight_model::http::interaction::{
InteractionResponse, InteractionResponseData, InteractionResponseType,
};
use crate::framework::{
interactions::{InteractionError, context::InteractionContext},
runtime::Runtime,
};
pub struct InteractionResponder<T> {
ctx: Arc<InteractionContext<T>>,
}
impl<T> InteractionResponder<T> {
pub fn new(ctx: Arc<InteractionContext<T>>) -> Self {
Self { ctx }
}
pub async fn content(&self, content: impl Into<String>) -> Result<(), InteractionError> {
self.ctx
.rt
.http
.interaction(self.ctx.rt.app.id)
.create_response(
self.ctx.event.id,
&self.ctx.event.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(InteractionResponseData {
content: Some(content.into()),
..Default::default()
}),
},
)
.await
.map_err(|_| InteractionError::RespondingFailed)?;
Ok(())
}
}
+10 -1
View File
@@ -1,7 +1,10 @@
pub mod message_command_runner;
pub mod messages;
use thiserror::Error;
pub mod components;
pub mod context;
pub mod message_command;
pub mod message_command_registry;
pub mod registry;
pub mod runner;
@@ -21,6 +24,12 @@ pub enum InteractionError {
#[error("The original message was attempted to be edited, but it failed")]
OriginalMessageEditingFailed,
#[error("Responding to the interaction failed")]
RespondingFailed,
#[error("No prefixes were returned for the event")]
PrefixesVectorEmpty,
#[error(transparent)]
DatabaseError(#[from] crate::database::models::DatabaseError),
}
+12 -8
View File
@@ -3,20 +3,24 @@ 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,
use crate::{
framework::{
events::traits::{EventResult, OnInteractionHandler},
runtime::Runtime,
},
state::State,
};
#[derive(Debug)]
pub struct InteractionRunner;
#[async_trait]
impl<T> OnInteractionHandler<T> for InteractionRunner
where
T: Send + Sync + 'static,
{
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult {
impl OnInteractionHandler<State> for InteractionRunner {
async fn on_interaction(
&self,
event: &InteractionCreate,
rt: Arc<Runtime<State>>,
) -> EventResult {
Ok(rt.registry.execute(event, rt.clone()).await?)
}
}
-1
View File
@@ -158,7 +158,6 @@
//! ```
pub mod bot;
pub mod cache;
pub mod commands;
pub mod events;
pub mod interactions;
+2 -52
View File
@@ -1,76 +1,26 @@
//! 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::PrefixGetter, commands::Command, interactions::registry::ComponentRegistry,
bot::IntoPrefixes, 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>,
/// The command prefixes getter.
pub prefixes: Arc<dyn PrefixGetter<T> + Send + Sync + 'static>,
/// The commands available in the application.
pub prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync + 'static>,
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()
-15
View File
@@ -26,7 +26,6 @@ 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(
@@ -78,17 +77,3 @@ 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")
}
}
+86 -218
View File
@@ -1,18 +1,98 @@
use async_trait::async_trait;
use scc::HashSet;
use std::sync::Arc;
use twilight_model::id::Id;
use crate::{
database::models::{Call, Channel},
discord::sanitizing::SanitizeDiscordMarkdown,
framework::commands::{
Command, CommandError, context::CommandContext, permissions::Permission,
database::models::{Call, Channel, Message, MessageRow, Report},
framework::commands::{Command, CommandError, context::CommandContext},
framework::interactions::{
InteractionError, context::InteractionContext, message_command::MessageCommand,
},
groups::calls::{get_display_profile, permissions::NotBannedPermission},
state::State,
utils::Bye,
utils::{Bye, get_random_u64},
};
#[derive(Debug, Default)]
pub struct ReportMessageCommand;
#[async_trait]
impl MessageCommand<State> for ReportMessageCommand {
fn name(&self) -> &'static str {
"Report Message"
}
fn description(&self) -> &'static str {
"Report a message to the moderators."
}
async fn execute(
&self,
ctx: InteractionContext<State>,
target_message_id: u64,
) -> Result<(), InteractionError> {
// Extract channel_id from resolved data or fallback to event.channel (Discord API v10+)
let channel_id = ctx
.event
.channel
.as_ref()
.map(|c| c.id.get())
.or_else(|| ctx.event.channel.as_ref().map(|c| c.id.get()))
.ok_or(InteractionError::InteractionNotFound)?;
let db = &ctx.rt.state.db;
let channel = Channel::get_by_id(channel_id, db)
.await
.map_err(|_| InteractionError::InteractionNotFound)?;
let call_id = channel
.and_then(|c| c.call_id)
.ok_or(InteractionError::InteractionNotFound)?;
// Fetch the message from the database
let message_row = MessageRow::get_by_call_and_message_id(call_id, target_message_id, db)
.await
.map_err(|_| InteractionError::InteractionNotFound)?
.ok_or(InteractionError::InteractionNotFound)?;
// If the message is a reply, fetch the replied-to message and store in replying_to
let replying_to = if let Some(replying_to_id) = message_row.data.replying_to_id {
if let Ok(Some(reply_msg_row)) =
MessageRow::get_by_call_and_message_id(call_id, replying_to_id as u64, db).await
{
Some(reply_msg_row.data)
} else {
None
}
} else {
None
};
// Build the report using the new flat message and replying_to
let report = Report {
report_id: get_random_u64(),
message: message_row.data,
replying_to,
created_at: time::OffsetDateTime::now_utc(),
reviewed_at: None,
created_by_ids: vec![ctx.event.author_id().map(|id| id.get()).unwrap_or(0)],
reviewed_by_id: None,
resolution: None,
};
// Insert the report
report
.insert(db)
.await
.map_err(InteractionError::DatabaseError)?;
// Respond to the user
Arc::new(ctx)
.send()
.content("Message has been reported to the moderators.")
.await?;
Ok(())
}
}
#[derive(Default, Debug)]
pub struct CallCommand {
/// A set of channel IDs that are currently searching for a call. This works as a lock for two
@@ -40,10 +120,6 @@ 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>,
@@ -117,112 +193,6 @@ 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;
@@ -236,10 +206,6 @@ 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>,
@@ -320,101 +286,3 @@ 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(())
}
}
-27
View File
@@ -1,27 +0,0 @@
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")
}
}
-186
View File
@@ -1,186 +0,0 @@
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()
}
+52 -343
View File
@@ -1,91 +1,26 @@
use std::{error::Error, sync::Arc};
use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use itertools::Itertools;
use twilight_model::{
gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate},
id::Id,
};
use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource};
use linkify::LinkFinder;
use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id};
use twilight_validate::request::webhook_username;
use crate::{
constants::COLOR_WHITE,
database::models::{Call, Channel, Message, User, UserIncident},
discord::sanitizing::SanitizeDiscordMarkdown,
database::models::{Call, Channel},
discord::{images, sanitizing::SanitizeDiscordMarkdown},
framework::{
events::traits::{
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
},
interactions::components::ActionRow,
events::traits::{EventResult, OnMessageHandler},
runtime::Runtime,
},
groups::calls::{
components::{CodeOfConductButton, NeedHelpButton},
embeds, get_display_profile,
moderation::{Action, Rule, escape_message},
},
groups::calls::moderation::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 CallMessageHandler;
pub struct MessageRedirector;
#[async_trait]
impl OnMessageHandler<State> for CallMessageHandler {
impl OnMessageHandler<State> for MessageRedirector {
async fn on_message(&self, event: &MessageCreate, rt: Arc<Runtime<State>>) -> EventResult {
if event.author.bot || event.author.system.unwrap_or(false) {
return Ok(());
@@ -141,16 +76,6 @@ impl OnMessageHandler<State> for CallMessageHandler {
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,
@@ -175,275 +100,59 @@ impl OnMessageHandler<State> for CallMessageHandler {
let profile = get_display_profile(event);
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(
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:?}",
call.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",
opposite_channel.id
);
} 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(())
}
}
#[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(())
}
struct DisplayProfile {
pub name: String,
pub avatar_url: String,
}
#[derive(Debug)]
pub struct CallMessageDeleteHandler;
fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
let member = event.member.clone();
#[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(());
}
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick) {
if webhook_username(nick).is_err() {
"Invalid Nickname".to_string()
} 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 {
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?;
}
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()
};
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,
);
DisplayProfile { name, avatar_url }
}
@@ -85,12 +85,4 @@ faping
masturbate
masturbating
masterbating
masterbaiting
horny
hrny
whore
whre
tickle your pickle
tickle yr pickle
tickle his pickle
tickle my pickle
masterbaiting
@@ -44,6 +44,4 @@ get the fuck out
pedo
pdf
pdfile
blackie
motherfucker
mf
blackie
+1 -6
View File
@@ -8,9 +8,4 @@ fuh
fuck you
f u
f you
fuck u
damn
dmn
shit
sht
ass
fuck u
@@ -1 +0,0 @@
lkrkfejnkjrngkjngrkjgenkgejnrkjgnknj
@@ -1 +0,0 @@
slap that ass
-56
View File
@@ -1,59 +1,3 @@
//! 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,
}
}
+52 -436
View File
@@ -13,97 +13,26 @@
//! | 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 | | Soft censor + warn |
//! | 8 | Non-english chats | | Block immediately + penalty |
//! | 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::channel::Message;
use twilight_model::gateway::payload::incoming::MessageCreate;
use unicode_segmentation::UnicodeSegmentation;
use unidecode::unidecode;
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
/// 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>>) {
pub fn escape_message(event: &MessageCreate) -> String {
let mut message = event.content.to_string();
for mention in &event.mentions {
@@ -124,42 +53,34 @@ pub fn escape_message(event: &Message) -> (String, Vec<Arc<dyn Rule>>) {
);
}
let (censored, rules) = moderate_message(&message);
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
log::debug!("Escaped and normalized message: {censored:?}");
for link in finder.links(&message.clone()) {
let url = link.as_str();
(censored, rules)
message = message.replace(url, &"\\#".repeat(url.len()));
}
let normalized = normalize(message.clone());
log::debug!("Escaped and normalized message: {normalized:?}");
message.sanitize(false)
}
/// 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 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 from normalized start indices to original start indices (indices are grapheme
/// indices).
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'),
@@ -187,52 +108,27 @@ 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() {
if char.is_alphanumeric() {
normalized.push(char);
} else {
normalized.push(' ');
mappings.push(i);
}
mappings.push(i);
}
}
NormalizedMessage {
message: normalized.to_ascii_lowercase(),
message: normalized,
mappings,
}
}
/// 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();
for (i, ch) in phrase.replace(" ", "").chars().enumerate() {
if i == phrase.len() - 1 {
pattern.push_str(&format!("{ch}+"));
} else {
pattern.push_str(&format!("{ch}+\\s*"));
}
for ch in phrase.replace(" ", "").chars() {
pattern.push_str(&format!("{ch}+\\s*"));
}
let pattern = format!("\\b{pattern}\\b");
@@ -240,13 +136,6 @@ 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();
@@ -261,151 +150,49 @@ fn regex_from_phrases_list(phrases: &[&str]) -> Regex {
Regex::new(&pattern).expect("Failed to compile regex")
}
/// 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> {
pub fn analyze_message(message: &str) -> Vec<(Vec<String>, Arc<dyn Rule>)> {
let mut results = Vec::new();
for rule in RULES.iter() {
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()));
let detections = rule.detect(message);
if !detections.is_empty() {
results.push((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: &[RuleAnalysis],
analysis: &[(Vec<String>, Arc<dyn Rule>)],
) -> String {
let mut censored = original.to_string();
// Collect all ranges to censor
let mut ranges = Vec::new();
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());
ranges.push((start_index, end_index));
for (findings, rule) in analysis {
for finding in findings {
while let Some(start) = normalized.message.find(finding) {
let end = start + finding.len();
}
}
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));
}
}
}
// Sort ranges in reverse order to avoid shifting indices
ranges.sort_by(|a, b| b.0.cmp(&a.0));
for (start_index, end_index) in ranges {
if start_index < end_index && end_index <= censored.len() {
censored.replace_range(start_index..end_index, "[censored]");
}
}
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(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;
/// 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>);
fn detect(&self, message: &str) -> 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()
@@ -442,14 +229,11 @@ impl Rule for HateSpeechAndSlursRule {
&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(),
)
fn detect(&self, message: &str) -> Vec<String> {
self.0
.find_iter(message)
.map(|m| message[m.start()..m.end()].to_string())
.collect()
}
}
@@ -475,14 +259,11 @@ impl Rule for ThreatsAndSelfHarmRule {
&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(),
)
fn detect(&self, message: &str) -> Vec<String> {
self.0
.find_iter(message)
.map(|m| message[m.start()..m.end()].to_string())
.collect()
}
}
@@ -508,14 +289,11 @@ impl Rule for ExplicitSexualLanguageRule {
"Explicit Sexual Language"
}
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(),
)
fn detect(&self, message: &str) -> Vec<String> {
self.0
.find_iter(message)
.map(|m| message[m.start()..m.end()].to_string())
.collect()
}
}
@@ -540,181 +318,19 @@ impl Rule for SuggestiveLanguageRule {
"Suggestive Language"
}
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(),
)
fn detect(&self, message: &str) -> Vec<String> {
self.0
.find_iter(message)
.map(|m| message[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(ExplicitSexualLanguageRule::new()),
Arc::new(SuggestiveLanguageRule::new()),
Arc::new(AdvertisingRule),
Arc::new(SensitiveTopicsAndTriggeringContentRule::new()),
Arc::new(MildSwearWordsRule::new()),
Arc::new(CasualInsultsAndTeasing::new()),
Arc::new(LinksRule),
Arc::new(ExplicitSexualLanguageRule::new()),
]
});
-37
View File
@@ -1,37 +0,0 @@
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)
}
}
}
-4
View File
@@ -1,5 +1,3 @@
//! Event handling for the gateway sub-module.
use std::sync::Arc;
use async_trait::async_trait;
@@ -13,7 +11,6 @@ use crate::{
state::State,
};
/// Logs a ready message when the bot is ready.
#[derive(Debug)]
pub struct ReadyHandler;
@@ -30,7 +27,6 @@ impl OnReadyHandler<State> for ReadyHandler {
}
}
/// Logs a reconnection message when the gateway asks for a reconnection.
#[derive(Debug)]
pub struct ReconnectHandler;
-2
View File
@@ -1,3 +1 @@
//! Logging gateway events.
pub mod events;
-16
View File
@@ -1,19 +1,3 @@
//! 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;
-83
View File
@@ -1,83 +0,0 @@
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(())
}
}
-5
View File
@@ -1,5 +1,3 @@
//! The event handlers for the servers sub-module.
use std::sync::Arc;
use async_trait::async_trait;
@@ -16,7 +14,6 @@ use crate::{
state::State,
};
/// Adds servers to the DB when they get created.
#[derive(Debug)]
pub struct ServerCreateHandler;
@@ -46,7 +43,6 @@ impl OnServerCreateHandler<State> for ServerCreateHandler {
}
}
/// Updates DB server data when a server gets updated.
#[derive(Debug)]
pub struct ServerUpdateHandler;
@@ -78,7 +74,6 @@ impl OnServerUpdateHandler<State> for ServerUpdateHandler {
}
}
/// Deletes servers from the DB when they get deleted or payphone is kicked.
#[derive(Debug)]
pub struct ServerDeleteHandler;
-7
View File
@@ -1,8 +1 @@
//! 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;
-36
View File
@@ -1,36 +0,0 @@
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)
}
}
-9
View File
@@ -1,9 +0,0 @@
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;
+22 -85
View File
@@ -107,36 +107,27 @@ use std::{
sync::mpsc,
};
use async_trait::async_trait;
use crate::{
cli::{Args, Commands},
groups::calls::commands::ReportMessageCommand,
framework::bot::Bot,
groups::{
about::{commands::HelpCommand, components::HelpGoBackButton},
calls::{
commands::{CallCommand, HangupCommand},
events::MessageRedirector,
},
gateway::events::{ReadyHandler, ReconnectHandler},
servers::events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
},
state::State,
};
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, ShardId};
use crate::{
cli::{Args, Commands},
constants::DEFAULT_PREFIX,
database::models::Server,
framework::{
bot::{Bot, PrefixGetter},
runtime::Runtime,
},
groups::{
about::{commands::HelpCommand, components::HelpGoBackButton},
calls::{
commands::{CallCommand, FriendRequestCommand, HangupCommand, SkipCommand},
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler},
},
gateway::events::{ReadyHandler, ReconnectHandler},
servers::{
commands::PrefixCommand,
events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
},
},
state::State,
};
use twilight_gateway::Intents;
mod cli;
mod constants;
@@ -148,10 +139,6 @@ 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();
@@ -165,18 +152,8 @@ pub async fn main() {
Commands::Start {
token,
database_addr,
database_username,
database_password,
shard,
} => {
start(
token,
database_addr,
database_username,
database_password,
shard.into(),
)
.await;
start(token, database_addr).await;
}
Commands::Docs { bind, open } => {
docs(bind, open).await;
@@ -185,45 +162,14 @@ pub async fn main() {
}
}
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,
) {
async fn start(token: String, database_addr: String) {
log::info!("Welcome to Payphone, Nyeki!");
log::info!(
"Payphone is running on version v{}",
env!("CARGO_PKG_VERSION")
);
let db = database::init(database_addr, database_username, database_password)
let db = database::init(database_addr)
.await
.expect("Could not connect to the database!");
@@ -231,23 +177,18 @@ async fn start(
let state = State { db };
let bot = Bot::new(state, ServerPrefixOrPayphoneMention)
.shard(shard)
let bot = Bot::new(state, ["p."])
.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)
.message_command(ReportMessageCommand)
.on_ready(ReadyHandler)
.on_reconnect(ReconnectHandler)
.on_message(CallMessageHandler)
.on_message_edit(CallMessageEditHandler)
.on_message_delete(CallMessageDeleteHandler)
.on_message(MessageRedirector)
.on_server_create(ServerCreateHandler)
.on_server_update(ServerUpdateHandler)
.on_server_delete(ServerDeleteHandler);
@@ -255,10 +196,6 @@ async fn start(
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...");
+1
View File
@@ -19,6 +19,7 @@
//! ```
use scylla::client::session::Session;
#[derive(Debug)]
pub struct State {
pub db: Session,
-55
View File
@@ -1,13 +1,6 @@
//! 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;
}
@@ -45,8 +38,6 @@ 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.
@@ -54,49 +45,3 @@ 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())
}
Generated
-667
View File
@@ -1,667 +0,0 @@
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" },
]