diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index f7fae1e..0000000 --- a/.dockerignore +++ /dev/null @@ -1,166 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -k8s/ -.vscode/ -.git/ \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index efa407c..0000000 --- a/.gitignore +++ /dev/null @@ -1,162 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file diff --git a/.python-version b/.python-version deleted file mode 100644 index e4fba21..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 66dc29a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "pylint.args": ["--extension-pkg-allow-list=orjson"], - "conventionalCommits.scopes": ["shard", "tasks", "kube", "scripts"] -} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7e09ff3..0000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM registry.nyeki.dev/docker/python/with-uv:3.12-alpine AS base - - -WORKDIR /app -COPY pyproject.toml uv.lock /app/ - -RUN python3 -m uv sync --no-dev - -COPY . /app/ - - -FROM base AS shard - -CMD [ "bot.py" ] - - -FROM base AS tasks - -CMD [ "tasks.py" ] diff --git a/README.md b/README.md deleted file mode 100644 index fd30a41..0000000 --- a/README.md +++ /dev/null @@ -1,264 +0,0 @@ -# Payphone - -Payphone is deployed using Kubernetes + Docker. To set up a local kubernetes cluster, you can use -`minikube`. - -## Starting a Cluster - -This section will guide you step by step to create a multi-node cluster or a local development -cluster and start up Payphone. Make sure to have: - -1. The Discord bot's token, either for development or production depending on what you're setting - up the cluster for. -2. You'll probably need at least 5 CPU cores (6 recommended) across all of your machines + 1 core - per shard, 8 GB of ram + 512Mb of RAM per shard, and 30Gb of disk space available. It'll still - run with less, but it'll be way slower. In multi-node environment, these specifications are - cluster-wide, meaning that if you have 3 nodes with 3 cores, that makes it 9 cores in total and - therefore suitable for a multi-shard deployment. Do not run nodes with less than 2 CPU cores, 2 - Gb (ideally 4 Gb) of RAM, and 15 Gb of disk space available. - -### Kubernetes Setup - -First of all, you need to install Kubernetes in the machine where you'll set up either a node or a -whole cluster. - -Make sure to have Docker installed in your machine before running the following steps. You can -check how to install Docker in the -[Docker documentation](https://docs.docker.com/get-started/get-docker/). If you're setting up a new -node for production, it's better if you only install the Docker Engine without Docker Desktop. - -#### For Development - -Ideally, use `minikube` for local development. This'll allow you to run a complete cluster locally. -Make sure to have downloaded and installed minikube first. On Windows, it's available with -`chocolatey` running `choco install minikube`. You can read the full set up guide for other -installation methods at the [official minikube documentation](https://minikube.sigs.k8s.io/docs/). - -After `minikube` is installed, you need to set up a local cluster. This'll create a virtual machine -in a Docker container to act as nodes in your cluster. Run the following command to start a 3-node -local cluster: - -```sh -minikube start --network-plugin=cni --cni=calico --nodes=3 -``` - -This'll create 3 docker containers and start them, and set them up to use kubectl connecting them -automatically. Management `kubectl` commands can only be ran from the master/control-panel node, -but with `kubectl` you don't need to worry about that since running `kubectl` from your machine -will work as if you were in a master node. - -`kubectl` comes integrated with `Docker Desktop`. You can enable Kubernetes in Docker Desktop at -Docker Desktop > Settings > Kubernetes > Enable Kubernetes. - -#### For Production - -These steps are required in order to set up a new `microk8s` node. These installation steps assume -they're being ran in an Ubuntu 22.4+ server with at least 2 CPU cores, 4 GB RAM, and 25 GB disk -space, and `snap` installed. You also need root privileges to follow these steps. - -First of all, install `microk8s`. - -```sh -sudo snap install microk8s --classic -``` - -After the installation, check that `microk8s` is up and running using the following command: - -```sh -microk8s status --wait-ready -``` - -> [!INFO] In case the last command fails to run because of missing permissions, you'll be guided -> on how to give your current user the required permissions to run the command. Follow those steps -> and re-run the command afterwards. - -Then, you need to enable the required extensions for `microk8s` to work properly. Run the following -commands to enable them: - -```sh -microk8s enable dns -microk8s enable ha-cluster -microk8s enable community - -sudo systemctl enable iscsid # Needed to run the following command. - -microk8s enable openebs -``` - -The rest of the guide uses `kubectl`, but `microk8s` has `kubectl` as a subcommand, so either: - -1. You create an alias in `.bash_aliases`, appending `alias kubectl='microk8s kubectl'` to your - `~/.bash_aliases` file. If you don't have one, create an empty file and add the command in it. - After updating the file, run `source ~/.bash_aliases` to apply the changes. - -2. Use `microk8s kubectl` instead of `kubectl` in the following steps. This'll require you to type - `microk8s` at the front of all `kubectl` commands that are run from now on. - -To add an alias, run the following command: - -```sh -echo "alias kubectl='microk8s kubectl'" >> ~/.bash_aliases && source ~/.bash_aliases -``` - -##### Add a Node to the Cluster - -To add a new node to the cluster, run the following command on a master node: - -```sh -microk8s add-node -``` - -You'll get a command to paste in your new node. Copy the one that ends with `--worker` and paste it -in your new node. Wait a few minutes until it connects, and then you're ready to go! No need to -follow the deployment steps if the cluster is already up and running. - -##### SSH Security - -To secure the access to the server, you'll need to follow the following steps: - -###### Disable Password Authentication - -Having SSH password authentication leaves an open door for brute force attacks. Before disabling -it, make sure that the server has your SSH key to prevent from getting locked out after proceeding. - -After you have made sure your keys have been properly configured, edit the SSH configuration file: - -```sh -nano /etc/ssh/ssh_config -``` - -In the file, find the following configurations: -1. `ChallengeResponseAuthentication` -2. `PasswordAuthentication` -3. `UsePAM` - -If they're set in the file, set their value to `no`. After that, restart the SSH service: - -```sh -systemctl restart ssh -``` - -###### Set Up `fail2ban` - -The next step to secure SSH is to set up `fail2ban`, which will rate limit failed attempts to log -in. First, install it in the server: - -```sh -apt update -apt install fail2ban -``` - -Next, make a copy of the configuration file: - -```sh -cd /etc/fail2ban -cp jail.conf jail.local -``` - -You now need to update it to enable SSH protection: - -```sh -nano jail.local -``` - -Find the `[ssh]` section and add the following line (or update it if it already exists): - -```toml -enabled = true -``` - -After updating that line, close the file, enable, and start the service: - -```sh -systemctl enable fail2ban -systemctl start fail2ban - -# Check the status of the service to make sure it was enabled properly. You can exit the status -# screen pressing "q". -systemctl status fail2ban -``` - -## Deploying Payphone - -The following steps show how to pull and install Payphone on a running cluster of nodes. - -### Building and Publishing the Payphone Image - -Payphone's container images are hosted here at `git.nyeki.dev`. To login, run the following -command: - -```sh -docker login git.nyeki.dev -``` - -A new browser tab will open and you'll be prompted for log in. Once you accept, you'll be logged in -to the container registry. - -When you have made an update and want to update the production deployment's image, you need to -build and push the image to the container registry: - -```sh -docker build -t git.nyeki.dev/payphone/payphone:latest . -docker push git.nyeki.dev/payphone/payphone:latest -``` - -### Starting Payphone in the Cluster - -#### Setting Up Config Files - -First of all, you'll need to set up the environment variables that Payphone needs. For this, -you'll need: - -1. A Discord bot token, base64 encoded. You can encode your token at - [www.base64encode.org](https://www.base64encode.org/). -2. A suggestions channel that the bot can write to (its ID). -3. A non-expiring invite to the official server. - -First of all, open `k8s/config.yml`. At the bottom of the file, you'll find a `data` object with -some environment variables definitions. Set `SUGGESTIONS_CHANNEL_ID` and `SERVER_INVITE` and save -the file. - -Then, after you've base64-encoded your token, open `k8s/secrets.yml` and in a similar `data` object -at the bottom of the file, set `TOKEN` to the base64-encoded Discord bot token and save the -changes. - -After the files are updated with their new values, run the following commands to apply these -changes: - -```sh -kubectl apply -f k8s/config.yml -kubectl apply -f k8s/secrets.yml -``` - -If you see a ConfigMap and a Secret being created, that means you've successfully applied the -changes. - -#### Setting Up Redis and PostgreSQL - -Now, you can initialize Redis. This step is pretty straightforward and you can forget about it -later. - -```sh -kubectl apply -f k8s/redis.yml -kubectl apply -f k8s/psql.yml -``` - -#### Setting Up Payphone - -Finally, you need to start the bot's instance. First, log into your `registry.nyeki.dev` account from -`kubectl` (using the same credentials that in `git.nyeki.dev`) with the following command: - -```sh -kubectl create secret docker-registry regcred --docker-server=https://registry.nyeki.dev/v1/ --docker-username= --docker-password= -``` - -After that command is ran, the next step is to update the amount of shards. To set the amount, -open `k8s/bot.yml` and set the total number of shards in both `spec.replicas` and -`spec.template.spec.containers.bot.env.TOTAL_SHARDS.value`. - -Once it's done, you can save and close the file and run the final command. - -```sh -kubectl apply -f k8s/tasks.yml -kubectl apply -f k8s/bot.yml -``` diff --git a/bot.py b/bot.py deleted file mode 100644 index ca1460b..0000000 --- a/bot.py +++ /dev/null @@ -1,8 +0,0 @@ -from payphone import bot -from payphone.core import logging - - -logging.setup("Payphone") - - -bot.run() diff --git a/censored.txt b/censored.txt deleted file mode 100644 index a825c40..0000000 --- a/censored.txt +++ /dev/null @@ -1,469 +0,0 @@ -2g1c -2 girls 1 cup -acrotomophilia -alabama hot pocket -alaskan pipeline -anal -anilingus -anus -apeshit -arsehole -asshole -assmunch -auto erotic -autoerotic -babeland -baby batter -baby juice -ball gag -ball gravy -ball kicking -ball licking -ball sack -ball sucking -bangbros -bangbus -bareback -barely legal -barenaked -bastard -bastardo -bastinado -bbw -bdsm -beaner -beaners -beaver cleaver -beaver lips -beastiality -bestiality -big black -big breasts -big knockers -big tits -bimbos -birdlock -bitch -bitches -black cock -blonde action -blonde on blonde action -blowjob -blow job -blow your load -blue waffle -blumpkin -bollocks -bondage -boner -boob -boobs -booty call -brown showers -brunette action -bukkake -bulldyke -bullet vibe -bullshit -bung hole -bunghole -busty -butt -buttcheeks -butthole -camel toe -camgirl -camslut -camwhore -carpet muncher -carpetmuncher -chocolate rosebuds -cialis -circlejerk -cleveland steamer -clit -clitoris -clover clamps -clusterfuck -cock -cocks -coprolagnia -coprophilia -cornhole -coon -coons -creampie -cum -cumming -cumshot -cumshots -cunnilingus -cunt -darkie -date rape -daterape -deep throat -deepthroat -dendrophilia -dick -dildo -dingleberry -dingleberries -dirty pillows -dirty sanchez -doggie style -doggiestyle -doggy style -doggystyle -dog style -dolcett -domination -dominatrix -dommes -donkey punch -double dong -double penetration -dp action -dry hump -dvda -eat my ass -ecchi -ejaculation -erotic -erotism -escort -eunuch -fag -faggot -fecal -felch -fellatio -feltch -female squirting -femdom -figging -fingerbang -fingering -fisting -foot fetish -footjob -frotting -fuck buttons -fucktards -fudge packer -fudgepacker -futanari -gangbang -gang bang -gay sex -genitals -giant cock -girl on -girl on top -girls gone wild -goatcx -goatse -god damn -gokkun -golden shower -goodpoop -goo girl -goregasm -grope -group sex -g-spot -guro -hand job -handjob -hentai -homoerotic -honkey -hooker -horny -hot carl -hot chick -how to kill -how to murder -huge fat -humping -incest -intercourse -jack off -jail bait -jailbait -jelly donut -jerk off -jigaboo -jiggaboo -jiggerboo -jizz -juggs -kike -kinbaku -kinkster -kinky -knobbing -leather restraint -leather straight jacket -lemon party -livesex -lolita -lovemaking -make me come -male squirting -masturbate -masturbating -masturbation -menage a trois -milf -missionary position -mong -motherfucker -mound of venus -mr hands -muff diver -muffdiving -nambla -nawashi -negro -neonazi -nigga -nigger -nig nog -nimphomania -nipple -nipples -nsfw images -nude -nudity -nutten -nympho -nymphomania -octopussy -omorashi -one cup two girls -one guy one jar -orgasm -orgy -paedophile -paki -panties -panty -pedobear -pedophile -pegging -penis -phone sex -piece of shit -pikey -pissing -piss pig -pisspig -playboy -pleasure chest -pole smoker -ponyplay -poof -poon -poontang -punany -poop chute -poopchute -porn -porno -pornography -prince albert piercing -pthc -pubes -pussy -queaf -queef -quim -raghead -raging boner -rape -raping -rapist -rectum -reverse cowgirl -rimjob -rimming -rosy palm -rosy palm and her 5 sisters -rusty trombone -sadism -santorum -scat -schlong -scissoring -semen -sex -sexcam -sexo -sexy -sexual -sexually -sexuality -shaved beaver -shaved pussy -shemale -shibari -shitblimp -shota -shrimping -skeet -slanteye -slut -s&m -smut -snatch -snowballing -sodomize -sodomy -spastic -spic -splooge -splooge moose -spooge -spread legs -spunk -strap on -strapon -strappado -strip club -style doggy -suicide girls -sultry women -swastika -swinger -tainted love -taste my -tea bagging -threesome -throating -thumbzilla -tied up -tight white -spank -sperm -tit -vagina -vulva -whore -ballsack -blowjob -busty -clitoris -cockring -cunt -deepthroat -dominatrix -ejaculation -fetish -gangbang -genital -jizzed -lube -milf -nudity -pecker -pussylick -semen -shemale -slutty -threesome -titfuck -tit -tits -titties -titty -tongue in a -topless -tosser -towelhead -tranny -tribadism -tub girl -tubgirl -tushy -twat -twink -twinkle -two girls one cup -undressing -upskirt -urethra play -urophilia -vagina -venus mound -viagra -vibrator -violet wand -vorarephilia -voyeur -voyeurweb -voyuer -vulva -wank -wetback -wet dream -white powder -whore -worldsex -wrapping men -wrinkled starfish -xxx -yellow showers -yiffy -zoophilia -๐Ÿ–• -honky -kike -weed -cocaine -heroin -meth -ecstasy -lsd -marijuana -opioid -self-harm -self harm -self-injury -self injury -self-mutilation -self mutilation -hurting myself -abortion -rape -molest -pedophile -incest -abuse -trafficking -domestic violence -child abuse -neglect -victim -heathen -infidel -heretic -blasphemer -pagan -zealot -cultist -fanatic -apostate -idolater -fatty -lardass -twig -beanpole -chubby -porker -skeletor -chunk -blubber -niga -niger -nga \ No newline at end of file diff --git a/dev-compose.yml b/dev-compose.yml deleted file mode 100644 index 99a5379..0000000 --- a/dev-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: payphone - -services: - redis: - image: redis:latest - ports: - - 6379:6379 - - postgresql: - image: postgres:latest - ports: - - 5432:5432 - environment: - - POSTGRES_USER=payphone - - POSTGRES_PASSWORD=payphone - - POSTGRES_DB=payphone \ No newline at end of file diff --git a/k8s/bot.yml b/k8s/bot.yml deleted file mode 100644 index 50d5f0c..0000000 --- a/k8s/bot.yml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: bot - labels: - app: bot -spec: - serviceName: "bot-headless" - replicas: 30 # Adjust this manually together with env.TOTAL_SHARDS down below. - selector: - matchLabels: - app: bot - template: - metadata: - labels: - app: bot - spec: - containers: - - name: bot - image: registry.nyeki.dev/payphone/payphone/shard:latest - imagePullPolicy: "Always" - envFrom: - - configMapRef: - name: bot-config - - secretRef: - name: bot-secrets - env: - - name: SHARD_ID - valueFrom: - fieldRef: - fieldPath: metadata.labels['apps.kubernetes.io/pod-index'] - - name: SHARD_COUNT - value: "30" # Adjust this manually together with spec.replicas up at the top. - resources: - limits: - cpu: "1000m" - memory: "1Gi" - requests: - cpu: "250m" - memory: "256Mi" - imagePullSecrets: - - name: regcred - volumeClaimTemplates: - - metadata: - name: bot-storage - spec: - accessModes: - - ReadWriteOnce # Ensures only one node can mount the volume at a time - storageClassName: openebs-jiva-csi-default - resources: - requests: - storage: 1Gi # Defines the storage size ---- -apiVersion: v1 -kind: Service -metadata: - name: bot-headless - labels: - app: bot -spec: - clusterIP: None - selector: - app: bot diff --git a/k8s/config.yml b/k8s/config.yml deleted file mode 100644 index 5dbf509..0000000 --- a/k8s/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: bot-config -data: - IS_DEBUG: "false" - REDIS_HOST: "redis" - REDIS_PORT: "6379" diff --git a/k8s/psql.yml b/k8s/psql.yml deleted file mode 100644 index 7251720..0000000 --- a/k8s/psql.yml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: postgresql -spec: - type: ClusterIP # Internal access only - selector: - app: postgresql - ports: - - protocol: TCP - port: 5432 - targetPort: 5432 - ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: postgres-storage -spec: - accessModes: - - ReadWriteOnce - storageClassName: openebs-hostpath # Your storage class - resources: - requests: - storage: 20Gi # Adjust storage as needed - ---- -apiVersion: v1 -kind: Pod -metadata: - name: postgresql - labels: - app: postgresql -spec: - containers: - - name: postgres - image: postgres:latest # Standard PostgreSQL image - args: - - -c - - max_connections=2000 - env: - - name: POSTGRES_DB - value: "payphone" - - name: POSTGRES_USER - value: "payphone" - - name: POSTGRES_PASSWORD - value: "payphone" - ports: - - containerPort: 5432 - volumeMounts: - - name: postgres-storage - mountPath: /var/lib/postgresql/data - resources: - requests: - cpu: "1.5" - memory: "3Gi" - limits: - cpu: "2" - memory: "4Gi" - volumes: - - name: postgres-storage - persistentVolumeClaim: - claimName: postgres-storage - restartPolicy: Always diff --git a/k8s/redis.yml b/k8s/redis.yml deleted file mode 100644 index 12227e0..0000000 --- a/k8s/redis.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Redis Pod -apiVersion: v1 -kind: Pod -metadata: - name: redis - labels: - app: redis -spec: - containers: - - name: redis - image: redis:7.4.1-alpine - command: ["redis-server"] - args: ["--maxclients", "10000"] - ports: - - containerPort: 6379 - resources: - requests: - memory: "1Gi" - cpu: "1" - limits: - memory: "2Gi" - cpu: "1" - ---- -# Redis Service -apiVersion: v1 -kind: Service -metadata: - name: redis -spec: - selector: - app: redis - ports: - - protocol: TCP - port: 6379 - targetPort: 6379 - type: ClusterIP diff --git a/k8s/secrets.yml b/k8s/secrets.yml deleted file mode 100644 index 6fd2315..0000000 --- a/k8s/secrets.yml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: bot-secrets -type: Opaque -data: - TOKEN: TVRJMU56ZzVOemMxT1RRM09URTNNekl4TWcuR2J0d0dsLk9IVGJmN2JScVZHa0tTeXNPTzRGUXAzQnV0MmxISUF6dGpuZnhz diff --git a/k8s/tasks.yml b/k8s/tasks.yml deleted file mode 100644 index f786609..0000000 --- a/k8s/tasks.yml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: tasks -spec: - containers: - - name: connect - image: registry.nyeki.dev/payphone/payphone/tasks:latest - command: ["uv", "run", "--no-dev", "tasks.py", "connect"] - envFrom: - - secretRef: - name: bot-secrets - - configMapRef: - name: bot-config - resources: - requests: - memory: "250Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "1" - # - name: expire - # image: registry.nyeki.dev/payphone/payphone/tasks:latest - # command: ["uv", "run", "--no-dev", "tasks.py", "expire"] - # envFrom: - # - secretRef: - # name: bot-secrets - # - configMapRef: - # name: bot-config - # resources: - # requests: - # memory: "250Mi" - # cpu: "100m" - # limits: - # memory: "1Gi" - # cpu: "1" - restartPolicy: Always - imagePullSecrets: - - name: regcred diff --git a/payphone/__init__.py b/payphone/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/bot.py b/payphone/bot.py deleted file mode 100644 index bd4fd92..0000000 --- a/payphone/bot.py +++ /dev/null @@ -1,499 +0,0 @@ -from typing import Literal - -from datetime import datetime, timezone - -import sys - -import discord -from discord.ext import commands -from discord.ext.commands import errors - -from tortoise import expressions - -import redis.asyncio as redis - -import sentry_sdk - -from payphone.db import setup, models -from payphone.core import constants, logging, exceptions, formatting, help_command - - -class Payphone(commands.AutoShardedBot): - log = logging.logger - - redis = redis.Redis( - host=constants.REDIS_HOST, port=constants.REDIS_PORT, decode_responses=True - ) - - def __init__(self): - intents = discord.Intents.default() - intents.guilds = True - intents.guild_messages = True - intents.guild_reactions = True - intents.message_content = True - - super().__init__( - intents=intents, - command_prefix=self.get_prefix, - strip_after_prefix=True, - case_insensitive=True, - help_command=help_command.HelpCommand(), - ) - - self.shard_ids = [constants.SHARD_ID] - self.shard_count = constants.SHARD_COUNT - - for cog in ["calls", "admin", "mods"]: - self.load_extension(f"payphone.cogs.{cog}.cog") - self.log.info("Loaded cog %s.", cog) - - from payphone.core import calls - - self.call_handler = calls.CallHandler(self) - - self.owner_ids = [ - # 657091104877051907, # Anastasia - 1357199907512914051, # Rafael - ] - - async def on_ready(self) -> None: - """Runs when the bot has finished initializing.""" - - self.log.info("Ready! Logged in as %s.", self.user.display_name) - - async def set_presence(self, shard_id: int) -> None: - """Sets the presence of the bot back to online. - - Args: - shard_id (int): The ID of the shard to set the status for. - """ - - await self.change_presence( - activity=discord.CustomActivity( - name=f"Listening to p.help | Shard {shard_id + 1}" - ), - status=discord.Status.online, - shard_id=shard_id, - ) - - async def on_shard_connect(self, _shard_id: int) -> None: - """Runs when the shard connects. - - Args: - shard_id (int): The ID of the shard that connected. - """ - - self.log.info("Shard has connected.") - - async def on_shard_ready(self, shard_id: int) -> None: - """Sets the activity for the shard. - - Args: - shard_id (int): The ID of the initialized shard. - """ - - self.log.info("Shard is ready.") - - await self.set_presence(shard_id) - - async def on_shard_resumed(self, shard_id: int) -> None: - """Sets the activity for the shard. - - Args: - shard_id (int): The ID of the resumed shard. - """ - - self.log.info("Shard has resumed.") - - await self.set_presence(shard_id) - - async def get_or_fetch_channel( - self, channel_id: int - ) -> discord.abc.GuildChannel | discord.abc.PrivateChannel | discord.Thread | None: - """Get's a channel from the bot's cache, or fetches it from Discord if it's not available. - - Args: - channel_id (int): The channel's ID. - - Returns: - (abc.GuildChannel | abc.PrivateChannel | discord.Thread | None): The channel, or None - if not found. - """ - - channel = self.get_channel(channel_id) - - if channel is None: - try: - return await self.fetch_channel(channel_id) - - except (discord.errors.Forbidden, discord.errors.NotFound): - return None - - return channel - - async def get_prefix( - self, message: discord.Message | None = None, server_id: int | None = None - ) -> str | list[str]: - """Returns the prefix for the message. - - The prefix is case-insensitive, meaning that it'll adapt to the message to be valid not - depending of the case. In case that the prefix is invalid, the default or set prefix in its - original case will be returned. - - This method can also be used on a server's raw ID. Either `message` or `server_id` must be - set, but not both. - - Args: - message (discord.Message | None): The message to check the prefix for, or None. - server_id (int | None): The server's ID to get the prefix from. - - Returns: - str: The prefix for the message. - - Raises: - ValueError: Either `message` or `server_id` must be set, but not both. - """ - - default_prefixes = ["p.", "P.", self.user.mention, f"<@!{self.user.id}>"] - - if message is not None and server_id is not None: - raise ValueError( - "Either `message` or `server_id` must be set, but not both." - ) - - if message is None and server_id is None: - raise ValueError("Either `message` or `server_id` must be set.") - - if message is not None and message.guild is None: - return default_prefixes - - if message is not None and message.content is None: - return default_prefixes - - if ( - message is not None - and message.content is not None - and message.content.startswith(self.user.mention) - ): - return default_prefixes - - server_id = server_id if server_id is not None else message.guild.id - - key = f"prefix:{server_id}" - - prefix: str | None = await self.redis.get(key) - - if prefix is None: - server, _ = await models.Server.update_or_create( - ( - {"name": message.guild.name} - if message is not None and message.guild is not None - else None - ), - id=server_id, - ) - - await self.redis.set(key, server.prefix, ex=60 * 60) - - prefix = server.prefix - - if message is not None and message.content.lower().startswith(prefix.lower()): - return message.content[: len(prefix)] - - return prefix - - def clean_message(self, message: discord.Message) -> str: - """Cleans a message. - - Args: - message (discord.Message): The message to clean. - call (Call): The call the message is in. - - Returns: - (str): The cleaned message. - """ - - content = message.content - - content = content.replace( - "@everyone", f"@{constants.INVISIBLE_CHARACTER}everyone" - ).replace("@here", f"@{constants.INVISIBLE_CHARACTER}here") - - for mention in message.mentions: - content = content.replace( - f"<@{mention.id}>", f"@**{mention.display_name}**" - ) - - for mention in message.role_mentions: - content = content.replace(f"<@{mention.id}>", f"@**{mention.name}**") - - for mention in message.channel_mentions: - content = content.replace(f"<@{mention.id}>", f"#**{mention.name}**") - - return content - - async def is_moderator(self, user_id: int) -> bool: - """Checks whether a user is a moderator. - - Args: - user_id (int): The user to check's ID. - - Returns: - (bool): Whether the user is a moderator. - """ - - user = await models.User.get_cache_or_db(self.redis, user_id) - - return user.is_moderator - - async def is_banned(self, user_id: int) -> bool: - """Checks whether a user is banned. - - Checks whether a user is banned, and caches the result in Redis. The cache expires in 5 - minutes. - - Args: - user_id (int): The user to check's ID. - - Returns: - bool: Whether the user is banned or not. - """ - - key = f"banned:{user_id}" - - is_banned = await self.redis.get(key) - - if is_banned is not None: - return bool(int(is_banned)) - - ban = await models.Ban.filter( - expressions.Q(user_id=user_id), - expressions.Q( - expires_at=None, - expires_at__gt=datetime.now(timezone.utc), - join_type="OR", - ), - ).first() - - await self.redis.set( - key, int(ban is not None), exat=ban.expires_at if ban else None - ) - - return is_banned - - async def ban( - self, - user_id: int, - report: models.Report | None, - expires_at: datetime | None, - created_by_id: int | None, - reason: str | None = None, - ) -> models.Ban: - """Bans a user. - - Args: - user_id (int): The user to ban's ID. - report (Report | None): The report that caused the ban. - expires_at (datetime | None): When the ban expires. - created_by_id (int | None): The ID of the user who created the ban. - reason (str | None): The reason for the ban. - - Returns: - (Ban): The ban. - """ - - ban = models.Ban( - user_id=user_id, - report=report, - expires_at=expires_at, - created_by_id=created_by_id, - reason=reason, - ) - - await ban.save() - - await models.User.filter(id=user_id).update( - bans_count=expressions.F("bans_count") + 1 - ) - - await self.redis.set(f"banned:{user_id}", 1, exat=expires_at) - - return ban - - async def unban( - self, user_id: int, expired_by: int | None, send_notification: bool = True - ) -> None: - """Unbans a user. - - Args: - user_id (int): The user to unban's ID. - expired_by (int | None): The user who expired the ban. - send_notification (bool): Whether to send a notification to the user or not. - """ - - await models.Ban.filter( - ( - expressions.Q(expires_at__gt=datetime.now(timezone.utc)) - | expressions.Q(expires_at=None) - ) - & expressions.Q(user_id=user_id) - ).update(expires_at=datetime.now(timezone.utc), expired_by_id=expired_by) - - await self.redis.delete(f"banned:{user_id}") - - if send_notification: - try: - await (await self.get_or_fetch_user(user_id)).send( - f"๐Ÿ”” You have been unbanned from Payphone{f" by <@{expired_by}>" if expired_by else ""}." - ) - - except discord.DiscordException: - pass - - async def on_command_error( - self, context: commands.Context, exception: commands.CommandError - ) -> None: - """Handles each of the errors raised by commands. - - Args: - context (commands.Context): The context of the command. - exception (commands.CommandError): The exception raised. - """ - - if isinstance(exception, exceptions.ChannelDisabledError): - await context.send( - "โŒ You cannot run commands in this channel.", delete_after=20.0 - ) - - elif isinstance(exception, exceptions.BannedError): - await context.send("โŒ You have been banned from Payphone.") - - elif isinstance(exception, exceptions.NotOnCallError): - await context.send("โŒ You need to be on a call to run this command.") - - elif isinstance(exception, exceptions.NotAModeratorError): - await context.send("โŒ You're not a bot moderator.") - - elif isinstance(exception, errors.MissingRequiredArgument): - await context.send( - f"โŒ `{exception.param.name}` is missing. Use `{context.clean_prefix}help {context.command.name}` to learn how to use it." - ) - - elif isinstance(exception, errors.BotMissingPermissions): - await context.send( - f"โŒ Payphone's missing {formatting.text_join([f"`{perm.replace("_", " ").title()}`" for perm in exception.missing_permissions])} permissions to run this command." - ) - - elif isinstance(exception, errors.NoPrivateMessage): - await context.send("โŒ This command can only be run in a server.") - - elif isinstance(exception, errors.MissingPermissions): - await context.send("โŒ You don't have permission to run this command.") - - elif isinstance(exception, errors.CommandOnCooldown): - await context.send( - f"โŒ You're going too fast! Wait `{int(exception.retry_after)}` seconds before trying again." - ) - - elif isinstance(exception, errors.NotOwner): - await context.send("โŒ You're not the owner of this bot.") - - elif isinstance(exception, exceptions.AccountTooYoungError): - await context.send( - "โŒ Your account is too young to use Payphone. It must be at least " - f"{exception.days} days old to be able to run commands." - ) - - elif isinstance(exception, errors.CommandNotFound): - return - - else: - sentry_sdk.capture_exception(exception) - return await super().on_command_error(context, exception) - - async def on_error(self, *args, **kwargs): - sentry_sdk.capture_exception(sys.exc_info()) - - @property - def embed_author(self) -> discord.EmbedAuthor: - """The author of the embeds.""" - - return discord.EmbedAuthor( - name=self.user.display_name, - icon_url=self.user.display_avatar.url, - ) - - @commands.check - async def is_channel_enabled(self, ctx: commands.Context) -> Literal[True]: - """Checks whether the current channel is enabled to run a command or not. - - Args: - ctx (commands.Context): The current context. - - Raises: - exceptions.ChannelDisabledError: The current channel is disabled. - """ - - if ctx.guild is None: - return True - - if ctx.command.qualified_name in ["config enable", "config disable"]: - return True - - server_id = ctx.guild.id - channel_id = ctx.channel.id - - channels = await self.redis.hgetall(f"channels:{server_id}") - - if str(channel_id) not in channels: - channel, _ = await models.Channel.get_or_create( - server_id=server_id, id=channel_id - ) - - if channel.is_enabled: - await self.redis.hset(f"channels:{server_id}", channel_id, 1) - return True - - channels = await models.Channel.filter(server_id=server_id, is_enabled=True) - - if not channels: - await self.redis.hset(f"channels:{server_id}", channel_id, 1) - return True - - await self.redis.hset(f"channels:{server_id}", channel_id, 0) - raise exceptions.ChannelDisabledError() - - if channels[str(channel_id)] == "1": - return True - - raise exceptions.ChannelDisabledError() - - -def run(): - """Runs the bot.""" - - sentry_sdk.init( - constants.SENTRY_DSN, - environment="development" if constants.IS_DEBUG else "production", - server_name=f"Shard #{constants.SHARD_ID + 1}", - attach_stacktrace=True, - send_default_pii=True, - enable_tracing=True, - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - release=constants.VERSION, - ) - - bot = Payphone() - - try: - import uvloop - - uvloop.install() - - except ImportError: - pass - - bot.loop.run_until_complete(setup.init()) - - bot.run(constants.TOKEN) diff --git a/payphone/cogs/__init__.py b/payphone/cogs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/cogs/admin/__init__.py b/payphone/cogs/admin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/cogs/admin/cog.py b/payphone/cogs/admin/cog.py deleted file mode 100644 index 68a026f..0000000 --- a/payphone/cogs/admin/cog.py +++ /dev/null @@ -1,500 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import discord -from discord.ext import commands, pages - -from tortoise import expressions - -from payphone.db import models -from payphone.bot import Payphone -from payphone.cogs import base -from payphone.cogs.admin import embeds, views -from payphone.core import checks, constants, formatting - - -class Admin(base.Cog): - """Administration in Payphone.""" - - show_in_help = False - - def __init__(self, bot: Payphone) -> None: - """Initializes the Calls cog. - - Args: - bot (Payphone): The bot instance. - """ - - self.bot = bot - - @commands.command(aliases=["mod"], extras={"help_name": "Set Moderator Status"}) - @commands.is_owner() - async def moderator( - self, ctx: commands.Context, user: discord.User, toggle: bool | None = None - ) -> None: - """Gives the user the Moderator role. - - Options: - - ``: The user to give the role. - - `[toggle]`: Whether to give or remove the role. - - Permissions: - - Bot Owner - """ - - user: models.User = await models.User.get_or_none(id=user.id) - - if user is None: - return await ctx.send("โŒ User not found.") - - user.is_moderator = toggle if toggle is not None else not user.is_moderator - await user.save() - await user.set_cache(self.bot.redis) - - await ctx.send( - f"โœ… Toggled the moderator role {'on' if user.is_moderator else 'off'} for <@{user.id}>.", - allowed_mentions=discord.AllowedMentions(users=False), - ) - - @commands.command(aliases=["b"]) - @checks.is_moderator() - async def ban( - self, - ctx: commands.Context, - user: discord.User | int | None = None, - duration: str | None = None, - *, - reason: str | None = None, - ) -> None: - """Bans the user from the bot. - - Options: - - `[user]`: The user to ban or their ID. If none is provided, the author will be banned. - - `[duration]`: The duration of the ban (e.g 5d, 4h, 1M). If none is provided, the ban will be permanent. - - `[reason]`: The reason for the ban. - - Permissions: - - Bot Moderator - """ - - user_id = user if isinstance(user, int) else user.id if user else ctx.author.id - - if duration: - try: - duration = formatting.parse_timedelta(duration) - - except ValueError: - reason = f"{duration} {reason}" if reason else duration - duration = None - - await self.bot.ban( - user_id, - None, - datetime.now(timezone.utc) + duration if duration else None, - ctx.author.id, - reason, - ) - - await ctx.send(f"<@!{user_id}> was banned from Payphone.") - - @commands.command(aliases=["ub"]) - @checks.is_moderator() - async def unban( - self, ctx: commands.Context, user: discord.User | None = None - ) -> None: - """Unbans the user from the bot. - - Options: - - `[user]`: The user to unban. If none is provided, the author will be unbanned. - - Permissions: - - Bot Moderator - """ - - user = user or ctx.author - - await self.bot.unban(user.id, ctx.author.id) - - await ctx.send(f"{user.mention} was unbanned from Payphone.") - - user: models.User = await models.User.get(id=user.id) - await user.set_cache(self.bot.redis) - - @commands.command(aliases=["sup"], extras={"help_name": "Supporter Perks"}) - @commands.is_owner() - async def supporter( - self, ctx: commands.Context, user: discord.User, months: int = 1 - ) -> None: - """Gives the user the Supporter role. - - Options: - - ``: The user to give the role. - - `[months]`: The number of months to give the role. - - Permissions: - - Bot Owner - """ - - instance, _ = await models.User.update_or_create( - id=user.id, - defaults={ - "is_supporter_until": ( - datetime.now(timezone.utc) + timedelta(days=30 * months) - if months > 0 - else None - ) - }, - ) - await instance.set_cache(self.bot.redis) - - if months: - await ctx.send( - f"Gave the Supporter role to <@{user.id}> for {months} months." - ) - - else: - await ctx.send(f"Removed the Supporter role from <@{user.id}>.") - - @commands.command(aliases=["rs"]) - @checks.is_moderator() - async def reports(self, ctx: commands.Context) -> None: - """Shows the reports. - - Permissions: - - Bot Moderator - """ - - report = ( - await models.Report.filter(resolved=False) - .select_related("reported_by") - .first() - ) - remaining_reports = await models.Report.filter(resolved=False).count() - - if report is None: - return await ctx.send("โœ… No reports left, have this cookie ๐Ÿช !!") - - await ctx.send( - embed=embeds.report_embed(report), - view=views.ReportView(self.bot, report, (0, remaining_reports)), - ) - - @commands.command(aliases=["bs", "ws", "bans", "warns", "warnings"]) - @checks.is_moderator() - async def record( - self, ctx: commands.Context, user: discord.User | int | None = None - ) -> None: - """Shows all bans and warns of a specific user. - - Options: - - `[user]`: The user to show the bans of. - - Permissions: - - Bot Moderator - """ - - user_id = user if isinstance(user, int) else user.id if user else ctx.author.id - - user: models.User | None = await models.User.get_or_none(id=user_id) - - if user is None: - return await ctx.send("โŒ User not found.") - - bans = ( - await models.Ban.filter(user_id=user_id) - .prefetch_related("report", "created_by", "expired_by") - .order_by("-created_at") - ) - warns = ( - await models.Warn.filter(user_id=user_id) - .prefetch_related("report", "created_by") - .order_by("-created_at") - ) - - if user.bans_count != len(bans): - user.bans_count = len(bans) - await user.save() - - if not bans and not warns: - if user.bans_count or user.warns_count: - await ctx.send( - f"โ“ <@{user_id}> has no bans nor warns recorded, but they have been banned" - f" `{user.bans_count}` times and warned `{user.warns_count}` times.", - allowed_mentions=discord.AllowedMentions(users=False), - ) - return - - await ctx.send(f"<@!{user_id}> hasn't been banned nor warned.") - return - - ban_embeds = [ - ( - discord.Embed( - color=constants.MODERATOR_COLOR, - title=f"Ban #{ban.id}", - description=formatting.trim(ban.report.message_content, 500), - author=discord.EmbedAuthor( - name=ban.report.user_name, - icon_url=ban.report.user_avatar_url, - ), - fields=[ - discord.EmbedField( - name="Banned Until", - value=( - f" ()" - if ban.expires_at - else "Forever" - ), - ), - discord.EmbedField( - name="User Avatar", - value=f"[Click to open avatar URL](<{ban.report.user_avatar_url}>)", - ), - ] - + ( - [ - discord.EmbedField( - name="Replying To", - value=f'"{formatting.trim(ban.report.reply_message_content)}" by {ban.report.reply_user_name}', - ) - ] - if ban.report.reply_message_content - else [] - ), - thumbnail=ban.report.user_avatar_url, - footer=discord.EmbedFooter( - text=f"Banned by {(await self.bot.get_or_fetch_user(ban.created_by.id)).display_name}" - + ( - f" ยท Unbanned by {(await self.bot.get_or_fetch_user(ban.expired_by.id)).display_name}" - if ban.expired_by - else "" - ) - + f" ยท {user.warns_count}w, {user.bans_count}b" - ), - ) - if ban.report - else discord.Embed( - color=constants.MODERATOR_COLOR, - title=f"Ban #{ban.id}", - description=f"The user was banned manually by <@!{ban.created_by.id}> . {(f"It expires " if not ban.is_expired else f"It expired ") if ban.expires_at else "It doesn't expire"}.", - fields=[ - discord.EmbedField( - name="Reason", value=ban.reason or "No reason provided." - ) - ], - footer=discord.EmbedFooter( - text=f"Banned by {(await self.bot.get_or_fetch_user(ban.created_by.id)).display_name}" - + ( - f" ยท Unbanned by {(await self.bot.get_or_fetch_user(ban.expired_by.id)).display_name}" - if ban.expired_by - else "" - ) - + f" ยท {user.warns_count}w, {user.bans_count}b" - ), - ) - ) - for ban in bans - ] - - warn_embeds = [ - ( - discord.Embed( - color=constants.MASK_COLOR, - title=f"Warn #{warn.id}", - description=formatting.trim(warn.report.message_content, 500), - author=discord.EmbedAuthor( - name=warn.report.user_name, - icon_url=warn.report.user_avatar_url, - ), - fields=[ - discord.EmbedField( - name="User Avatar", - value=f"[Click to open avatar URL](<{warn.report.user_avatar_url}>)", - ), - ] - + ( - [ - discord.EmbedField( - name="Replying To", - value=f'"{formatting.trim(warn.report.reply_message_content)}" by {warn.report.reply_user_name}', - ) - ] - if warn.report.reply_message_content - else [] - ), - thumbnail=warn.report.user_avatar_url, - footer=discord.EmbedFooter( - text=f"Warned by {(await self.bot.get_or_fetch_user(warn.created_by.id)).display_name} ยท {user.warns_count}w, {user.bans_count}b" - ), - ) - if warn.report - else discord.Embed( - color=constants.MASK_COLOR, - title=f"Warn #{warn.id}", - description=f"The user was warned manually by <@!{warn.created_by.id}> .", - fields=[ - discord.EmbedField( - name="Reason", value=warn.reason or "No reason provided." - ) - ], - footer=discord.EmbedFooter( - text=f"Warned by {(await self.bot.get_or_fetch_user(warn.created_by.id)).display_name} ยท {user.warns_count}w, {user.bans_count}b" - ), - ) - ) - for warn in warns - ] - - await pages.Paginator( - ban_embeds + warn_embeds, - author_check=True, - ).send(ctx) - - @commands.command(aliases=["rmw"], extras={"help_name": "Remove Warning"}) - @checks.is_moderator() - async def remove_warning(self, ctx: commands.Context, warn_id: int) -> None: - """Delete a specific warning. - - Options: - - ``: The ID of the warning to delete. - - Permissions: - - Bot Moderator - """ - - warn = await models.Warn.get_or_none(id=warn_id) - - if warn is None: - return await ctx.send("โŒ Warning not found.") - - await models.User.filter(id=warn.user_id).update( - warns_count=expressions.F("warns_count") - 1 - ) - - await warn.delete() - - user: models.User = await models.User.get_or_none(id=warn.user_id) - - if user: - await user.set_cache(self.bot.redis) - - await ctx.send( - f"โœ… Deleted warning `#{warn.id}` for user <@{warn.user_id}>.", - allowed_mentions=discord.AllowedMentions(users=False), - ) - - @commands.command(aliases=["rmb"], extras={"help_name": "Remove Ban"}) - @checks.is_moderator() - async def remove_ban(self, ctx: commands.Context, ban_id: int) -> None: - """Delete a specific ban. - - Options: - - ``: The ID of the ban to delete. - - Permissions: - - Bot Moderator - """ - - ban = await models.Ban.get_or_none(id=ban_id) - - if ban is None: - return await ctx.send("โŒ Ban not found.") - - await models.User.filter(id=ban.user_id).update( - bans_count=expressions.F("bans_count") - 1 - ) - - await ban.delete() - - user: models.User = await models.User.get_or_none(id=ban.user_id) - - if user: - await user.set_cache(self.bot.redis) - - if ( - not await models.Ban.filter( - expressions.Q( - expires_at__gt=datetime.now(timezone.utc), - expires_at=None, - join_type="OR", - ), - expressions.Q(user_id=ban.user_id), - ).exists() - ) and (ban.expires_at is None or ban.expires_at > datetime.now(timezone.utc)): - await self.bot.unban(ban.user_id, ctx.author.id) - - await ctx.send( - f"โœ… Deleted ban `#{ban.id}` for user <@{ban.user_id}>.", - allowed_mentions=discord.AllowedMentions(users=False), - ) - - @commands.command(aliases=["clr"], extras={"help_name": "Clear Criminal Record"}) - @checks.is_moderator() - async def clear_record( - self, ctx: commands.Context, user: discord.User | int | None = None - ) -> None: - """Clears the warns and bans history of a user. - - Options: - - `[user]`: The user, their ID, or the author of the command if none is specified. - - Permissions: - - Bot Moderator - """ - - user_id = user if isinstance(user, int) else user.id if user else ctx.author.id - - await models.User.filter(id=user_id).update(bans_count=0, warns_count=0) - await models.Ban.filter(user_id=user_id).delete() - await models.Warn.filter(user_id=user_id).delete() - - await self.bot.unban(user_id, ctx.author.id, False) - - user: models.User = await models.User.get_or_none(id=user_id) - - if user: - await user.set_cache(self.bot.redis) - - await ctx.send( - f"โœ… Cleared the record of <@{user_id}>.", - allowed_mentions=discord.AllowedMentions(users=False), - ) - - try: - await (await self.bot.get_or_fetch_user(user_id)).send( - f"โœ… Your record has been cleared by {ctx.author.mention}. You are free to use Payphone again." - ) - - except discord.DiscordException: - pass - - @commands.command(aliases=["l"]) - @checks.is_in_server(constants.PAYPHONE_HUB_ID) - @checks.is_moderator() - async def lock(self, ctx: commands.Context) -> None: - """Closes and locks a thread. - - Permissions: - - Bot Moderator - - In Payphone Hub - """ - - if isinstance(ctx.channel, discord.Thread): - # Keep the message sending above because it'll reopen the ticket otherwise. - await ctx.send("๐ŸŽซ This ticket was closed.") - - return await ctx.channel.edit( - archived=True, - locked=True, - ) - - await ctx.send("โŒ This is not a ticket!") - - -def setup(bot: Payphone) -> None: - """Loads the Admin cog into the bot. - - Args: - bot (Payphone): The bot instance. - """ - - bot.add_cog(Admin(bot)) diff --git a/payphone/cogs/admin/embeds.py b/payphone/cogs/admin/embeds.py deleted file mode 100644 index e0f4829..0000000 --- a/payphone/cogs/admin/embeds.py +++ /dev/null @@ -1,35 +0,0 @@ -import discord - -from payphone.db import models -from payphone.core import constants, formatting - - -def report_embed(report: models.Report) -> discord.Embed: - """Creates an embed for a report. - - Args: - report (models.Report): The report to create an embed for. - - Returns: - (discord.Embed): The embed. - """ - - return discord.Embed( - color=constants.PRIMARY_COLOR, - title="Message Report", - description=( - f"**ID:** {report.id}\n" - f"**User ID**: {report.user_id}\n" - f"**User Name**: {report.user_name}\n" - f"**Channel ID**: {report.channel_id}\n" - f"**Created At**: \n" - ), - fields=[ - discord.EmbedField( - name="Message Content", - value=formatting.trim(report.message_content, 1024), - inline=False, - ) - ], - thumbnail=report.user_avatar_url, - ) diff --git a/payphone/cogs/admin/views.py b/payphone/cogs/admin/views.py deleted file mode 100644 index 3f22f50..0000000 --- a/payphone/cogs/admin/views.py +++ /dev/null @@ -1,264 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import discord - -from payphone.db import models -from payphone.bot import Payphone -from payphone.core import constants, formatting -from payphone.cogs.admin import embeds - - -class ReportView(discord.ui.View): - """The report view.""" - - def __init__( - self, bot: Payphone, report: models.Report, counter: tuple[int, int] - ) -> None: - """Initializes the ReportView. - - Args: - bot (Payphone): The bot. - report (models.Report): The report. - counter (tuple[int, int]): The amount of reports checked during this session, and the - total amount of reports assigned to be checked during this session. - """ - - super().__init__(timeout=None) - - self.add_item( - discord.ui.Button( - label=f"{counter[0]}/{counter[1]}", - disabled=True, - style=discord.ButtonStyle.gray, - row=1, - ) - ) - - self.bot = bot - self.report = report - self.counter = counter - - @discord.ui.select( - placeholder="Select an action", - options=[ - discord.SelectOption( - value="warn", - label="Warn", - description="Send a warning to the user. This is done automatically.", - ), - discord.SelectOption( - value="ban", - label="Ban", - description="Ban the user. The duration of the ban will be applied automatically.", - ), - discord.SelectOption( - value="ban_permanently", - label="Permanent Ban", - description="Ban the user permanently. Use this when the user must not be allowed back.", - ), - ], - ) - async def action_callback( - self, select: discord.SelectMenu, interaction: discord.Interaction - ) -> None: - """Handles the action select.""" - - interacter = await models.User.get_or_none(id=interaction.user.id) - - if interacter is None or not interacter.is_moderator: - return await interaction.response.send_message( - "โŒ You are not allowed to do this. Only moderators can perform moderative actions.", - ephemeral=True, - ) - - self.report.resolved = True - await self.report.save() - - report = ( - await models.Report.filter(resolved=False) - .select_related("reported_by") - .first() - ) - reports_remaining = await models.Report.filter(resolved=False).count() - - if report is None: - self.stop() - - await interaction.response.edit_message( - content=f"โœ… No reports left, have this cookie ๐Ÿช !!\n-# You handled {reports_remaining + self.counter[0] + 1} reports.", - embeds=[], - view=None, - ) - - else: - await interaction.response.edit_message( - embed=embeds.report_embed(report), - view=ReportView( - self.bot, - report, - (self.counter[0] + 1, reports_remaining + self.counter[0] + 1), - ), - ) - - user = await models.User.get_cache_or_db(self.bot.redis, self.report.user_id) - - if select.values[0] == "warn" and user.warns_count + user.bans_count < 3: - user.warns_count += 1 - await user.save(force_update=True) - await user.set_cache(self.bot.redis) - - embed = discord.Embed( - color=constants.MASK_COLOR, - title="Warned", - description=f"You've been warned. You've received `{user.warns_count}` warnings in total.", - fields=[ - discord.EmbedField( - name="Message Content", - value=f"{formatting.trim(self.report.message_content, 500)}", - ), - discord.EmbedField( - name="User Name", - value=self.report.user_name, - ), - ], - thumbnail=self.report.user_avatar_url, - footer=discord.EmbedFooter( - text="โš ๏ธ Repeating this action will result in a ban." - ), - ) - - warn = models.Warn( - user_id=self.report.user_id, - report=self.report, - created_by_id=interaction.user.id, - ) - await warn.save() - - try: - await (await self.bot.get_or_fetch_user(self.report.user_id)).send( - embed=embed - ) - - except discord.Forbidden: - pass - - try: - await ( - await self.bot.get_or_fetch_user(self.report.reported_by.id) - ).send( - f"๐Ÿ”” You've helped us warn `{(await self.bot.get_or_fetch_user(self.report.user_id)).display_name}` with your report. Thank you so much!" - ) - - except discord.Forbidden: - pass - - else: - user.bans_count += 1 - await user.save(force_update=True) - await user.set_cache(self.bot.redis) - - expires_at = None - - if select.values[0] in ["ban", "warn"]: - expiry_times = { - 1: datetime.now(timezone.utc) + timedelta(days=1), - 2: datetime.now(timezone.utc) + timedelta(days=7), - 3: datetime.now(timezone.utc) + timedelta(days=30), - } - - expires_at = expiry_times.get(user.bans_count, None) - - await self.bot.ban( - self.report.user_id, - self.report, - expires_at=expires_at, - created_by_id=interaction.user.id, - ) - - embed = discord.Embed( - color=constants.MODERATOR_COLOR, - title="Banned", - description=f"You've been banned. You've been banned `{user.bans_count}` times.", - fields=[ - discord.EmbedField( - name="Message Content", - value=f"You sent the following message in a call:\n> {formatting.trim(self.report.message_content, 900)}", - ), - discord.EmbedField( - name="User Name", - value=self.report.user_name, - ), - discord.EmbedField( - name="Expires At", - value=( - f" " - if expires_at - else "Never" - ), - ), - ], - thumbnail=self.report.user_avatar_url, - footer=discord.EmbedFooter( - text=( - "โš ๏ธ In case you get banned again, your ban will last longer. Multiple bans will result in a permanent ban." - if expires_at is not None - else "โš ๏ธ You've been permanently banned." - ) - ), - ) - - try: - await (await self.bot.get_or_fetch_user(self.report.user_id)).send( - embed=embed - ) - - except discord.Forbidden: - pass - - try: - await ( - await self.bot.get_or_fetch_user(self.report.reported_by.id) - ).send( - f"๐Ÿ”” You've helped us ban `{(await self.bot.get_or_fetch_user(self.report.user_id)).display_name}` with your report. Thank you so much!" - ) - - except discord.Forbidden: - pass - - @discord.ui.button(label="Skip", style=discord.ButtonStyle.gray) - async def skip_callback( - self, _button: discord.ui.Button, interaction: discord.Interaction - ) -> None: - """Skips the report.""" - - interacter = await models.User.get_or_none(id=interaction.user.id) - - if interacter is None or not interacter.is_moderator: - return await interaction.response.send_message( - "โŒ You are not allowed to do this. Only moderators can perform moderative actions.", - ephemeral=True, - ) - - self.report.resolved = True - await self.report.save() - - report = await models.Report.filter(resolved=False).first() - reports_remaining = await models.Report.filter(resolved=False).count() - - if report is None: - self.stop() - - return await interaction.response.edit_message( - content=f"โœ… No reports left, have this cookie ๐Ÿช !!\n-# You handled {reports_remaining + self.counter[0] + 1} reports.", - embeds=[], - view=None, - ) - - await interaction.response.edit_message( - embed=embeds.report_embed(report), - view=ReportView( - self.bot, - report, - (self.counter[0] + 1, reports_remaining + self.counter[0] + 1), - ), - ) diff --git a/payphone/cogs/base.py b/payphone/cogs/base.py deleted file mode 100644 index 3ae1872..0000000 --- a/payphone/cogs/base.py +++ /dev/null @@ -1,7 +0,0 @@ -from discord.ext import commands - - -class Cog(commands.Cog): - """Base cog with Payphone-specific properties.""" - - show_in_help: bool = True diff --git a/payphone/cogs/calls/__init__.py b/payphone/cogs/calls/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/cogs/calls/cog.py b/payphone/cogs/calls/cog.py deleted file mode 100644 index 94793dd..0000000 --- a/payphone/cogs/calls/cog.py +++ /dev/null @@ -1,685 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import asyncio - -import random - -import discord -from discord.ext import commands - -import aiohttp - -import sentry_sdk - -from payphone.db import models -from payphone.bot import Payphone -from payphone.cogs import base -from payphone.core import constants, censorship, messages, masks, checks, formatting, exceptions - - -class Calls(base.Cog): - """Calls in Payphone.""" - - def __init__(self, bot: Payphone) -> None: - """Initializes the Calls cog. - - Args: - bot (Payphone): The bot instance. - """ - - self.bot = bot - - @commands.command(aliases=["c"]) - @commands.bot_has_permissions( - send_messages=True, - read_messages=True, - read_message_history=True, - use_external_emojis=True, - manage_webhooks=True, - manage_messages=True, - ) - @checks.is_not_banned() - async def call(self, ctx: commands.Context) -> None: - """ - Call and connect with other people. - """ - - if await self.bot.call_handler.is_on_call(ctx.channel.id): - return await ctx.send( - f"โ˜Ž๏ธ You're already on a call! Use `{ctx.clean_prefix}hangup` to hang up." - ) - - if await self.bot.call_handler.is_searching(ctx.channel.id): - return await ctx.send( - f"โ˜Ž๏ธ You're already searching for a call. Use `{ctx.clean_prefix}hangup` to hang up the phone." - ) - - await self.bot.call_handler.set_searching(ctx.channel.id, True) - - await ctx.send( - ( - f"**TIP**: {random.choice(messages.TIPS).replace("{prefix}", ctx.clean_prefix)}\n" - f"โ˜Ž๏ธ {random.choice(messages.CALLING)}" - ) - ) - - @commands.command(aliases=["s"]) - @checks.is_not_banned() - async def skip(self, ctx: commands.Context) -> None: - """ - Skip the current call and search for another one. - """ - - if not await self.bot.call_handler.is_on_call(ctx.channel.id): - return await ctx.send( - f"โ˜Ž๏ธ You're not on a call! Use `{ctx.clean_prefix}call` to start one." - ) - - call = await self.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - - participant = await call.get_participant(ctx.author.id) - - if participant is None: - return await ctx.send( - "โŒ You cannot skip a call you have not participated in.\n" - "-# You didn't send any messages in this call, so you can't skip it." - ) - - channels = await self.bot.call_handler.skip(ctx.channel.id) - - for channel in channels: - if channel == ctx.channel.id: - await ctx.send( - ( - f"**TIP**: {random.choice(messages.TIPS).replace("{prefix}", ctx.clean_prefix)}\n" - "โ˜Ž๏ธ You skipped the call. Searching for another one..." - ) - ) - - else: - await self.bot.get_partial_messageable(channel).send( - f"โ˜Ž๏ธ {random.choice(messages.OTHER_PARTY_HANGUP)}" - ) - - @commands.command(aliases=["h"]) - @checks.is_not_banned() - async def hangup(self, ctx: commands.Context) -> None: - """ - Hang up the call. - """ - - if not await self.bot.call_handler.is_on_call(ctx.channel.id): - if await self.bot.call_handler.is_searching(ctx.channel.id): - return await ctx.send(f"โ˜Ž๏ธ {random.choice(messages.NO_ANSWER)}") - - return await ctx.send( - f"โ˜Ž๏ธ You're not on a call! Use `{ctx.clean_prefix}call` to start one." - ) - - call = await self.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - - participant = await call.get_participant(ctx.author.id) - - if participant is None: - return await ctx.send( - "โŒ You cannot hang up a call you have not participated in.\n" - "-# You didn't send any messages in this call, so you can't hang up. Try saying hi!" - ) - - channels = await self.bot.call_handler.hangup(ctx.channel.id) - - other_channel = channels[0] if channels[0] != ctx.channel.id else channels[1] - - await asyncio.gather( - ctx.send(f"โ˜Ž๏ธ {random.choice(messages.HANGUP)}"), - self.bot.get_partial_messageable(other_channel).send( - f"โ˜Ž๏ธ {random.choice(messages.OTHER_PARTY_HANGUP)}" - ), - ) - - @commands.command(aliases=["fr", "friend"]) - @checks.is_not_banned() - async def friendreq(self, ctx: commands.Context) -> None: - """ - Send a friend request to the other party. - """ - - call = await self.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - - if call is None: - return await ctx.send( - f"โ˜Ž๏ธ You're not on a call! Use `{ctx.clean_prefix}call` to start one." - ) - - other_channel = ( - call.channels[0] if call.channels[0] != ctx.channel.id else call.channels[1] - ) - - await asyncio.gather( - self.bot.get_partial_messageable(other_channel).send( - f"๐Ÿ“ฌ **{ctx.author.display_name}** wants you to send them a friend request on Discord! Their username is `{ctx.author.name}`." - ), - ctx.send("๐Ÿ“ฌ Your friend request has been sent!"), - ) - - @commands.command(aliases=["m"]) - @commands.bot_has_permissions( - send_messages=True, - use_external_emojis=True, - ) - @checks.is_not_banned() - async def mask(self, ctx: commands.Context, toggle: bool | None = None) -> None: - """ - Toggle your mask. - - Options: - - `[toggle]`: Whether to toggle your mask on or off. If no toggle is specified, your mask will be displayed. - """ - - call = await self.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - user = await models.User.get_cache_or_db(self.bot.redis, ctx.author.id) - - if toggle is not None: - user.is_masked = toggle - - await user.save(force_update=True) - await user.set_cache(self.bot.redis) - - message = f"๐ŸŽญ Your mask {"is" if toggle is None else "was set to"} {"`on`" if user.is_masked else "`off`"}." - - if call is not None: - participant = await call.get_participant(ctx.author.id) - - if user.is_masked: - if participant is not None: - message += f" You're currently being displayed as **{masks.get_mask_emoji(participant.mask)} {masks.get_mask_name(participant.mask)}**." - - else: - message += " Participate in the call to see your mask." - - message += ( - " You can toggle your mask on or off using `p.mask on` and `p.mask off`." - ) - - return await ctx.send(message) - - @commands.command(aliases=["r"]) - @commands.bot_has_permissions( - send_messages=True, - ) - @checks.is_not_banned() - async def report(self, ctx: commands.Context) -> None: - """ - Report a message in a call. - """ - - embed = discord.Embed( - color=constants.MASK_COLOR, - title="Reporting has been moved!", - description="๐Ÿ“ This command has been replaced! Payphone has switched to per-message reporting due to the high amount of reports.", - fields=[ - discord.EmbedField( - name="Reporting on Desktop", - value="Right click the message you want to report and, in the opened menu, press `Apps > Report Message`.", - ), - discord.EmbedField( - name="Reporting on Mobile", - value="Press and hold the message you want to report and, in the opened menu, press `Apps > Report Message`.", - ), - ], - ) - - await ctx.send(embed=embed) - - @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: - """ - Handle messages in calls. - """ - - if message.author.created_at > datetime.now(timezone.utc) - timedelta( - days=exceptions.AccountTooYoungError.days - ): - return - - if not isinstance(message.channel, (discord.channel.TextChannel)): - return - - ctx = await self.bot.get_context(message) - - if message.author.bot or ctx.valid or message.author.system: - return - - if message.type not in [discord.MessageType.default, discord.MessageType.reply]: - return - - if await self.bot.is_banned(message.author.id): - return - - call = await self.bot.call_handler.get_call_by_channel_id(message.channel.id) - - if call is None: - return - - participant, was_mask_toggled = await call.set_participant( - message.author.id, - message.author.display_name, - message.author.display_avatar.url, - message.channel.id, - ) - - if participant.display_name.lower() in [ - "everyone", - "here", - "wumpus", - "discord", - ]: - await message.reply( - f"โŒ You can't use {participant.display_name} as your name in calls.\n-# Restriction imposed by Discord." - ) - return - - other_channel = ( - call.channels[0] - if call.channels[0] != message.channel.id - else call.channels[1] - ) - - blocked_section = ( - censorship.censor( - message.content, - participant.user.flag & models.UserFlag.SUPPORTER, - ) - if not (participant.user.flag & models.UserFlag.MODERATOR) - else None - ) - - if blocked_section: - await message.channel.send( - ( - "โŒ Your message wasn't sent because it contained a blocked word or " - f"pattern: `{blocked_section}`" - ), - allowed_mentions=discord.AllowedMentions.none(), - delete_after=10, - ) - await self.bot.get_partial_messageable(other_channel).send( - ( - f"โŒ **{message.author.display_name}** sent a message that was blocked.\n" - "-# If you see an inappropiate message, please report it using `Message > " - "Apps > Report Message`." - ), - allowed_mentions=discord.AllowedMentions.none(), - ) - return - - message_content = self.bot.clean_message(message) - - notices = [] - - if message.stickers: - if participant.user.flag & ( - models.UserFlag.SUPPORTER | models.UserFlag.MODERATOR - ): - sticker = message.stickers[0] - - if sticker.format != discord.StickerFormatType.lottie: - message_content = f"[Sticker: {sticker.name}]({sticker.url})" - - else: - return await message.channel.send( - "๐Ÿ’– Stickers and GIFs are only allowed to donators! You can become one at [our " - "Ko-Fi page]()." - ) - - if message.attachments: - if participant.user.flag & models.UserFlag.MODERATOR: - for attachment in message.attachments: - message_content += ( - f"[{constants.BRAILE_CHARACTER}]({attachment.url})" - ) - - else: - await message.channel.send( - f"โŒ **{participant.display_name}** You can't send attachments in calls." - ) - - if not message_content: - return - - notices.append("The original message contained attachments.") - - embeds = [] - - if message.reference: - if await call.is_message_from_call(message.reference.message_id): - replied_message = message.reference.cached_message - - if replied_message is None: - replied_message = await message.channel.fetch_message( - message.reference.message_id - ) - - original_message_id = ( - await self.bot.call_handler.get_original_message_id( - replied_message.id - ) - ) - - if ( - original_message_id is not None - and original_message_id != replied_message.id - ): - try: - replied_message = await self.bot.get_partial_messageable( - other_channel - ).fetch_message(original_message_id) - - except discord.errors.NotFound: - replied_message = None - - except discord.errors.Forbidden: - replied_message = None - - await self.bot.get_partial_messageable(other_channel).send( - "โŒ **Payphone** is missing permissions to read messages from this " - "channel. Make sure it's got `Read Message History` permissions." - ) - - if replied_message is not None: - replied_message_content = self.bot.clean_message(replied_message) - replied_participant = await call.get_participant( - replied_message.author.id - ) - - if replied_participant is None: - notices.append("In reply to a message from outside this call.") - - else: - embeds.append( - discord.Embed( - color=constants.PRIMARY_COLOR, - description=replied_message_content, - author=discord.EmbedAuthor( - name=( - replied_participant.display_name - if replied_participant.channel_id - == message.channel.id - else replied_participant.name - ), - icon_url=( - replied_participant.display_avatar_url - if replied_participant.channel_id - == message.channel.id - else replied_participant.avatar_url - ), - ), - ) - ) - - else: - notices.append("In reply to a message from outside this call.") - - else: - notices.append("In reply to a message from outside this call.") - - if was_mask_toggled: - if participant.is_masked: - embeds.append( - discord.Embed( - color=constants.MASK_COLOR, - description=f"๐ŸŽญ **{participant.display_name}** is wearing a mask! Their real identity is hidden.", - footer=discord.EmbedFooter( - text="You can set your mask using p.mask on." - ), - ) - ) - - async with aiohttp.ClientSession() as session: - for _ in range(3): - try: - webhook = discord.Webhook.from_url( - await self.bot.call_handler.get_channel_webhook_url( - other_channel - ), - session=session, - bot_token=constants.TOKEN, - ) - - notices_text = ( - ("\n-# โ“˜ " + "\n-# โ“˜ ".join(notices)) if notices else "" - ) - - propagated_msg = await webhook.send( - content=formatting.trim( - message_content, 2000 - len(notices_text) - ) - + notices_text, - username=participant.display_name, - avatar_url=participant.display_avatar_url, - embeds=embeds, - allowed_mentions=discord.AllowedMentions.none(), - wait=True, - ) - - await call.set_message( - message.id, - message.channel.id, - propagated_msg.id, - message.author.id, - ) - - break - - except discord.errors.NotFound: - await self.bot.call_handler.refresh_channel_webhook_url( - other_channel - ) - - except discord.errors.Forbidden: - await self.bot.get_partial_messageable(other_channel).send( - "โŒ **Payphone** is missing `Manage Webhooks` permissions. You won't see any messages while that permission is not set." - ) - break - - except discord.errors.DiscordException as exc: - sentry_sdk.capture_exception(exc) - - @commands.Cog.listener() - async def on_raw_message_delete( - self, payload: discord.RawMessageDeleteEvent - ) -> None: - """ - Handle deleted messages in calls. - """ - - call = await self.bot.call_handler.get_call_by_channel_id(payload.channel_id) - - if call is None: - return - - propagation = await call.get_message_propagation(payload.message_id) - - if propagation is None: - return - - propagation_channel, propagation_id = propagation - - async with aiohttp.ClientSession() as session: - webhook = discord.Webhook.from_url( - await self.bot.call_handler.get_channel_webhook_url( - propagation_channel - ), - session=session, - bot_token=constants.TOKEN, - ) - - try: - await webhook.delete_message(propagation_id) - - except discord.errors.NotFound: - pass - - @commands.Cog.listener() - async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent) -> None: - """ - Handle edited messages in calls. - """ - - call = await self.bot.call_handler.get_call_by_channel_id(payload.channel_id) - - if call is None: - return - - blocked_section = censorship.censor(payload.data["content"]) - - if blocked_section: - try: - await self.bot.get_partial_messageable( - payload.channel_id - ).get_partial_message(payload.message_id).reply( - ( - "โŒ Your message wasn't sent because it contained a blocked word or " - f"pattern: `{blocked_section}`" - ), - delete_after=10, - ) - - except discord.errors.Forbidden: - await self.bot.get_partial_messageable(payload.channel_id).send( - ( - f"โŒ **{payload.data["member"]["nick"] or payload.data["member"]["user"]["global_name"] or payload.data["member"]["user"]["username"]}** " - "your message wasn't sent because it contained a blocked message or " - f"pattern: `{blocked_section}`\n-# Payphone doesn't have read message history " - "permission? Make sure to keep that one enabled." - ), - delete_after=10, - allowed_mentions=discord.AllowedMentions.none(), - ) - - return - - propagation = await call.get_message_propagation(payload.message_id) - - if propagation is None: - return - - propagation_channel, propagation_id = propagation - - async with aiohttp.ClientSession() as session: - webhook = discord.Webhook.from_url( - await self.bot.call_handler.get_channel_webhook_url( - propagation_channel - ), - session=session, - bot_token=constants.TOKEN, - ) - - await webhook.edit_message(propagation_id, content=payload.data["content"]) - - @commands.message_command( - name="Report Message" if not constants.IS_DEBUG else "(Dev) Report Message", - guild_ids=[constants.PAYPHONE_HUB_ID] if constants.IS_DEBUG else None, - ) - async def report_message( - self, ctx: discord.ApplicationContext, message: discord.Message - ) -> None: - """ - Report a message in a call. - """ - - if await self.bot.is_banned(ctx.author.id): - return await ctx.send_response( - "โŒ You're banned from using Payphone.", ephemeral=True - ) - - if message.author.bot: - message_id = await self.bot.call_handler.get_original_message_id(message.id) - author_id = await self.bot.call_handler.get_message_author_id(message_id) - found_message = bool(author_id) - - else: - message_id = message.id - author_id = message.author.id - found_message = bool(await self.bot.call_handler.get_message(message.id)) - - if not found_message: - return await ctx.send_response( - ( - "โŒ More than 15 minutes have passed since this message was sent.\n" - "-# You'll also see this error if the message wasn't sent during a call." - ), - ephemeral=True, - ) - - await ctx.send_response("โœ… The message has been reported.", ephemeral=True) - - replying_to = ( - await self.bot.get_partial_messageable( - message.reference.channel_id - ).fetch_message(message.reference.message_id) - if message.reference and not message.reference.cached_message - else ( - message.reference.cached_message - if message.reference and message.reference.cached_message - else None - ) - ) - - await models.Report.update_or_create( - message_id=message_id, - defaults={ - "channel_id": message.channel.id, - "user_id": author_id, - "user_name": message.author.display_name, - "user_avatar_url": message.author.display_avatar.url, - "message_content": message.content, - "reply_message_content": ( - replying_to.clean_content if replying_to else None - ), - "reply_user_id": (replying_to.author.id if replying_to else None), - "reply_user_name": ( - replying_to.author.display_name if replying_to else None - ), - "reply_user_avatar_url": ( - replying_to.author.display_avatar.url if replying_to else None - ), - "reported_by_id": ctx.author.id, - }, - ) - - @commands.command( - extras={ - "show_in_help": False, - "help_name": "Cat Mask", - "help_error_message": "No.", - } - ) - async def catmask(self, ctx: commands.Context) -> None: - """First easter egg. How'd you find it?""" - - await ctx.message.delete() - - call = await self.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - - if not call: - return - - participant, _ = await call.set_participant( - ctx.author.id, - ctx.author.display_name, - ctx.author.display_avatar.url, - ctx.channel.id, - ) - - await participant.set_mask(masks.get_random_mask(call.masks, masks.CAT_MASKS)) - - -def setup(bot: Payphone) -> None: - """Loads the Calls cog into the bot. - - Args: - bot (Payphone): The bot instance. - """ - - bot.add_cog(Calls(bot)) diff --git a/payphone/cogs/mods/__init__.py b/payphone/cogs/mods/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/cogs/mods/cog.py b/payphone/cogs/mods/cog.py deleted file mode 100644 index 3f1c2f8..0000000 --- a/payphone/cogs/mods/cog.py +++ /dev/null @@ -1,139 +0,0 @@ -import discord -from discord.ext import commands - -from payphone.db import models -from payphone.bot import Payphone -from payphone.cogs import base - - -class Mods(base.Cog): - """Administration in Payphone.""" - - def __init__(self, bot: Payphone) -> None: - """Initializes the Calls cog. - - Args: - bot (Payphone): The bot instance. - """ - - self.bot = bot - - @commands.group(invoke_without_command=True) - @commands.guild_only() - @commands.has_guild_permissions(manage_guild=True) - async def config( - self, ctx: commands.Context, setting: str = "", value: str | None = None - ) -> None: - """ - Set up Payphone for the current server. - - Settings: - - `prefix`: Set a custom prefix. - - `enable`: Enable a (or all) channel(s) to run commands in it. Non-enabled channels will pass to be disabled. - - `disable`: Disables a channel to run commands in it. Non-disabled channels will stay enabled by default. - - You can use `{prefix}help config ` to get help about a specific configuration. - """ - - await ctx.send_help("config") - - @config.command(extras={"help_name": "Prefix"}) - @commands.guild_only() - @commands.has_guild_permissions(manage_guild=True) - async def prefix(self, ctx: commands.Context, prefix: str = "p.") -> None: - """ - Set a custom bot prefix. - - Options: - - ``: The new custom prefix. - - Permissions: - - Manage Server - """ - - if len(prefix) > 10: - return await ctx.send( - "โŒ The prefix must not be longer than 10 characters." - ) - - server, _ = await models.Server.update_or_create( - id=ctx.guild.id, defaults={"name": ctx.guild.name} - ) - - await server.set_prefix(self.bot.redis, prefix) - - await ctx.send(f"โœ… The prefix was set to `{prefix}`!") - - @config.command(extras={"help_name": "Enable Channel"}) - @commands.guild_only() - @commands.has_guild_permissions(manage_channels=True) - async def enable( - self, ctx: commands.Context, channel: discord.TextChannel | str | None - ) -> None: - """ - Enable a channel to run commands in it. Non-enabled channels will pass to be disabled. - - Options: - - `[]`: The channel to enable or "all". If no channel is specified, the current channel will be enabled. - - Permissions: - - Manage Channels - """ - - server_id = ctx.guild.id - channel = channel or ctx.channel - - if isinstance(channel, str) and channel.lower() == "all": - await models.Channel.filter(server_id=server_id).update(is_enabled=None) - await ctx.bot.redis.delete(f"channels:{server_id}") - - await ctx.send( - "โœ… All channels were enabled! You can now run commands in any channel." - ) - - else: - await models.Channel.update_or_create( - id=channel.id, server_id=server_id, is_enabled=True - ) - await ctx.bot.redis.hset(f"channels:{server_id}", channel.id, "1") - - await ctx.send( - f"โœ… {channel.mention} was enabled! Other non-enabled channels were disabled." - ) - - @config.command(extras={"help_name": "Disable Channel"}) - @commands.guild_only() - @commands.has_guild_permissions(manage_channels=True) - async def disable( - self, ctx: commands.Context, channel: discord.TextChannel | None - ) -> None: - """ - Disables a channel to run commands in it. Non-disabled channels will stay enabled by default. - - Options: - - `[]`: The channel to disable. If no channel is specified, the current channel will be disabled. - - Permissions: - - Manage Channels - """ - - channel = channel or ctx.channel - - await models.Channel.update_or_create( - id=channel.id, server_id=ctx.guild.id, is_enabled=False - ) - await ctx.bot.redis.hset(f"channels:{ctx.guild.id}", channel.id, "0") - - await ctx.send( - f"โœ… {channel.mention} was disabled. Commands cannot be ran in this channel anymore." - ) - - -def setup(bot: Payphone) -> None: - """Loads the Mods cog into the bot. - - Args: - bot (Payphone): The bot instance. - """ - - bot.add_cog(Mods(bot)) diff --git a/payphone/core/__init__.py b/payphone/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/core/calls.py b/payphone/core/calls.py deleted file mode 100644 index 71e18c2..0000000 --- a/payphone/core/calls.py +++ /dev/null @@ -1,684 +0,0 @@ -from dataclasses import dataclass - -import orjson - -from payphone.db import models -from payphone.bot import Payphone -from payphone.core import masks - - -@dataclass -class Participant: - """A participant in Payphone.""" - - _bot: Payphone - - id: int - name: str - avatar_url: str - mask: str - is_masked: bool - channel_id: int - call_id: int - user: models.User | None = None - - @property - def display_name(self) -> str: - """Gets the display name of the participant. - - Returns: - str: The display name of the participant. - """ - - return self.name if not self.is_masked else masks.get_mask_name(self.mask) - - @property - def display_avatar_url(self) -> str: - """Gets the display avatar URL of the participant. - - Returns: - str: The display avatar URL of the participant. - """ - - return ( - self.avatar_url - if not self.is_masked - else masks.get_mask_image_url(self.mask) - ) - - async def set_mask(self, mask: str) -> None: - """Sets the mask of the participant. - - Args: - mask (str): The mask to set. - """ - - if mask == self.mask: - return - - pipe = self._bot.redis.pipeline() - pipe.srem(f"calls:{self.call_id}:masks", self.mask) - pipe.sadd(f"calls:{self.call_id}:masks", mask) - await pipe.execute() - - self.mask = mask - self.is_masked = True - - data = orjson.dumps( - { - "name": self.name, - "avatar_url": self.avatar_url, - "mask": mask, - "is_masked": True, - "channel_id": self.channel_id, - } - ) - - await self._bot.redis.hset(f"calls:{self.call_id}:participants", self.id, data) - - -@dataclass -class Message: - id: int - call_id: int - channel_id: int - propagation_id: int - propagation_channel_id: int - author_id: int - - -@dataclass -class Call: - """A call in Payphone.""" - - _bot: Payphone - - id: int - channels: tuple[int, int] - masks: list[str] - - async def set_message( - self, id: int, channel_id: int, propagation_id: int, author_id: int - ) -> None: - """Sets a message as part of a call. - - Args: - id (int): The ID of the message to set. - channel_id (int): The ID of the channel to set the message in. - propagation_id (int): The ID of the propagation message. - author_id (int): The ID of the author of the message. - """ - - pipe = self._bot.redis.pipeline() - - pipe.hset("calls:messages", id, self.id) - pipe.hset("calls:messages:channels", id, channel_id) - pipe.hset("calls:messages:propagations", id, propagation_id) - pipe.hset("calls:messages:reversed", propagation_id, id) - pipe.hset("calls:messages:authors", id, author_id) - pipe.hset("calls:last-message", self.id, "") - pipe.hexpire("calls:messages", 15 * 60, id) - pipe.hexpire("calls:messages:channels", 15 * 60, id) - pipe.hexpire("calls:messages:propagations", 15 * 60, id) - pipe.hexpire("calls:messages:reversed", 15 * 60, propagation_id) - pipe.hexpire("calls:messages:authors", 15 * 60, id) - pipe.hexpire("calls:last-message", 15 * 60, self.id) - - await pipe.execute() - - async def get_message(self, id: int) -> Message | None: - """Gets a message as part of a call. - - Args: - id (int): The ID of the message to get. - - Returns: - Message | None: The message that was set. - """ - - call_id = await self._bot.redis.hget("calls:messages", id) - - if call_id is None: - return - - channel_id = await self._bot.redis.hget("calls:messages:channels", id) - propagation_id = await self._bot.redis.hget("calls:messages:propagations", id) - author_id = await self._bot.redis.hget("calls:messages:authors", id) - - return Message( - id=id, - call_id=int(call_id), - channel_id=int(channel_id), - propagation_id=int(propagation_id), - propagation_channel_id=( - self.channels[0] - if int(channel_id) != self.channels[0] - else self.channels[1] - ), - author_id=int(author_id), - ) - - async def is_message_from_call(self, id: int) -> bool: - """Checks if a message is from a call. - - Args: - id (int): The ID of the message to check. - - Returns: - bool: True if the message is from the call, False otherwise. - """ - - return bool(await self._bot.redis.hexists("calls:messages", id)) or bool( - await self._bot.redis.hexists("calls:messages:reversed", id) - ) - - async def get_message_propagation(self, id: int) -> tuple[int, int] | None: - """Gets a message as part of a call. - - Args: - id (int): The ID of the message to get. - - Returns: - tuple[int, int]: The channel and propagation ID of the message. - """ - - pipe = self._bot.redis.pipeline() - - pipe.hget("calls:messages:channels", id) - pipe.hget("calls:messages:propagations", id) - - channel_id, propagation_id = await pipe.execute() - - if channel_id is None or propagation_id is None: - return - - propagation_channel_id = ( - self.channels[0] - if int(channel_id) != self.channels[0] - else self.channels[1] - ) - - return int(propagation_channel_id), int(propagation_id) - - async def set_participant( - self, user_id: int, user_name: str, user_avatar_url: str, channel_id: int - ) -> tuple[Participant, bool]: - """Sets a participant in a call. - - Args: - user_id (int): The ID of the user to set. - user_name (str): The name of the user to set. - user_avatar_url (str): The avatar URL of the user to set. - channel_id (int): The ID of the channel the user is in. - - Returns: - (Participant, bool): The participant that was set and whether they changed their masked - status. - """ - - participant = await self.get_participant(user_id) - - user = await models.User.get_cache_or_db(self._bot.redis, user_id) - mask = ( - masks.get_random_mask(self.masks) - if participant is None - else participant.mask - ) - - data = orjson.dumps( - { - "name": user_name, - "avatar_url": user_avatar_url, - "mask": mask, - "is_masked": user.is_masked, - "channel_id": channel_id, - } - ) - - await self._bot.redis.hset(f"calls:{self.id}:participants", user_id, data) - await self._bot.redis.hexpire(f"calls:{self.id}:participants", 3600, user_id) - await self._bot.redis.sadd(f"calls:{self.id}:masks", mask) - await self._bot.redis.expire(f"calls:{self.id}:masks", 3600) - - return Participant( - _bot=self._bot, - id=user_id, - name=user_name, - avatar_url=user_avatar_url, - mask=mask, - is_masked=user.is_masked, - channel_id=channel_id, - user=user, - call_id=self.id, - ), (user.is_masked != participant.is_masked if participant else True) - - async def get_participant(self, user_id: int) -> Participant | None: - """Gets a participant in a call. - - Args: - user_id (int): The ID of the user to get. - - Returns: - Participant | None: The participant that was set. - """ - - data = await self._bot.redis.hget(f"calls:{self.id}:participants", user_id) - - if data is None: - return - - data = orjson.loads(data) - - return Participant( - _bot=self._bot, - id=user_id, - name=data["name"], - avatar_url=data["avatar_url"], - mask=data["mask"], - is_masked=data["is_masked"], - channel_id=data["channel_id"], - call_id=self.id, - ) - - async def get_participants(self) -> list["Participant"]: - """Gets all participants in a call. - - Returns: - list[Participant]: The participants in the call. - """ - - participants = [] - - data = await self._bot.redis.hgetall(f"calls:{self.id}:participants") - - for user_id, data in data.items(): - data = orjson.loads(data) - - participants.append( - Participant( - _bot=self._bot, - id=int(user_id), - name=data["name"], - avatar_url=data["avatar_url"], - mask=data["mask"], - is_masked=data["is_masked"], - channel_id=data["channel_id"], - call_id=self.id, - ) - ) - - return participants - - -class CallHandler: - """Handles calls in Payphone.""" - - def __init__(self, bot: Payphone) -> None: - """Initializes the CallHandler. - - Args: - bot (Payphone): The bot instance. - """ - - self.bot = bot - - async def set_searching(self, channel_id: int, status: bool) -> None: - """Sets the channel as searching for a call. - - Args: - channel_id (int): The ID of the channel to set as searching. - status (bool): The status to set the channel to. - """ - - if status: - await self.bot.redis.sadd("calls:channels:searching", channel_id) - - else: - await self.bot.redis.srem("calls:channels:searching", channel_id) - - async def connect(self) -> list[tuple[int, int]]: - """Connects two channels together. - - Returns: - list[tuple[int, int]]: A list of channel pairs. - """ - - pairs = [] - - while True: - channels = await self.bot.redis.spop("calls:channels:searching", 2) - - if len(channels) < 2: - if len(channels) == 1: - await self.set_searching(channels[0], True) - - break - - last_calls = await self.bot.redis.hmget("last-calls", *channels) - - break_outer = False - - for channel in channels: - if channel in last_calls: - next_channel = await self.bot.redis.spop("calls:channels:searching") - - if next_channel is not None: - other_channel_id = channels[abs(channels.index(channel) - 1)] - await self.set_searching(other_channel_id, True) - - channels[channels.index(channel)] = next_channel - - else: - for channel in channels: - await self.set_searching(channel, True) - - break_outer = True - break - - if break_outer: - break - - pairs.append(tuple(channels)) - - mapping = {} - reverse_mapping = {} - - for pair in pairs: - last_call_id = await self.bot.redis.incr("calls:calls:count") - - mapping[pair[0]] = last_call_id - mapping[pair[1]] = last_call_id - - reverse_mapping[last_call_id] = f"{pair[0]}:{pair[1]}" - - pipe = self.bot.redis.pipeline(transaction=True) - - if mapping: - pipe.hset("calls:channels:active", mapping=mapping) - pipe.hset("calls:calls:active", mapping=reverse_mapping) - - for pair in pairs: - pipe.hmset("last-calls", {pair[0]: pair[1], pair[1]: pair[0]}) - pipe.hset("calls:last-message", mapping[pair[0]], "") - pipe.hexpire("calls:last-message", 60, mapping[pair[0]]) - - await pipe.execute() - - return pairs - - async def expire(self) -> list[tuple[int, int]]: - """ - Ends all calls that have been inactive for more than 15 seconds. - """ - - active_calls = await self.bot.redis.hgetall("calls:calls:active") - - pairs = [] - - pipe = self.bot.redis.pipeline(transaction=True) - - for call_id, channels in active_calls.items(): - channels = [int(c) for c in channels.split(":")] - - last_message = await self.bot.redis.hget("calls:last-message", call_id) - - if last_message is not None: - continue - - pipe.hdel("calls:last-message", call_id) - pipe.hdel("calls:channels:active", *channels) - pipe.hdel("calls:calls:active", call_id) - pipe.srem("calls:channels:searching", *channels) - - pairs.append(tuple(channels)) - - await pipe.execute() - - return pairs - - async def is_searching(self, channel_id: int) -> bool: - """Checks if a channel is searching for a call. - - Args: - channel_id (int): The ID of the channel to check. - - Returns: - bool: True if the channel is searching for a call, False otherwise. - """ - - return bool( - await self.bot.redis.sismember("calls:channels:searching", channel_id) - ) - - async def is_on_call(self, channel_id: int) -> bool: - """Checks if a channel is on a call. - - Args: - channel_id (int): The ID of the channel to check. - - Returns: - bool: True if the channel is on a call, False otherwise. - """ - - return await self.bot.redis.hexists("calls:channels:active", channel_id) - - async def get_call_by_channel_id(self, channel_id: int) -> Call | None: - """Gets the call that a channel is in. - - Args: - channel_id (int): The ID of the channel to get the call for. - - Returns: - Call | None: The pair of channels that are connected. - """ - - call_id = await self.bot.redis.hget("calls:channels:active", channel_id) - - if call_id is None: - return - - return await self.get_call(int(call_id)) - - async def get_call(self, call_id: int) -> Call | None: - """Gets a call by its ID. - - Args: - call_id (int): The ID of the call to get. - - Returns: - Call | None: The call that was found. - """ - - used_masks = await self.bot.redis.smembers(f"calls:{call_id}:masks") - - channels = [ - int(c) - for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split( - ":" - ) - ] - - return Call(_bot=self.bot, id=call_id, channels=channels, masks=used_masks) - - async def hangup(self, channel_id: int) -> tuple[int, int] | tuple[int]: - """Ends a call in a channel. - - Args: - channel_id (int): The ID of the channel to end the call in. - - Returns: - tuple[int, int] | tuple[int]: The pair of channels that were connected. - """ - - await self.set_searching(channel_id, False) - - call_id = await self.bot.redis.hget("calls:channels:active", channel_id) - - if call_id is None: - # Channel is searching. - return (channel_id,) - - channels = [ - int(c) - for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split( - ":" - ) - ] - - pipe = self.bot.redis.pipeline(transaction=True) - - pipe.srem("calls:channels:searching", *channels) - pipe.hdel("calls:channels:active", *channels) - pipe.hdel("calls:calls:active", call_id) - - await pipe.execute() - - return tuple(channels) - - async def skip(self, channel_id: int) -> tuple[int, int]: - """Skips a channel in the call queue. - - Args: - channel_id (int): The ID of the channel to skip. - """ - - await self.set_searching(channel_id, True) - - call_id = await self.bot.redis.hget("calls:channels:active", channel_id) - - if call_id is None: - return - - channels = [ - int(c) - for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split( - ":" - ) - ] - - for channel in channels: - if channel != channel_id: - await self.set_searching(channel, False) - - await self.bot.redis.hdel("calls:channels:active", *channels) - - return tuple(channels) - - async def get_channel_webhook_url(self, channel_id: int) -> str: - """Gets the webhook for a channel. - - Args: - channel_id (int): The ID of the channel to get the webhook for. - - Returns: - str: The URL of the webhook. - """ - - cached_url = await self.bot.redis.hget("calls:channels:webhooks", channel_id) - - if cached_url: - return cached_url - - channel = await self.bot.get_or_fetch_channel(channel_id) - - if channel is None: - return - - for webhook in await channel.webhooks(): - if webhook.user == self.bot.user and webhook.token is not None: - await self.bot.redis.hset( - "calls:channels:webhooks", channel_id, webhook.url - ) - return webhook.url - - webhook = await channel.create_webhook(name="Payphone") - - await self.bot.redis.hset("calls:channels:webhooks", channel_id, webhook.url) - - return webhook.url - - async def refresh_channel_webhook_url(self, channel_id: int) -> str: - """Refreshes a channel's webhook. - - Args: - channel_id (int): The channel ID of the webhook to refresh. - - Returns: - str: The new webhook's URL. - """ - - await self.bot.redis.hdel("calls:channels:webhooks", channel_id) - - channel = await self.bot.get_or_fetch_channel(channel_id) - - if channel is None: - return - - for webhook in await channel.webhooks(): - if webhook.name == "Payphone": - await webhook.delete() - - return await self.get_channel_webhook_url(channel_id) - - async def get_message(self, message_id: int) -> Message | None: - """Gets a message sent in a call. - - Args: - message_id (int): The ID of the message to get. - - Returns: - Message | None: The message that was sent. - """ - - call_id = await self.bot.redis.hget("calls:messages", message_id) - - if call_id is None: - return - - call = await self.get_call(int(call_id)) - - return await call.get_message(message_id) - - async def get_message_author_id(self, id: int) -> int | None: - """Gets the author of a message as part of a call. - - Args: - id (int): The ID of the message to get. - - Returns: - int: The author ID of the message. - """ - - pipe = self.bot.redis.pipeline() - - pipe.hget("calls:messages", id) - pipe.hget("calls:messages:reversed", id) - - call_id, message_id = await pipe.execute() - - if call_id is None and message_id is None: - return - - message_id = int(message_id) if message_id else id - - author_id = await self.bot.redis.hget("calls:messages:authors", message_id) - - return int(author_id) if author_id else None - - async def get_original_message_id(self, id: int) -> int | None: - """ - Get the original message ID of a message in a call. - - Args: - id (int): The ID of the message to get. - - Returns: - int: The original message ID of the message. - """ - - message_id = await self.bot.redis.hget("calls:messages:reversed", id) - - if message_id: - return int(message_id) - - return None diff --git a/payphone/core/censorship.py b/payphone/core/censorship.py deleted file mode 100644 index e850ab1..0000000 --- a/payphone/core/censorship.py +++ /dev/null @@ -1,49 +0,0 @@ -import re - - -words = [] - -with open("censored.txt", encoding="UTF-8") as f: - for line in f: - words.append(line.strip()) - - -WORDS = re.compile(r"\w+") -PATTERN = re.compile( - f"^({"|".join(words + [word + "s" for word in words])})$", - re.IGNORECASE, -) -URL_PATTERN = re.compile(r"https?://[^\s]+", re.IGNORECASE) -INVITES_PATTERN = re.compile(r"(discord)?.gg/[^\s]+", re.IGNORECASE) - - -def censor(message: str, is_gif_allowed: bool = False) -> str: - """Returns the first censored word or pattern found. - - Args: - message (str): The message to moderate. - is_gif_allowed (bool, optional): If GIFs are allowed. Defaults to False. - - Returns: - str | None: The blocked pattern found as it was found. - """ - - for word in WORDS.finditer(message): - word = message[word.start() : word.end()] - if PATTERN.match(word): - return word - - for match in URL_PATTERN.finditer(message): - string = message[match.start() : match.end()] - - if is_gif_allowed and string.startswith("https://tenor.com/view/"): - continue - - return string - - match = next(INVITES_PATTERN.finditer(message), None) - - if match is not None: - return message[match.start() : match.end()] - - return None diff --git a/payphone/core/checks.py b/payphone/core/checks.py deleted file mode 100644 index 9a6e437..0000000 --- a/payphone/core/checks.py +++ /dev/null @@ -1,65 +0,0 @@ -from datetime import datetime, timezone, timedelta - -from discord.ext import commands - -from payphone.core import exceptions - - -# List of check IDs to skip. -skip_checks_ids = [1357199907512914051] - - -def is_moderator(): - """Checks whether the user is a moderator or not.""" - - async def predicate(ctx: commands.Context) -> bool: - if ctx.author.bot: - return False - - if await ctx.bot.is_moderator(ctx.author.id): - return True - - raise exceptions.NotAModeratorError() - - return commands.check(predicate) - - -def is_not_banned(): - """Checks that the user is not banned.""" - - async def predicate(ctx: commands.Context) -> bool: - if ctx.author.id in skip_checks_ids: - return True - - if ctx.author.created_at > datetime.now(timezone.utc) - timedelta( - days=exceptions.AccountTooYoungError.days - ): - raise exceptions.AccountTooYoungError() - - if await ctx.bot.is_banned(ctx.author.id): - raise exceptions.BannedError() - - return True - - return commands.check(predicate) - - -def is_on_call(): - """Checks that the user is on a call.""" - - async def predicate(ctx: commands.Context) -> bool: - call = await ctx.bot.call_handler.get_call_by_channel_id(ctx.channel.id) - - if call is None: - raise exceptions.NotOnCallError() - - return True - - return commands.check(predicate) - - -def is_in_server(server_id: int): - async def predicate(ctx): - return ctx.guild and ctx.guild.id == server_id - - return commands.check(predicate) diff --git a/payphone/core/constants.py b/payphone/core/constants.py deleted file mode 100644 index 68e5eb4..0000000 --- a/payphone/core/constants.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -import toml - -try: - import dotenv - - dotenv.load_dotenv() - -except ImportError: - print("dotenv is not installed. No .env file was loaded.") - -IS_DEBUG = os.getenv("IS_DEBUG", "false").lower() == "true" - -TOKEN = os.getenv("TOKEN") - -if TOKEN is None: - raise RuntimeError( - "No token provided. Make sure to set the TOKEN environment variable." - ) - -SHARD_ID = int(os.getenv("SHARD_ID", "0")) -SHARD_COUNT = int(os.getenv("SHARD_COUNT", "1")) - -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) - -INVISIBLE_CHARACTER = "\u200b" -BRAILE_CHARACTER = "\u2800" - -PRIMARY_COLOR = 0xE2D9D0 -SUPPORTER_COLOR = 0xAB9EFF -MODERATOR_COLOR = 0xFF8787 -MASK_COLOR = 0xFFC868 -APPROVE_COLOR = 0x0FFF95 - -DISCORD_SERVER_INVITE = "https://discord.gg/9daSsrfTmE" - -SENTRY_DSN = os.getenv( - "SENTRY_DSN", "https://cf0e77b5b2fd435bb90f9ad244aaa377@errors.nyeki.dev/69" -) - -VERSION = None - -PAYPHONE_HUB_ID = 1360766092900962414 - -with open("pyproject.toml", "r", encoding="UTF-8") as f: - data = toml.load(f) - VERSION = "v" + data["project"]["version"].split(".")[1] diff --git a/payphone/core/exceptions.py b/payphone/core/exceptions.py deleted file mode 100644 index ae9255c..0000000 --- a/payphone/core/exceptions.py +++ /dev/null @@ -1,23 +0,0 @@ -from discord.ext.commands import errors - - -class NotAModeratorError(errors.CheckFailure): - """Raised when the user is not a moderator.""" - - -class BannedError(errors.CheckFailure): - """Raised when the user is banned.""" - - -class ChannelDisabledError(errors.CheckFailure): - """Raised when the channel is disabled.""" - - -class NotOnCallError(errors.CheckFailure): - """Raised when the user is not on a call.""" - - -class AccountTooYoungError(errors.CheckFailure): - """Raised when the user's account is too young.""" - - days: int = 30 diff --git a/payphone/core/formatting.py b/payphone/core/formatting.py deleted file mode 100644 index e394960..0000000 --- a/payphone/core/formatting.py +++ /dev/null @@ -1,221 +0,0 @@ -from datetime import timedelta - -import re - - -def trim(text: str, length: int = 100) -> str: - """Trims a string if it's too long. - - Args: - text (str): The original text. - length (int, optional): The max length of the string before trimming. Defaults to 100. - - Returns: - str: The trimmed (or not) text. - """ - - if len(text) > length: - return text[: length - 3] + "..." - - return text - - -def text_join(texts: list[str], join_word: str = "and") -> str: - """Joins a list with commas and the last item with an "and". - - Args: - texts (list[str]): The list of texts to concatenate. - join_word (str): The word to use at the end of the joint. - - Returns: - str: The formatted concatenation. - """ - - if len(texts) > 1: - return f"{", ".join(texts[:-1])} {join_word} {texts[-1]}" - - return texts[0] - - -def monospace_grid(values: list[str], space: int = 14) -> str: - """Creates a monospace grid. - - Args: - values (list[str]): All the values that the list must contain - space (int, optional): The width of each column. Defaults to 14. - - Returns: - str: The formatted grid. - """ - - return "```" + "".join([v + " " * (space - len(v)) for v in values]) + "```" - - -def monospace_table( - rows: list[list[str | tuple[str, str]]], - headers: list | None = None, - column_separator: str = "|", - header_separator: str = "-", - intersection: str = "+", - padding: int = 1, -) -> str: - """Creates a monospace table. - - Creates a monospace table. Row columns can be either a string with the column's content, or a - tuple with two items, being the first one aligned to the left and the second one aligned to the - right. Non-string values will be converted to string using `str(value)`. - - Args: - rows (list[list[str | tuple[str, str]]]): The rows. - headers (list | None, optional): The headers. - column_separator (str, optional): The column separator. Defaults to "|". - header_separator (str, optional): The header separator. Defaults to "-". - intersection (str, optional): The intersection character where column and row separators - meet. - padding (int, optional): The padding that each column has at its sides. - - Returns: - str: The monospace table. - """ - - max_sizes = ( - [ - max( - *( - [ - ( - len(str(row[i])) - if not isinstance(row[i], tuple) - else (sum(len(str(item)) for item in row[i]) + 1) - ) - for row in rows - ] - + ([len(headers[i])] if headers is not None else []) - ) - ) - for i in range(len(rows[0])) - ] - if len(rows) >= 2 - else ( - [ - ( - len(str(col)) - if not isinstance(col, tuple) - else (sum(len(str(item)) for item in col) + 1) - ) - for col in rows[0] - ] - if len(rows) > 0 - else [] - ) - ) - - def format_row(row: list) -> str: - return column_separator.join( - [ - (" " * padding) - + ( - (str(col) + (" " * (max_sizes[i] - len(str(col))))) - if not isinstance(col, tuple) - else ( - str(col[0]) - + (" " * (max_sizes[i] - len(str(col[0])) - len(str(col[1])))) - + str(col[1]) - ) - ) - + (" " * padding) - for i, col in enumerate(row) - ] - ) - - formatted_rows = [] - - for row in rows: - formatted_rows.append(format_row(row)) - - table = "\n".join(formatted_rows) - - if headers is not None: - header = format_row(headers) - - separator = intersection.join( - [header_separator * (size + padding * 2) for size in max_sizes] - ) - - table = f"{header}\n{separator}\n{table}" - - return table - - -def time_to_text(time_diff: timedelta) -> str: - """Converts a timedelta to its string representation. - - Args: - time_diff (timedelta): The timedelta. - - Returns: - str: The string representation. - """ - - years = time_diff.days // 365 - days = time_diff.days % 365 - hours, remainder = divmod(time_diff.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - - parts = [] - if years > 0: - parts.append(f"{years} year{"s" if years != 1 else ""}") - if days > 0: - parts.append(f"{days} day{"s" if days != 1 else ""}") - if hours > 0: - parts.append(f"{hours} hour{"s" if hours != 1 else ""}") - if minutes > 0: - parts.append(f"{minutes} min{"s" if minutes != 1 else ""}") - if seconds > 0: - parts.append(f"{seconds} sec{"s" if seconds != 1 else ""}") - - return " ".join(parts) - - -def parse_timedelta(text: str) -> timedelta: - """Parses a timedelta string formatted as 30h 45m to a timedelta object. - - Args: - text (str): The formatted string. - - Raises: - ValueError: The string is invalid. - - Returns: - datetime.timedelta: The timedelta object. - """ - - pattern = re.compile( - r"(?:(\d+)y)?\s*" # years - r"(?:(\d+)mo)?\s*" # months - r"(?:(\d+)w)?\s*" # weeks - r"(?:(\d+)d)?\s*" # days - r"(?:(\d+)h)?\s*" # hours - r"(?:(\d+)m)?\s*" # minutes - r"(?:(\d+)s)?" # seconds - ) - matches = pattern.match(text.strip()) - - if matches.span() == (0, 0): - raise ValueError("Invalid time string format") - - years, months, weeks, days, hours, minutes, seconds = matches.groups() - - years = int(years) if years else 0 - months = int(months) if months else 0 - weeks = int(weeks) if weeks else 0 - days = int(days) if days else 0 - hours = int(hours) if hours else 0 - minutes = int(minutes) if minutes else 0 - seconds = int(seconds) if seconds else 0 - - days += weeks * 7 - days += months * 30 - days += years * 365 - - return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) diff --git a/payphone/core/help_command.py b/payphone/core/help_command.py deleted file mode 100644 index 70b7f48..0000000 --- a/payphone/core/help_command.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import List, Dict - -import discord -from discord.ext import commands - -from payphone.cogs import base -from payphone.core import formatting, constants, utils - - -class HelpView(discord.ui.View): - """The view sent with the help command.""" - - def __init__(self): - super().__init__( - discord.ui.Button( - label="Credits", - style=discord.ButtonStyle.gray, - url="https://payphone.nyeki.dev", - ), - discord.ui.Button( - label="Donate", - style=discord.ButtonStyle.gray, - url="https://ko-fi.com/payphonedev", - ), - discord.ui.Button( - label="Vote", - style=discord.ButtonStyle.gray, - url="https://top.gg/bot/1212940413795827763/vote", - ), - discord.ui.Button( - label="Server", - style=discord.ButtonStyle.gray, - url=constants.DISCORD_SERVER_INVITE, - ), - timeout=None, - ) - - -class HelpCommand(commands.HelpCommand): - async def send_bot_help( - self, mapping: Dict[base.Cog | None, List[commands.Command]] - ) -> None: - bot_commands: List[commands.Command] = [] - - for cog, cog_commands in mapping.items(): - if cog is not None and not cog.show_in_help: - continue - - for command in cog_commands: - if isinstance(command, commands.Command): - if command.extras.get("show_in_help", True): - bot_commands += [command] - - embed = discord.Embed( - author=self.context.bot.embed_author, - title="Payphone, call and connect with other people.", - description=( - "Here is the list of all available commands. You can see more details about a " - f"specific command with `{self.context.clean_prefix}help `." - ), - thumbnail=self.context.bot.user.display_avatar.url, - fields=[ - discord.EmbedField( - name="Commands", - value=formatting.monospace_grid( - [c.name for c in bot_commands], space=12 - ), - ) - ], - color=constants.PRIMARY_COLOR, - footer=discord.EmbedFooter( - text=f"{constants.VERSION} ยท Use {self.context.clean_prefix}help to get more information about a specific command." - ), - ) - - await self.context.send(embed=embed, view=HelpView()) - - async def send_command_help(self, command: commands.Command) -> None: - error_message = command.extras.get("help_error_message") - - if error_message: - return await self.context.send(f"โŒ {error_message}") - - command_name = ( - command.name - if isinstance(command, discord.SlashCommand) - else command.qualified_name - ) - command_options = "".join( - [f" <{option.name}>" for option in command.options] - if isinstance(command, discord.SlashCommand) - else [ - (f" [{name}]" if utils.is_optional(info.annotation) else f" <{name}>") - for name, info in command.params.items() - if name not in ["self", "ctx"] - ] - ) - command_description = ( - command.description if command.description else command.help - ).replace("{prefix}", self.context.clean_prefix) - - full_command_usage = ( - self.context.clean_prefix + command.qualified_name + command_options - ) - reply_argument = ( - command.extras.get("reply_argument", None) if command.extras else None - ) - - embed = discord.Embed( - author=discord.EmbedAuthor( - name="Payphone", icon_url=self.context.bot.user.display_avatar.url - ), - title=( - command.extras["help_name"] - if command.extras and "help_name" in command.extras - else command_name.title() - ) - + " Command", - description=command_description, - thumbnail=self.context.bot.user.display_avatar.url, - color=constants.PRIMARY_COLOR, - fields=[ - discord.EmbedField( - name="Usage", - value=f"{f"`โ”Œโ”€ [<{reply_argument}>]`\n" if reply_argument else ""}`{full_command_usage}`", - ) - ] - + ( - [ - discord.EmbedField( - name="Aliases", - value=", ".join( - [ - f"`{self.context.clean_prefix}{alias}`" - for alias in command.aliases - ] - ), - ) - ] - if len(command.aliases) > 0 - else [] - ), - footer=discord.EmbedFooter( - f"Use {self.context.clean_prefix}help to get more information about a specific command." - ), - ) - - await self.context.send(embed=embed) - - async def send_group_help(self, group): - return await self.send_command_help(group) - - async def send_error_message(self, error): - await self.context.send(f"โŒ {error}") diff --git a/payphone/core/logging.py b/payphone/core/logging.py deleted file mode 100644 index d08f8c8..0000000 --- a/payphone/core/logging.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging - -from rich import logging as rlogging - - -logger = logging.getLogger("payphone") - - -def _get_handler(process_name: str, module: str | None = None) -> None: - """Generates a handler for the given process name and module. - - Args: - process_name (str): The process name. - module (str | None, optional): The module inside the process. Defaults to None. - """ - - fmt = f"[blue][{process_name}{f": [green]{module}[/green]" if module is not None else ""}][/blue] %(message)s" - formatter = logging.Formatter(fmt) - - handler = rlogging.RichHandler( - level=logging.INFO, markup=True, rich_tracebacks=True - ) - handler.setFormatter(formatter) - - return handler - - -def setup(process_name: str): - """Sets up pycord logging. - - Args: - process_name (str): The name of the process. - """ - - logging.basicConfig(level=logging.DEBUG, handlers=[_get_handler(process_name)]) - - discord_logger = logging.getLogger("discord") - discord_logger.handlers.clear() - discord_logger.addHandler(_get_handler(process_name, "discord")) - discord_logger.propagate = False - - asyncio_logger = logging.getLogger("asyncio") - asyncio_logger.handlers.clear() - asyncio_logger.addHandler(_get_handler(process_name, "asyncio")) - asyncio_logger.propagate = False diff --git a/payphone/core/masks.py b/payphone/core/masks.py deleted file mode 100644 index 76f75fa..0000000 --- a/payphone/core/masks.py +++ /dev/null @@ -1,118 +0,0 @@ -import random - -from payphone.core import constants - - -MASKS = { - "ferret": 1278176917123760139 if constants.IS_DEBUG else 1278173356423909448, - "octopus": 1278176904410697880 if constants.IS_DEBUG else 1278173331421397072, - "cat": 1278176891290783824 if constants.IS_DEBUG else 1278173317034934282, - "monkey": 1278176881195094049 if constants.IS_DEBUG else 1278173297456185344, - "panda": 1278176868767629455 if constants.IS_DEBUG else 1278173272390762538, - "sheep": 1278176856910073946 if constants.IS_DEBUG else 1278173251914432563, - "horse": 1278176842116894760 if constants.IS_DEBUG else 1278173237582237708, - "bee": 1278176823846506577 if constants.IS_DEBUG else 1278173216652787826, - "wild boar": 1278176806498996235 if constants.IS_DEBUG else 1278173196658413649, - "duck": 1278176794591232102 if constants.IS_DEBUG else 1278173181483679918, - "chicken": 1278176781790216233 if constants.IS_DEBUG else 1278173167218589726, - "elephant": 1278176771413512213 if constants.IS_DEBUG else 1278173150063886346, - "cow": 1278176761565413483 if constants.IS_DEBUG else 1278173125024153733, - "deer": 1278176744691597312 if constants.IS_DEBUG else 1278173110105014406, - "turkey": 1278176730619576321 if constants.IS_DEBUG else 1278173091796877363, - "dog": 1278176719932751903 if constants.IS_DEBUG else 1278173070846201856, - "beaver": 1278176709530882091 if constants.IS_DEBUG else 1278173051485425705, - "camel": 1278176699921465394 if constants.IS_DEBUG else 1278173035018584086, - "koala": 1278176687493746709 if constants.IS_DEBUG else 1278173021345021952, - "hippo": 1278176675846295603 if constants.IS_DEBUG else 1278173007419932804, - "ostrich": 1278176661317095494 if constants.IS_DEBUG else 1278172994669248523, - "rhino": 1278176650340864000 if constants.IS_DEBUG else 1278172980848885832, - "squirrel": 1278176637627924570 if constants.IS_DEBUG else 1278172965590274088, - "chamaleon": 1278176625392877648 if constants.IS_DEBUG else 1278172953305022496, - "frog": 1278176614512857088 if constants.IS_DEBUG else 1278172937043710094, - "skunk": 1278176604509442079 if constants.IS_DEBUG else 1278172923495976981, - "walrus": 1278176554723311678 if constants.IS_DEBUG else 1278172912368619603, - "hedgehog": 1278176541750067252 if constants.IS_DEBUG else 1278172899911405678, - "pig": 1278176531415564332 if constants.IS_DEBUG else 1278172885239861338, - "hen": 1278176518643646485 if constants.IS_DEBUG else 1278172871302184994, - "turtle": 1278176507428343891 if constants.IS_DEBUG else 1278172858199314455, - "mouse": 1278175957047578696 if constants.IS_DEBUG else 1278172846442414081, - "rabbit": 1278175944007487489 if constants.IS_DEBUG else 1278172831921999945, - "goat": 1278175933915856938 if constants.IS_DEBUG else 1278172815421472808, - "lion": 1278175922964664382 if constants.IS_DEBUG else 1278172803534684235, - "wolf": 1278175902047535165 if constants.IS_DEBUG else 1278172791052570687, - "snake": 1278175869331968061 if constants.IS_DEBUG else 1278172776259260547, - "llama": 1278175850008674325 if constants.IS_DEBUG else 1278172764913799168, - "crab": 1278175835634925620 if constants.IS_DEBUG else 1278172752322232330, - "owl": 1278175822947029152 if constants.IS_DEBUG else 1278172738309193749, - "bat": 1278175813572886550 if constants.IS_DEBUG else 1278172726426865724, - "zebra": 1278175799962505216 if constants.IS_DEBUG else 1278172715114565753, - "fox": 1278175788818104474 if constants.IS_DEBUG else 1278172702955274292, - "gorilla": 1278175775891259456 if constants.IS_DEBUG else 1278172687797194824, - "sloth": 1278175765661356175 if constants.IS_DEBUG else 1278172676837343242, - "puffer fish": 1278175753497743360 if constants.IS_DEBUG else 1278172665093558294, - "bear": 1278175741569142867 if constants.IS_DEBUG else 1278172653076746301, - "giraffe": 1278175732601983006 if constants.IS_DEBUG else 1278172640296702073, - "penguin": 1278175721956573236 if constants.IS_DEBUG else 1278172627629899808, - "parrot": 1278175706798489642 if constants.IS_DEBUG else 1278172600702337074, -} - -CAT_MASKS = { - "dinosaur": 1278255435652010014 if constants.IS_DEBUG else 1278254725833297965, - "shleepy cat": 1278255475066146816 if constants.IS_DEBUG else 1278254779767984181, - "cozy cat": 1278255420124696663 if constants.IS_DEBUG else 1278254704324771893, - "coquette cat": 1278255408254816300 if constants.IS_DEBUG else 1278254684200636428, - "panic cat": 1278255451993145345 if constants.IS_DEBUG else 1278254760700674098, -} - - -def get_random_mask(not_int: list[str] = [], masks: dict[str, int] = MASKS) -> str: - """Returns a random mask. - - Args: - not_int (list[str], optional): Masks to exclude. Defaults to []. - masks (dict[str, int], optional): The masks. Defaults to MASKS. - - Returns: - str: The mask. - """ - - return random.choice([mask for mask in masks.keys() if mask not in not_int]) - - -def get_mask_name(mask: str) -> str: - """Returns the name of the mask. - - Args: - mask (str): The mask. - - Returns: - str: The name of the mask. - """ - - return mask.title() - - -def get_mask_image_url(mask: str) -> str: - """Returns the URL of the mask image. - - Args: - mask (str): The mask. - - Returns: - str: The URL of the mask image. - """ - - return f"https://cdn.discordapp.com/emojis/{MASKS[mask] if mask in MASKS else CAT_MASKS[mask]}.png" - - -def get_mask_emoji(mask: str) -> str: - """Returns the emoji of the mask. - - Args: - mask (str): The mask. - - Returns: - str: The emoji of the mask. - """ - - return f"<:{mask.replace(" ", "")}:{MASKS[mask] if mask in MASKS else CAT_MASKS[mask]}>" diff --git a/payphone/core/messages.py b/payphone/core/messages.py deleted file mode 100644 index 6430f70..0000000 --- a/payphone/core/messages.py +++ /dev/null @@ -1,96 +0,0 @@ -from payphone.core import constants - -""" -TIPS = [ - "You can report calls using `Apps > Report Message`.", - "Help us sharing Payphone to make it more active!", - # "You can check your stats with `{prefix}me`.", - "Emojis will only show if they're from a server Payphone's in.", - "You cannot send links.", - "[Vote for Payphone on top.gg]() to help us reach more users!", - "[Write a review]() to tell everyone your opinion about Payphone!", - "Calls will be automatically hung up after 15 minutes if nobody talks.", - "You can change Payphone's prefix with `{prefix}config prefix`.", - "Do you have any suggestions? Use `{prefix}suggest`!", - "You can use `{prefix}skip` to directly search for a new call.", - # "The links from `{prefix}history` will take you to that call.", - "Want to stay hidden? Use `{prefix}mask on`!", - "Did you know you can check your mask with `{prefix}mask`?", - "Do you like Payphone? Consider [making a donation]() to help us grow Payphone (and keep it running)!", - "Using `{prefix}friendreq` is the best way to ask the other party to add you.", - "Enable and disable channels using `{prefix}config enable` and `{prefix}config disable`!", - "There's a secret easter egg command. Try to find it! (Hint: cat)", - "Make sure to read Payphone's [Code of Conduct](). Let's keep Payphone safe and fun for everyone!", - f"Did you join [Payphone's new official server](<{constants.DISCORD_SERVER_INVITE}>) yet? We have a lot of fun stuff there!", - # "How high are you on the leaderboard? Use `{prefix}me`, `{prefix}leaderboard`, and `{prefix}leaderboard servers` to check it out!", -] -""" - -TIPS = [ - "Thank you anonymous person and Stasi, we got our monthly costs covered!" -] - -CALLING = [ - "Dialing now! Please wait...", - "Trying to connect you...", - "Making the call, hang tight!", - "Searching for someone to answer...", - "Your call is being placed...", - "Connecting your call now...", - "Calling! One moment please...", - "Attempting to reach your contact...", - "Establishing the connection...", - "Your call is on the way!", -] - -ANSWERED = [ - "Call connected! You're live!", - "The call was answered.", - "Someone picked up the call!", - "Connected! Start talking.", - "You're now connected!", - "The call was successfully answered.", - "Call picked up! You're connected.", - "You're now live on the call!", - "Call answered. Go ahead!", - "Connection successful! You're live.", -] - -HANGUP = [ - "Call ended. You hung up.", - "You've disconnected the call.", - "You ended the call.", - "Call terminated. You hung up.", - "You have hung up the call.", - "You ended the conversation.", - "You've finished the call.", - "Call ended by you.", - "You disconnected the call.", - "You have ended the call.", -] - -OTHER_PARTY_HANGUP = [ - "The other party has hung up.", - "Call ended by the other party.", - "The other person disconnected.", - "The call was terminated by the other party.", - "The other person has hung up.", - "Conversation ended by the other party.", - "The call has been finished by the other party.", - "The other party ended the call.", - "Disconnected by the other party.", - "The other person ended the call.", -] - -NO_ANSWER = [ - "No answer. Maybe try again later?", - "It looks like nobody picked up.", - "The call went unanswered.", - "No response. Try calling again later.", - "No one is available to answer.", - "The call wasn't answered.", - "Nobody answered the call.", - "The call went unanswered. Try again soon!", - "No answer at the moment.", - "The call wasn't picked up.", -] diff --git a/payphone/core/utils.py b/payphone/core/utils.py deleted file mode 100644 index 693893e..0000000 --- a/payphone/core/utils.py +++ /dev/null @@ -1,25 +0,0 @@ -import typing - -from discord.ext import commands - - -def is_optional(type_hint) -> bool: - """Checks if the type_hint is optional or not. - - Args: - type_hint (typing.Type): The type hint. - - Returns: - bool: Whether the type hint is optional or not. - """ - - origin = typing.get_origin(type_hint) - - if origin is typing.Union: - args = typing.get_args(type_hint) - return type(None) in args - - if isinstance(type_hint, commands.Greedy): - return True - - return False diff --git a/payphone/db/__init__.py b/payphone/db/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payphone/db/migrations/models/0_20241112230205_init.py b/payphone/db/migrations/models/0_20241112230205_init.py deleted file mode 100644 index 8d4517f..0000000 --- a/payphone/db/migrations/models/0_20241112230205_init.py +++ /dev/null @@ -1,55 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - CREATE TABLE IF NOT EXISTS "channel" ( - "id" BIGSERIAL NOT NULL PRIMARY KEY, - "server_id" BIGINT NOT NULL, - "is_enabled" BOOL NOT NULL DEFAULT True -); -CREATE INDEX IF NOT EXISTS "idx_channel_server__d5ded1" ON "channel" ("server_id"); -CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled"); -COMMENT ON TABLE "channel" IS 'A channel.'; -CREATE TABLE IF NOT EXISTS "user" ( - "id" BIGSERIAL NOT NULL PRIMARY KEY, - "is_masked" BOOL NOT NULL DEFAULT False, - "is_moderator" BOOL NOT NULL DEFAULT False, - "is_supporter_until" TIMESTAMPTZ -); -COMMENT ON TABLE "user" IS 'A user.'; -CREATE TABLE IF NOT EXISTS "report" ( - "id" SERIAL NOT NULL PRIMARY KEY, - "message_id" BIGINT NOT NULL UNIQUE, - "message_content" TEXT NOT NULL, - "channel_id" BIGINT NOT NULL, - "user_id" BIGINT NOT NULL, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "resolved" BOOL NOT NULL DEFAULT False, - "resolved_at" TIMESTAMPTZ, - "resolved_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_report_resolve_32f036" ON "report" ("resolved"); -COMMENT ON TABLE "report" IS 'A report of a message.'; -CREATE TABLE IF NOT EXISTS "ban" ( - "id" SERIAL NOT NULL PRIMARY KEY, - "user_id" BIGINT NOT NULL, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "expires_at" TIMESTAMPTZ, - "created_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - "expired_by_id" BIGINT REFERENCES "user" ("id") ON DELETE CASCADE, - "report_id" INT REFERENCES "report" ("id") ON DELETE CASCADE -); -CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at"); -COMMENT ON TABLE "ban" IS 'A ban.'; -CREATE TABLE IF NOT EXISTS "aerich" ( - "id" SERIAL NOT NULL PRIMARY KEY, - "version" VARCHAR(255) NOT NULL, - "app" VARCHAR(100) NOT NULL, - "content" JSONB NOT NULL -);""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - """ diff --git a/payphone/db/migrations/models/10_20250315222933_update.py b/payphone/db/migrations/models/10_20250315222933_update.py deleted file mode 100644 index 7887c8a..0000000 --- a/payphone/db/migrations/models/10_20250315222933_update.py +++ /dev/null @@ -1,25 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE BIGINT USING "report_id"::BIGINT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE BIGINT USING "report_id"::BIGINT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE BIGINT USING "report_id"::BIGINT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE BIGINT USING "report_id"::BIGINT; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE INT USING "report_id"::INT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE INT USING "report_id"::INT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE INT USING "report_id"::INT; - ALTER TABLE "ban" ALTER COLUMN "report_id" TYPE INT USING "report_id"::INT; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/11_20250315230128_update.py b/payphone/db/migrations/models/11_20250315230128_update.py deleted file mode 100644 index 5554f27..0000000 --- a/payphone/db/migrations/models/11_20250315230128_update.py +++ /dev/null @@ -1,15 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" ADD "reply_user_name" TEXT; - ALTER TABLE "report" ADD "reply_message_content" TEXT; - ALTER TABLE "report" ADD "reply_user_avatar_url" TEXT;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" DROP COLUMN "reply_user_name"; - ALTER TABLE "report" DROP COLUMN "reply_message_content"; - ALTER TABLE "report" DROP COLUMN "reply_user_avatar_url";""" diff --git a/payphone/db/migrations/models/12_20250315230558_update.py b/payphone/db/migrations/models/12_20250315230558_update.py deleted file mode 100644 index a712056..0000000 --- a/payphone/db/migrations/models/12_20250315230558_update.py +++ /dev/null @@ -1,11 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" ADD "reply_user_id" BIGINT;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" DROP COLUMN "reply_user_id";""" diff --git a/payphone/db/migrations/models/13_20250318130207_update.py b/payphone/db/migrations/models/13_20250318130207_update.py deleted file mode 100644 index 5275dc6..0000000 --- a/payphone/db/migrations/models/13_20250318130207_update.py +++ /dev/null @@ -1,11 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "ban" ADD "reason" TEXT;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "ban" DROP COLUMN "reason";""" diff --git a/payphone/db/migrations/models/14_20250319193241_update.py b/payphone/db/migrations/models/14_20250319193241_update.py deleted file mode 100644 index 066b691..0000000 --- a/payphone/db/migrations/models/14_20250319193241_update.py +++ /dev/null @@ -1,13 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" ADD "reported_by_id" BIGINT; - ALTER TABLE "report" ADD CONSTRAINT "fk_report_user_3a7d2f49" FOREIGN KEY ("reported_by_id") REFERENCES "user" ("id") ON DELETE CASCADE;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "report" DROP CONSTRAINT IF EXISTS "fk_report_user_3a7d2f49"; - ALTER TABLE "report" DROP COLUMN "reported_by_id";""" diff --git a/payphone/db/migrations/models/15_20250330150313_update.py b/payphone/db/migrations/models/15_20250330150313_update.py deleted file mode 100644 index ab19f5f..0000000 --- a/payphone/db/migrations/models/15_20250330150313_update.py +++ /dev/null @@ -1,19 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - CREATE TABLE IF NOT EXISTS "warn" ( - "id" SERIAL NOT NULL PRIMARY KEY, - "user_id" BIGINT NOT NULL, - "reason" TEXT, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "created_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - "report_id" BIGINT REFERENCES "report" ("id") ON DELETE CASCADE -); -COMMENT ON TABLE "warn" IS 'A warn.';""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP TABLE IF EXISTS "warn";""" diff --git a/payphone/db/migrations/models/1_20241112230215_update.py b/payphone/db/migrations/models/1_20241112230215_update.py deleted file mode 100644 index 6878fde..0000000 --- a/payphone/db/migrations/models/1_20241112230215_update.py +++ /dev/null @@ -1,17 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/2_20241112230633_update.py b/payphone/db/migrations/models/2_20241112230633_update.py deleted file mode 100644 index 6878fde..0000000 --- a/payphone/db/migrations/models/2_20241112230633_update.py +++ /dev/null @@ -1,17 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/3_20241112232746_update.py b/payphone/db/migrations/models/3_20241112232746_update.py deleted file mode 100644 index 6878fde..0000000 --- a/payphone/db/migrations/models/3_20241112232746_update.py +++ /dev/null @@ -1,17 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/4_20241112232813_update.py b/payphone/db/migrations/models/4_20241112232813_update.py deleted file mode 100644 index 6616544..0000000 --- a/payphone/db/migrations/models/4_20241112232813_update.py +++ /dev/null @@ -1,19 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" DROP COLUMN "resolved_by_id"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" ADD "resolved_by_id" BIGINT; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/5_20241112232936_update.py b/payphone/db/migrations/models/5_20241112232936_update.py deleted file mode 100644 index e1ee5c8..0000000 --- a/payphone/db/migrations/models/5_20241112232936_update.py +++ /dev/null @@ -1,21 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" ADD "resolved_by_id" BIGINT; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");; - ALTER TABLE "report" ADD CONSTRAINT "fk_report_user_e65f1760" FOREIGN KEY ("resolved_by_id") REFERENCES "user" ("id") ON DELETE CASCADE;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - ALTER TABLE "report" DROP CONSTRAINT "fk_report_user_e65f1760"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" DROP COLUMN "resolved_by_id"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/6_20241114014251_update.py b/payphone/db/migrations/models/6_20241114014251_update.py deleted file mode 100644 index e1b9a07..0000000 --- a/payphone/db/migrations/models/6_20241114014251_update.py +++ /dev/null @@ -1,21 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" ADD "user_avatar_url" TEXT NOT NULL; - ALTER TABLE "report" ADD "user_name" TEXT NOT NULL; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "report" DROP COLUMN "user_avatar_url"; - ALTER TABLE "report" DROP COLUMN "user_name"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/7_20241114024101_update.py b/payphone/db/migrations/models/7_20241114024101_update.py deleted file mode 100644 index 9607b60..0000000 --- a/payphone/db/migrations/models/7_20241114024101_update.py +++ /dev/null @@ -1,21 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "user" ADD "warns_count" INT NOT NULL DEFAULT 0; - ALTER TABLE "user" ADD "bans_count" INT NOT NULL DEFAULT 0; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "user" DROP COLUMN "warns_count"; - ALTER TABLE "user" DROP COLUMN "bans_count"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/8_20241115225636_update.py b/payphone/db/migrations/models/8_20241115225636_update.py deleted file mode 100644 index c22000b..0000000 --- a/payphone/db/migrations/models/8_20241115225636_update.py +++ /dev/null @@ -1,24 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - CREATE TABLE IF NOT EXISTS "server" ( - "id" BIGSERIAL NOT NULL PRIMARY KEY, - "prefix" TEXT NOT NULL, - "language" TEXT NOT NULL -); -COMMENT ON TABLE "server" IS 'A server.';; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - DROP TABLE IF EXISTS "server"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/migrations/models/9_20241115230145_update.py b/payphone/db/migrations/models/9_20241115230145_update.py deleted file mode 100644 index 6c484c7..0000000 --- a/payphone/db/migrations/models/9_20241115230145_update.py +++ /dev/null @@ -1,19 +0,0 @@ -from tortoise import BaseDBAsyncClient - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "server" ADD "name" TEXT NOT NULL; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX "idx_channel_server__d940e4"; - DROP INDEX "idx_ban_user_id_37a9cf"; - ALTER TABLE "server" DROP COLUMN "name"; - CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");; - CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;""" diff --git a/payphone/db/models.py b/payphone/db/models.py deleted file mode 100644 index d631327..0000000 --- a/payphone/db/models.py +++ /dev/null @@ -1,235 +0,0 @@ -from datetime import datetime, timezone - -import enum - -from tortoise import models, fields - -import redis.asyncio as redis - -import orjson - - -class Report(models.Model): - """ - A report of a message. - """ - - id = fields.BigIntField(pk=True) - - message_id = fields.BigIntField(unique=True) - message_content = fields.TextField() - channel_id = fields.BigIntField() - user_id = fields.BigIntField() - user_name = fields.TextField() - user_avatar_url = fields.TextField() - reply_message_content = fields.TextField(null=True, default=None) - reply_user_id = fields.BigIntField(null=True) - reply_user_name = fields.TextField(null=True, default=None) - reply_user_avatar_url = fields.TextField(null=True, default=None) - created_at = fields.DatetimeField(auto_now_add=True) - resolved = fields.BooleanField(default=False, index=True) - reported_by = fields.ForeignKeyField( - "models.User", related_name="reports", null=True - ) - resolved_at = fields.DatetimeField(null=True) - resolved_by = fields.ForeignKeyField( - "models.User", related_name="resolved_reports", null=True - ) - - -class UserFlag(enum.IntFlag): - """ - Flags for a user. - """ - - NONE = 0 - MODERATOR = 1 - SUPPORTER = 2 - - -class User(models.Model): - """ - A user. - """ - - id = fields.BigIntField(pk=True) - - warns_count: int = fields.IntField(default=0) - bans_count: int = fields.IntField(default=0) - - is_masked = fields.BooleanField(default=False) - is_moderator = fields.BooleanField(default=False) - is_supporter_until = fields.DatetimeField(null=True) - - @property - def flag(self) -> UserFlag: - """ - The flags of the user. - """ - - flags = UserFlag.NONE - - if self.is_moderator: - flags |= UserFlag.MODERATOR - - if ( - self.is_supporter_until is not None - and self.is_supporter_until > datetime.now(timezone.utc) - ): - flags |= UserFlag.SUPPORTER - - return flags - - @classmethod - async def get_cache_or_db(cls, redis_client: redis.Redis, user_id: int) -> "User": - """ - Returns the user from the cache or the database. - - Args: - redis_client (redis.Redis): The Redis client. - user_id (int): The user ID. - - Returns: - (User): The user. - """ - - cache = await redis_client.get(f"user:{user_id}") - - instance = User(id=user_id) - - if cache is not None: - cache = orjson.loads(cache) - - instance.warns_count = cache["warns_count"] - instance.bans_count = cache["bans_count"] - instance.is_masked = cache["is_masked"] - instance.is_moderator = cache["is_moderator"] - instance.is_supporter_until = ( - datetime.fromtimestamp(cache["is_supporter_until"], timezone.utc) - if cache["is_supporter_until"] is not None - else None - ) - - else: - instance, _ = await User.update_or_create(id=user_id) - - await instance.set_cache(redis_client) - - return instance - - async def set_cache(self, redis_client: redis.Redis) -> None: - """ - Sets the user in the cache. - - Args: - redis_client (redis.Redis): The Redis client. - """ - - await redis_client.set( - f"user:{self.id}", - orjson.dumps( - { - "warns_count": self.warns_count, - "bans_count": self.bans_count, - "is_masked": self.is_masked, - "is_moderator": self.is_moderator, - "is_supporter_until": ( - self.is_supporter_until.timestamp() - if self.is_supporter_until - else None - ), - } - ), - ex=60 * 15, - ) - - -class Warn(models.Model): - """A warn.""" - - user_id = fields.BigIntField() - report = fields.ForeignKeyField("models.Report", null=True) - reason = fields.TextField(null=True, default=None) - created_at = fields.DatetimeField(auto_now_add=True) - created_by = fields.ForeignKeyField("models.User", related_name="created_warns") - - -class Ban(models.Model): - """ - A ban. - """ - - user_id = fields.BigIntField() - report = fields.ForeignKeyField("models.Report", null=True) - reason = fields.TextField(null=True, default=None) - created_at = fields.DatetimeField(auto_now_add=True) - expires_at = fields.DatetimeField(null=True) - created_by = fields.ForeignKeyField("models.User", related_name="created_bans") - expired_by = fields.ForeignKeyField( - "models.User", related_name="expired_bans", null=True - ) - - @property - def is_expired(self) -> bool: - """ - Whether the ban is expired. - """ - - return self.expires_at is not None and self.expires_at < datetime.now( - timezone.utc - ) - - class Meta: - """ - Meta information. - """ - - indexes = [ - models.Index(fields=["user_id", "expires_at"]), - ] - - -class Channel(models.Model): - """ - A channel. - """ - - id = fields.BigIntField(pk=True) - server_id = fields.BigIntField(index=True) - - is_enabled = fields.BooleanField(null=True) - - class Meta: - """ - Meta information. - """ - - indexes = [ - models.Index(fields=["server_id", "is_enabled"]), - ] - - -class Server(models.Model): - """ - A server. - """ - - id = fields.BigIntField(pk=True) - name = fields.TextField() - prefix = fields.TextField(default="p.") - language = fields.TextField(default="en") - - async def set_prefix(self, redis_client: redis.Redis, prefix: str) -> None: - """ - Sets the prefix of the server. - - Args: - redis_client (redis.Redis): The Redis client. - prefix (str): The prefix. - """ - - self.prefix = prefix - - await self.save() - - await redis_client.set(f"prefix:{self.id}", prefix, ex=60 * 60 * 24) diff --git a/payphone/db/setup.py b/payphone/db/setup.py deleted file mode 100644 index 143f0d3..0000000 --- a/payphone/db/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -import tortoise - -from payphone.core import constants - - -CONFIG = { - "connections": { - "default": { - "engine": "tortoise.backends.asyncpg", - "credentials": { - "host": "localhost" if constants.IS_DEBUG else "postgresql", - "port": "5432", - "user": "payphone", - "password": "payphone", - "database": "payphone", - }, - "minsize": 5, - "maxsize": 100, - }, - }, - "apps": { - "models": { - "models": ["payphone.db.models", "aerich.models"], - "default_connection": "default", - } - }, -} - - -async def init() -> None: - """Initialize the database and it's connections.""" - - await tortoise.Tortoise.init(CONFIG, use_tz=True) diff --git a/payphone/tasks/__init__.py b/payphone/tasks/__init__.py deleted file mode 100644 index a5e05ab..0000000 --- a/payphone/tasks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from payphone.tasks import connect, expire diff --git a/payphone/tasks/connect.py b/payphone/tasks/connect.py deleted file mode 100644 index ad095d7..0000000 --- a/payphone/tasks/connect.py +++ /dev/null @@ -1,32 +0,0 @@ -import asyncio - -import random - -from payphone.bot import Payphone -from payphone.core import constants, messages, logging - - -async def main(): - logging.setup("Connect") - - bot = Payphone() - - await bot.login(constants.TOKEN) - - while True: - pairs = await bot.call_handler.connect() - - message = random.choice(messages.ANSWERED) - - await asyncio.gather( - *[ - bot.get_partial_messageable(channel).send(f"โ˜Ž๏ธ {message}") - for pair in pairs - for channel in pair - ] - ) - - if pairs: - logging.logger.info("Connected %s calls.", len(pairs)) - - await asyncio.sleep(1) diff --git a/payphone/tasks/expire.py b/payphone/tasks/expire.py deleted file mode 100644 index 2cb97b5..0000000 --- a/payphone/tasks/expire.py +++ /dev/null @@ -1,30 +0,0 @@ -import asyncio - -from payphone.bot import Payphone -from payphone.core import constants, logging - - -async def main(): - logging.setup("Expire") - - bot = Payphone() - - await bot.login(constants.TOKEN) - - while True: - pairs = await bot.call_handler.expire() - - await asyncio.gather( - *[ - bot.get_partial_messageable(channel).send( - "โ˜Ž๏ธ The call was hung up because nobody responded." - ) - for pair in pairs - for channel in pair - ] - ) - - if pairs: - logging.logger.info("Expired %s calls.", len(pairs)) - - await asyncio.sleep(1) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2b961f7..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[project] -name = "payphone" -version = "0.113.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ "aerich>=0.8.2", "aiohttp>=3.10.10", "orjson>=3.10.11", "py-cord>=2.6.1", "redis[hiredis]>=5.2.0", "rich>=13.9.4", "sentry-sdk>=2.22.0", "toml>=0.10.2", "tortoise-orm[accel,asyncpg]>=0.21.7", "typer>=0.13.0",] - -[dependency-groups] -dev = [ "black>=25.1.0", "python-dotenv>=1.0.1", "pyyaml>=6.0.2",] - -[tool.aerich] -tortoise_orm = "payphone.db.setup.CONFIG" -location = "./payphone/db/migrations" -src_folder = "." diff --git a/scripts/deploy.py b/scripts/deploy.py deleted file mode 100644 index ce29fe3..0000000 --- a/scripts/deploy.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import sys -import time - -try: - import toml - -except ImportError: - print( - "You need to install `toml` to use this file. Run `pip install toml` and try again." - ) - sys.exit() - -try: - import yaml - -except ImportError: - print( - "You need to install `yaml` to use this file. Run `pip install pyyaml` and try again." - ) - sys.exit() - -os.system("git pull") - -data = toml.load(open("pyproject.toml", encoding="utf-8")) -version = data["project"]["version"].split(".") -latest_tag = "v" + version[1] - -os.system( - f"microk8s kubectl set image sts/bot bot=registry.nyeki.dev/payphone/payphone/shard:{latest_tag}" -) -os.system( - f""" -microk8s kubectl set image pod/tasks \ - connect=registry.nyeki.dev/payphone/payphone/tasks:{latest_tag} -""" -) - -print("Waiting 30 seconds to run migrations...") - -time.sleep(30) - -with open("./k8s/bot.yml", encoding="utf-8") as stream: - try: - docs = list(yaml.load_all(stream, yaml.Loader)) - data = docs[0] - - os.system(f"microk8s kubectl exec bot-{data["spec"]["replicas"] - 1} -- uv run aerich upgrade") - except yaml.YAMLError as exc: - print("Could not read the amount of deployed shards.") - print(exc) - - -print("Done!") - -# expire=registry.nyeki.dev/payphone/payphone/shard:v{latest_version} diff --git a/scripts/update.py b/scripts/update.py deleted file mode 100644 index 9a0e7c6..0000000 --- a/scripts/update.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import sys - -try: - import toml - -except ImportError: - print( - "You need to install `toml` to use this file. Run `pip install toml` and try again." - ) - sys.exit() - -os.system("git pull") - -data = toml.load(open("pyproject.toml")) -version = data["project"]["version"].split(".") -version[1] = str(int(version[1]) + 1) -data["project"]["version"] = ".".join(version) - -version_num = version[1] - -targets = ["tasks", "shard"] - -for t in targets: - tag_version = f"registry.nyeki.dev/payphone/payphone/{t}:v{version_num}" - - os.system(f"docker build --target {t} --tag {tag_version} .") - os.system(f"docker push {tag_version}") - - tag_latest = f"registry.nyeki.dev/payphone/payphone/{t}:latest" - - os.system(f"docker tag {tag_version} {tag_latest}") - os.system(f"docker push {tag_latest}") - -toml.dump(data, open("pyproject.toml", "w")) - -os.system("git add pyproject.toml uv.lock") -os.system(f"git commit -m 'chore: ๐Ÿ”– Bump to version v{version_num}.'") -os.system("git push") diff --git a/tasks.py b/tasks.py deleted file mode 100644 index dbd3974..0000000 --- a/tasks.py +++ /dev/null @@ -1,26 +0,0 @@ -import asyncio - -import typer - -from payphone import tasks - - -app = typer.Typer(name="Tasks") - - -@app.command() -def connect() -> None: - """Connect to other people.""" - - asyncio.run(tasks.connect.main()) - - -@app.command() -def expire(): - """Expire calls.""" - - asyncio.run(tasks.expire.main()) - - -if __name__ == "__main__": - app() diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 8bf17b2..0000000 --- a/uv.lock +++ /dev/null @@ -1,893 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.12" - -[[package]] -name = "aerich" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asyncclick" }, - { name = "dictdiffer" }, - { name = "pydantic" }, - { name = "tortoise-orm", marker = "python_full_version < '4.0'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/d1/701a44d8f7fdccf700400ce16a26c679b3f4e36de9fb86c3d9f55f64b07f/aerich-0.8.2.tar.gz", hash = "sha256:0ed2b15bb017785d17323329e5448b4b12fcfec08a20932626c12aa59a8378d6", size = 32464 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/6a/a3f4e906ac8d7fe0af4d41ded66edb6332d9d289011da2033eeab69393d2/aerich-0.8.2-py3-none-any.whl", hash = "sha256:c1000cebd8525a1310a0cf0ea7d9a03cc2529413f182e90f08a6c6f4f61edcda", size = 36611 }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, -] - -[[package]] -name = "aiohttp" -version = "3.11.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/3f/c4a667d184c69667b8f16e0704127efc5f1e60577df429382b4d95fd381e/aiohttp-3.11.13.tar.gz", hash = "sha256:8ce789231404ca8fff7f693cdce398abf6d90fd5dae2b1847477196c243b1fbb", size = 7674284 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/a9/6657664a55f78db8767e396cc9723782ed3311eb57704b0a5dacfa731916/aiohttp-3.11.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eabb269dc3852537d57589b36d7f7362e57d1ece308842ef44d9830d2dc3c90", size = 705054 }, - { url = "https://files.pythonhosted.org/packages/3b/06/f7df1fe062d16422f70af5065b76264f40b382605cf7477fa70553a9c9c1/aiohttp-3.11.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b77ee42addbb1c36d35aca55e8cc6d0958f8419e458bb70888d8c69a4ca833d", size = 464440 }, - { url = "https://files.pythonhosted.org/packages/22/3a/8773ea866735754004d9f79e501fe988bdd56cfac7fdecbc8de17fc093eb/aiohttp-3.11.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55789e93c5ed71832e7fac868167276beadf9877b85697020c46e9a75471f55f", size = 456394 }, - { url = "https://files.pythonhosted.org/packages/7f/61/8e2f2af2327e8e475a2b0890f15ef0bbfd117e321cce1e1ed210df81bbac/aiohttp-3.11.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c929f9a7249a11e4aa5c157091cfad7f49cc6b13f4eecf9b747104befd9f56f2", size = 1682752 }, - { url = "https://files.pythonhosted.org/packages/24/ed/84fce816bc8da39aa3f6c1196fe26e47065fea882b1a67a808282029c079/aiohttp-3.11.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d33851d85537bbf0f6291ddc97926a754c8f041af759e0aa0230fe939168852b", size = 1737375 }, - { url = "https://files.pythonhosted.org/packages/d9/de/35a5ba9e3d21ebfda1ebbe66f6cc5cbb4d3ff9bd6a03e5e8a788954f8f27/aiohttp-3.11.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9229d8613bd8401182868fe95688f7581673e1c18ff78855671a4b8284f47bcb", size = 1793660 }, - { url = "https://files.pythonhosted.org/packages/ff/fe/0f650a8c7c72c8a07edf8ab164786f936668acd71786dd5885fc4b1ca563/aiohttp-3.11.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669dd33f028e54fe4c96576f406ebb242ba534dd3a981ce009961bf49960f117", size = 1692233 }, - { url = "https://files.pythonhosted.org/packages/a8/20/185378b3483f968c6303aafe1e33b0da0d902db40731b2b2b2680a631131/aiohttp-3.11.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1b20a1ace54af7db1f95af85da530fe97407d9063b7aaf9ce6a32f44730778", size = 1619708 }, - { url = "https://files.pythonhosted.org/packages/a4/f9/d9c181750980b17e1e13e522d7e82a8d08d3d28a2249f99207ef5d8d738f/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5724cc77f4e648362ebbb49bdecb9e2b86d9b172c68a295263fa072e679ee69d", size = 1641802 }, - { url = "https://files.pythonhosted.org/packages/50/c7/1cb46b72b1788710343b6e59eaab9642bd2422f2d87ede18b1996e0aed8f/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:aa36c35e94ecdb478246dd60db12aba57cfcd0abcad43c927a8876f25734d496", size = 1684678 }, - { url = "https://files.pythonhosted.org/packages/71/87/89b979391de840c5d7c34e78e1148cc731b8aafa84b6a51d02f44b4c66e2/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b5b37c863ad5b0892cc7a4ceb1e435e5e6acd3f2f8d3e11fa56f08d3c67b820", size = 1646921 }, - { url = "https://files.pythonhosted.org/packages/a7/db/a463700ac85b72f8cf68093e988538faaf4e865e3150aa165cf80ee29d6e/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e06cf4852ce8c4442a59bae5a3ea01162b8fcb49ab438d8548b8dc79375dad8a", size = 1702493 }, - { url = "https://files.pythonhosted.org/packages/b8/32/1084e65da3adfb08c7e1b3e94f3e4ded8bd707dee265a412bc377b7cd000/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5194143927e494616e335d074e77a5dac7cd353a04755330c9adc984ac5a628e", size = 1735004 }, - { url = "https://files.pythonhosted.org/packages/a0/bb/a634cbdd97ce5d05c2054a9a35bfc32792d7e4f69d600ad7e820571d095b/aiohttp-3.11.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afcb6b275c2d2ba5d8418bf30a9654fa978b4f819c2e8db6311b3525c86fe637", size = 1694964 }, - { url = "https://files.pythonhosted.org/packages/fd/cf/7d29db4e5c28ec316e5d2ac9ac9df0e2e278e9ea910e5c4205b9b64c2c42/aiohttp-3.11.13-cp312-cp312-win32.whl", hash = "sha256:7104d5b3943c6351d1ad7027d90bdd0ea002903e9f610735ac99df3b81f102ee", size = 411746 }, - { url = "https://files.pythonhosted.org/packages/65/a9/13e69ad4fd62104ebd94617f9f2be58231b50bb1e6bac114f024303ac23b/aiohttp-3.11.13-cp312-cp312-win_amd64.whl", hash = "sha256:47dc018b1b220c48089b5b9382fbab94db35bef2fa192995be22cbad3c5730c8", size = 438078 }, - { url = "https://files.pythonhosted.org/packages/87/dc/7d58d33cec693f1ddf407d4ab975445f5cb507af95600f137b81683a18d8/aiohttp-3.11.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9862d077b9ffa015dbe3ce6c081bdf35135948cb89116e26667dd183550833d1", size = 698372 }, - { url = "https://files.pythonhosted.org/packages/84/e7/5d88514c9e24fbc8dd6117350a8ec4a9314f4adae6e89fe32e3e639b0c37/aiohttp-3.11.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbfef0666ae9e07abfa2c54c212ac18a1f63e13e0760a769f70b5717742f3ece", size = 461057 }, - { url = "https://files.pythonhosted.org/packages/96/1a/8143c48a929fa00c6324f85660cb0f47a55ed9385f0c1b72d4b8043acf8e/aiohttp-3.11.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a1f7d857c4fcf7cabb1178058182c789b30d85de379e04f64c15b7e88d66fb", size = 453340 }, - { url = "https://files.pythonhosted.org/packages/2f/1c/b8010e4d65c5860d62681088e5376f3c0a940c5e3ca8989cae36ce8c3ea8/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba40b7ae0f81c7029583a338853f6607b6d83a341a3dcde8bed1ea58a3af1df9", size = 1665561 }, - { url = "https://files.pythonhosted.org/packages/19/ed/a68c3ab2f92fdc17dfc2096117d1cfaa7f7bdded2a57bacbf767b104165b/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5b95787335c483cd5f29577f42bbe027a412c5431f2f80a749c80d040f7ca9f", size = 1718335 }, - { url = "https://files.pythonhosted.org/packages/27/4f/3a0b6160ce663b8ebdb65d1eedff60900cd7108838c914d25952fe2b909f/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d474c5c1f0b9405c1565fafdc4429fa7d986ccbec7ce55bc6a330f36409cad", size = 1775522 }, - { url = "https://files.pythonhosted.org/packages/0b/58/9da09291e19696c452e7224c1ce8c6d23a291fe8cd5c6b247b51bcda07db/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e83fb1991e9d8982b3b36aea1e7ad27ea0ce18c14d054c7a404d68b0319eebb", size = 1677566 }, - { url = "https://files.pythonhosted.org/packages/3d/18/6184f2bf8bbe397acbbbaa449937d61c20a6b85765f48e5eddc6d84957fe/aiohttp-3.11.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4586a68730bd2f2b04a83e83f79d271d8ed13763f64b75920f18a3a677b9a7f0", size = 1603590 }, - { url = "https://files.pythonhosted.org/packages/04/94/91e0d1ca0793012ccd927e835540aa38cca98bdce2389256ab813ebd64a3/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fe4eb0e7f50cdb99b26250d9328faef30b1175a5dbcfd6d0578d18456bac567", size = 1618688 }, - { url = "https://files.pythonhosted.org/packages/71/85/d13c3ea2e48a10b43668305d4903838834c3d4112e5229177fbcc23a56cd/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a8a6bc19818ac3e5596310ace5aa50d918e1ebdcc204dc96e2f4d505d51740c", size = 1658053 }, - { url = "https://files.pythonhosted.org/packages/12/6a/3242a35100de23c1e8d9e05e8605e10f34268dee91b00d9d1e278c58eb80/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f27eec42f6c3c1df09cfc1f6786308f8b525b8efaaf6d6bd76c1f52c6511f6a", size = 1616917 }, - { url = "https://files.pythonhosted.org/packages/f5/b3/3f99b6f0a9a79590a7ba5655dbde8408c685aa462247378c977603464d0a/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a4a13dfbb23977a51853b419141cd0a9b9573ab8d3a1455c6e63561387b52ff", size = 1685872 }, - { url = "https://files.pythonhosted.org/packages/8a/2e/99672181751f280a85e24fcb9a2c2469e8b1a0de1746b7b5c45d1eb9a999/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:02876bf2f69b062584965507b07bc06903c2dc93c57a554b64e012d636952654", size = 1715719 }, - { url = "https://files.pythonhosted.org/packages/7a/cd/68030356eb9a7d57b3e2823c8a852709d437abb0fbff41a61ebc351b7625/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b992778d95b60a21c4d8d4a5f15aaab2bd3c3e16466a72d7f9bfd86e8cea0d4b", size = 1673166 }, - { url = "https://files.pythonhosted.org/packages/03/61/425397a9a2839c609d09fdb53d940472f316a2dbeaa77a35b2628dae6284/aiohttp-3.11.13-cp313-cp313-win32.whl", hash = "sha256:507ab05d90586dacb4f26a001c3abf912eb719d05635cbfad930bdbeb469b36c", size = 410615 }, - { url = "https://files.pythonhosted.org/packages/9c/54/ebb815bc0fe057d8e7a11c086c479e972e827082f39aeebc6019dd4f0862/aiohttp-3.11.13-cp313-cp313-win_amd64.whl", hash = "sha256:5ceb81a4db2decdfa087381b5fc5847aa448244f973e5da232610304e199e7b2", size = 436452 }, -] - -[[package]] -name = "aiosignal" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, -] - -[[package]] -name = "aiosqlite" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792 }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, -] - -[[package]] -name = "asyncclick" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/e1e5fdf1c1bb7e6e614987c120a98d9324bf8edfaa5f5cd16a6235c9d91b/asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c", size = 232900 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/cc/a436f0fc2d04e57a0697e0f87a03b9eaed03ad043d2d5f887f8eebcec95f/asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6", size = 99093 }, - { url = "https://files.pythonhosted.org/packages/92/c4/ae9e9d25522c6dc96ff167903880a0fe94d7bd31ed999198ee5017d977ed/asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678", size = 99115 }, -] - -[[package]] -name = "asyncpg" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, - { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, - { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, - { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, - { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, - { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, - { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, - { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, - { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, - { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, - { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, - { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, - { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, - { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, -] - -[[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 } -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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "ciso8601" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/e9/d83711081c997540aee59ad2f49d81f01d33e8551d766b0ebde346f605af/ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434", size = 28214 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/fc/e852e664bb90bf1112e17778512d6cbc5fa5f49b7c22969e4ee131f13d06/ciso8601-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75870a1e496a17e9e8d2ac90125600e1bafe51679d2836b2f6cb66908fef7ad6", size = 15755 }, - { url = "https://files.pythonhosted.org/packages/22/da/c82e665c627836be4d7d0a8ed38518f9833124a6fd85735881cac72427b8/ciso8601-2.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c117c415c43aa3db68ee16a2446cb85c5e88459650421d773f6f6444ce5e5819", size = 24291 }, - { url = "https://files.pythonhosted.org/packages/3c/6a/822b178b6c473533e5023aab6447b05d1683f95c3210eda5680f9262c93c/ciso8601-2.3.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ce5f76297b6138dc5c085d4c5a0a631afded99f250233fe583dc365f67fe8a8d", size = 15713 }, - { url = "https://files.pythonhosted.org/packages/de/c3/63b89c7ec2a4f9bcbdeb3401485992d13eeb4da943accef58f0820c62552/ciso8601-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e3205e4cfd63100f454ea67100c7c6123af32da0022bdc6e81058e95476a8ad", size = 40253 }, - { url = "https://files.pythonhosted.org/packages/96/01/b12f356afaa6dfc339c4b964f01c7b78f7d844dfe087cbbc9c68a5f048c0/ciso8601-2.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5308a14ac72898f91332ccfded2f18a6c558ccd184ccff84c4fb36c7e4c2a0e6", size = 40087 }, - { url = "https://files.pythonhosted.org/packages/05/ae/de5f920ebf5cdb2ef28237bdb48ac9ea980d794e16f1fbedffc430064208/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e825cb5ecd232775a94ef3c456ab19752ee8e66eaeb20562ea45472eaa8614ec", size = 40908 }, - { url = "https://files.pythonhosted.org/packages/b2/3c/cd79c9305480cc9bf8dce286bd7ec2035a3d140b3f3ae0b1232087a65240/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a8f96f91bdeabee7ebca2c6e48185bea45e195f406ff748c87a3c9ecefb25cc", size = 40881 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "dictdiffer" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754 }, -] - -[[package]] -name = "frozenlist" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, - { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, - { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, - { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, - { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, - { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, - { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, - { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, - { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, - { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, - { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, - { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, - { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, - { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, - { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, - { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, - { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, - { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, - { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, - { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, - { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, - { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, - { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, - { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, - { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, - { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, - { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, - { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, - { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, - { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, - { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, -] - -[[package]] -name = "hiredis" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/e5/789cfa8993ced0061a6ef7ea758302ef5cf3439629bf0d39c85a6ede4641/hiredis-3.1.0.tar.gz", hash = "sha256:51d40ac3611091020d7dea6b05ed62cb152bff595fa4f931e7b6479d777acf7c", size = 87616 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/64/9f9c1648853cd34e52b2af04c26cebb7f086cb4cd8ce056fecedd7664be9/hiredis-3.1.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:f7c7f89e0bc4246115754e2eda078a111282f6d6ecc6fb458557b724fe6f2aac", size = 81304 }, - { url = "https://files.pythonhosted.org/packages/42/18/f70f8366c4abcbb830480d72968502192e422ebd60b7ca5f7739872e78cd/hiredis-3.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:3dbf9163296fa45fbddcfc4c5900f10e9ddadda37117dbfb641e327e536b53e0", size = 44551 }, - { url = "https://files.pythonhosted.org/packages/a8/a0/bf584a34a8b8e7194c3386700113cd7380a585c3e37b57b45bcf036a3305/hiredis-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af46a4be0e82df470f68f35316fa16cd1e134d1c5092fc1082e1aad64cce716d", size = 42471 }, - { url = "https://files.pythonhosted.org/packages/97/90/a709dad5fcfa6a3d0480709fd9e24d1e0ba70cbe4b853a1fe63cf7026207/hiredis-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc63d698c43aea500a84d8b083f830c03808b6cf3933ae4d35a27f0a3d881652", size = 168205 }, - { url = "https://files.pythonhosted.org/packages/14/29/33f943cc874d4cc6269d472b2c8ebb7385008fbde192aa5108d617d99504/hiredis-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:676b3d88674134bfaaf70dac181d1790b0f33b3187bfb9da9221e17e0e624f83", size = 179055 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/a1315d474ec36c89e68ac8a3a258431b6f266af7bc4a31265a9527e494df/hiredis-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aed10d9df1e2fb0011db2713ac64497462e9c2c0208b648c97569da772b959ca", size = 168484 }, - { url = "https://files.pythonhosted.org/packages/a1/4f/14aca28a24463b92274464000691610eb41a9afab1e16a7a739be496f274/hiredis-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b5bd8adfe8742e331a94cccd782bffea251fa70d9a709e71f4510f50794d700", size = 169050 }, - { url = "https://files.pythonhosted.org/packages/77/8d/e5aa6857a70c0e3ca423973ea27065fa3cf2567d25cc397b649a1d45043e/hiredis-3.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fc4e35b4afb0af6da55495dd0742ad32ab88150428a6ecdbb3085cbd60714e8", size = 164624 }, - { url = "https://files.pythonhosted.org/packages/62/5d/c167de0a8c841cb4ea0e25a8145bbdb7e33b5028eaf905cd0901381f0a83/hiredis-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89b83e76eb00ab0464e7b0752a3ffcb02626e742e9509bc141424a9c3202e8dc", size = 162461 }, - { url = "https://files.pythonhosted.org/packages/70/b8/fa7e9ae73237999a5c7eb9f59e6c2198ed65eca5cad948b85e2c82c12cc2/hiredis-3.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98ebf08c907836b70a8f40e030df8ab6f174dc7f6fa765251d813e89f14069d8", size = 161292 }, - { url = "https://files.pythonhosted.org/packages/04/af/6b6db2d29e2455e97cbf7e19bae0ef1a6e5b61c08d42377a3511ef9cc3bb/hiredis-3.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6c840b9cec086328f2ee2cfee0038b5d6bbb514bac7b5e579da6e346eaac056c", size = 173465 }, - { url = "https://files.pythonhosted.org/packages/dc/50/c49d53832d71e1fdb1fe7c91a99b2d47043655cb0d535437264dccc19e2e/hiredis-3.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c5c44e9fa6f4462d0330cb5f5d46fa652512fc86b41d4d1974d0356f263e9105", size = 165818 }, - { url = "https://files.pythonhosted.org/packages/5f/47/81992b4b27b59152abf7e279c4adba7a5a0e1d99ccbee674a82c6e65b9bf/hiredis-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e665b14ab50aa175cfa306fcb00fffd4e3ff02ceb36ca6a4df00b1246d6a73c4", size = 163871 }, - { url = "https://files.pythonhosted.org/packages/dd/f6/1ee81c373a2087557c6020bf201b4d27d6aec173c8414c3d06900e91d9bd/hiredis-3.1.0-cp312-cp312-win32.whl", hash = "sha256:bd33db977ac7af97e8d035ffadb163b00546be22e5f1297b2123f5f9bf0f8a21", size = 20206 }, - { url = "https://files.pythonhosted.org/packages/b7/67/46d5a8d44812c6293c8088d642e473b0dd9e12478ef539eb4a77df643450/hiredis-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:37aed4aa9348600145e2d019c7be27855e503ecc4906c6976ff2f3b52e3d5d97", size = 21997 }, - { url = "https://files.pythonhosted.org/packages/7b/b0/0b4f96f537d259b818e4ee7657616eb6fabc0612eb4150d2253f84e33f8f/hiredis-3.1.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b87cddd8107487863fed6994de51e5594a0be267b0b19e213694e99cdd614623", size = 81311 }, - { url = "https://files.pythonhosted.org/packages/79/85/bd6cb6f7645a3803111a4f07fb2b55a23b836725bc8ec74ac7623fe8bef4/hiredis-3.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d302deff8cb63a7feffc1844e4dafc8076e566bbf10c5aaaf0f4fe791b8a6bd0", size = 44550 }, - { url = "https://files.pythonhosted.org/packages/13/48/b53c5d10d3fd073a2046d096d9d415d61b3564f74b0499ec757ddaf7cddc/hiredis-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a018340c073cf88cb635b2bedff96619df2f666018c655e7911f46fa2c1c178", size = 42471 }, - { url = "https://files.pythonhosted.org/packages/dd/a0/f9da8e920c1871edf703dfa05dd6781a3c53e5574cd2e4b38a438053a533/hiredis-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1e8ba6414ac1ae536129e18c069f3eb497df5a74e136e3566471620a4fa5f95", size = 168219 }, - { url = "https://files.pythonhosted.org/packages/42/59/82a3625dc9fc77f43b38d272eef8c731e359e535a13b29b83ce220d47f5d/hiredis-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a86b9fef256c2beb162244791fdc025aa55f936d6358e86e2020e512fe2e4972", size = 179065 }, - { url = "https://files.pythonhosted.org/packages/b2/aa/66933e4101198f2e2ae379c091fb9a8131cd3dce7a1e6d8fa5ff51244239/hiredis-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7acdc68e29a446ad17aadaff19c981a36b3bd8c894c3520412c8a7ab1c3e0de7", size = 168508 }, - { url = "https://files.pythonhosted.org/packages/7a/da/e1475f4d51225cbc4b04e3be22ecb6da80a536b747aa4bb263af318d8555/hiredis-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e06baea05de57e1e7548064f505a6964e992674fe61b8f274afe2ac93b6371", size = 168989 }, - { url = "https://files.pythonhosted.org/packages/34/d7/52dd39b5abb81eb24726934c3b9138cc9a30231fb93da8a3e2f829e3598c/hiredis-3.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35b5fc061c8a0dbfdb440053280504d6aaa8d9726bd4d1d0e1cfcbbdf0d60b73", size = 164488 }, - { url = "https://files.pythonhosted.org/packages/13/dd/aecfd9f24015b7e892304d6feb888db25b01492f05730f8f45155887de1f/hiredis-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c89d2dcb271d24c44f02264233b75d5db8c58831190fa92456a90b87fa17b748", size = 162476 }, - { url = "https://files.pythonhosted.org/packages/ff/77/4a5357b29e4c9f573439246d27cabad470ea4367a60a86f01c2a31c7c63f/hiredis-3.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:aa36688c10a08f626fddcf68c2b1b91b0e90b070c26e550a4151a877f5c2d431", size = 161380 }, - { url = "https://files.pythonhosted.org/packages/aa/5e/b357511490626e9c39b3148612bda945f2cd0c8dcd149f36fd7b9512bff4/hiredis-3.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3982a9c16c1c4bc05a00b65d01ffb8d80ea1a7b6b533be2f1a769d3e989d2c0", size = 173505 }, - { url = "https://files.pythonhosted.org/packages/3e/82/50c015dcf04ea85a89c4603684da9d95c7850931b5320c02c6f3d7ddd78f/hiredis-3.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d1a6f889514ee2452300c9a06862fceedef22a2891f1c421a27b1ba52ef130b2", size = 165928 }, - { url = "https://files.pythonhosted.org/packages/82/10/bd8f39423b0cb9624ccaf08d5e9c04f72dd46e9e9fc82e95cec42a42428d/hiredis-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8a45ff7915392a55d9386bb235ea1d1eb9960615f301979f02143fc20036b699", size = 163902 }, - { url = "https://files.pythonhosted.org/packages/0b/77/00b420ad567875e5a4b37a16f1a89fef1a22c6a9e1a12195c77bb5b101dd/hiredis-3.1.0-cp313-cp313-win32.whl", hash = "sha256:539e5bb725b62b76a5319a4e68fc7085f01349abc2316ef3df608ea0883c51d2", size = 20211 }, - { url = "https://files.pythonhosted.org/packages/cc/04/eaa88433249ddfc282018d3da4198d0b0018e48768e137bfad304aacb1ec/hiredis-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9020fd7e58f489fda6a928c31355add0e665fd6b87b21954e675cf9943eafa32", size = 22004 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "iso8601" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "multidict" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, - { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, - { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, - { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, - { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, - { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, - { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, - { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, - { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, - { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, - { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, - { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, - { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, - { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, - { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, - { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, - { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, - { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, - { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, - { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, - { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, - { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, - { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, - { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, - { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, - { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, - { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, - { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, - { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, - { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "orjson" -version = "3.10.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 }, - { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 }, - { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 }, - { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 }, - { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 }, - { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 }, - { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 }, - { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 }, - { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 }, - { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 }, - { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 }, - { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 }, - { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 }, - { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, - { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, - { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, - { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, - { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, - { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, - { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, - { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, - { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, - { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, - { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, - { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, - { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "payphone" -version = "0.112.0" -source = { virtual = "." } -dependencies = [ - { name = "aerich" }, - { name = "aiohttp" }, - { name = "orjson" }, - { name = "py-cord" }, - { name = "redis", extra = ["hiredis"] }, - { name = "rich" }, - { name = "sentry-sdk" }, - { name = "toml" }, - { name = "tortoise-orm", extra = ["accel", "asyncpg"] }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, -] - -[package.metadata] -requires-dist = [ - { name = "aerich", specifier = ">=0.8.2" }, - { name = "aiohttp", specifier = ">=3.10.10" }, - { name = "orjson", specifier = ">=3.10.11" }, - { name = "py-cord", specifier = ">=2.6.1" }, - { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" }, - { name = "rich", specifier = ">=13.9.4" }, - { name = "sentry-sdk", specifier = ">=2.22.0" }, - { name = "toml", specifier = ">=0.10.2" }, - { name = "tortoise-orm", extras = ["accel", "asyncpg"], specifier = ">=0.21.7" }, - { name = "typer", specifier = ">=0.13.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "black", specifier = ">=25.1.0" }, - { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "pyyaml", specifier = ">=6.0.2" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, -] - -[[package]] -name = "propcache" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/2c/921f15dc365796ec23975b322b0078eae72995c7b4d49eba554c6a308d70/propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e", size = 79867 }, - { url = "https://files.pythonhosted.org/packages/11/a5/4a6cc1a559d1f2fb57ea22edc4245158cdffae92f7f92afcee2913f84417/propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af", size = 46109 }, - { url = "https://files.pythonhosted.org/packages/e1/6d/28bfd3af3a567ad7d667348e7f46a520bda958229c4d545ba138a044232f/propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5", size = 45635 }, - { url = "https://files.pythonhosted.org/packages/73/20/d75b42eaffe5075eac2f4e168f6393d21c664c91225288811d85451b2578/propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b", size = 242159 }, - { url = "https://files.pythonhosted.org/packages/a5/fb/4b537dd92f9fd4be68042ec51c9d23885ca5fafe51ec24c58d9401034e5f/propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667", size = 248163 }, - { url = "https://files.pythonhosted.org/packages/e7/af/8a9db04ac596d531ca0ef7dde518feaadfcdabef7b17d6a5ec59ee3effc2/propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7", size = 248794 }, - { url = "https://files.pythonhosted.org/packages/9d/c4/ecfc988879c0fd9db03228725b662d76cf484b6b46f7e92fee94e4b52490/propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7", size = 243912 }, - { url = "https://files.pythonhosted.org/packages/04/a2/298dd27184faa8b7d91cc43488b578db218b3cc85b54d912ed27b8c5597a/propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf", size = 229402 }, - { url = "https://files.pythonhosted.org/packages/be/0d/efe7fec316ca92dbf4bc4a9ba49ca889c43ca6d48ab1d6fa99fc94e5bb98/propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138", size = 226896 }, - { url = "https://files.pythonhosted.org/packages/60/63/72404380ae1d9c96d96e165aa02c66c2aae6072d067fc4713da5cde96762/propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86", size = 221447 }, - { url = "https://files.pythonhosted.org/packages/9d/18/b8392cab6e0964b67a30a8f4dadeaff64dc7022b5a34bb1d004ea99646f4/propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d", size = 222440 }, - { url = "https://files.pythonhosted.org/packages/6f/be/105d9ceda0f97eff8c06bac1673448b2db2a497444de3646464d3f5dc881/propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e", size = 234104 }, - { url = "https://files.pythonhosted.org/packages/cb/c9/f09a4ec394cfcce4053d8b2a04d622b5f22d21ba9bb70edd0cad061fa77b/propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64", size = 239086 }, - { url = "https://files.pythonhosted.org/packages/ea/aa/96f7f9ed6def82db67c972bdb7bd9f28b95d7d98f7e2abaf144c284bf609/propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c", size = 230991 }, - { url = "https://files.pythonhosted.org/packages/5a/11/bee5439de1307d06fad176f7143fec906e499c33d7aff863ea8428b8e98b/propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d", size = 40337 }, - { url = "https://files.pythonhosted.org/packages/e4/17/e5789a54a0455a61cb9efc4ca6071829d992220c2998a27c59aeba749f6f/propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57", size = 44404 }, - { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 }, - { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 }, - { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 }, - { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116 }, - { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905 }, - { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221 }, - { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627 }, - { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217 }, - { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921 }, - { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200 }, - { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400 }, - { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116 }, - { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911 }, - { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563 }, - { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763 }, - { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650 }, - { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140 }, - { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296 }, - { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724 }, - { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499 }, - { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911 }, - { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301 }, - { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947 }, - { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072 }, - { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190 }, - { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145 }, - { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163 }, - { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249 }, - { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741 }, - { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061 }, - { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252 }, - { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425 }, - { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 }, -] - -[[package]] -name = "py-cord" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/27/c7/c539d69d5cfa1ea5891d596212f73d619e40c7fc9f02ae906f4147993b94/py_cord-2.6.1.tar.gz", hash = "sha256:36064f225f2c7bbddfe542d5ed581f2a5744f618e039093cf7cd2659a58bc79b", size = 965087 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/90/2690ded84e34b15ca2619932a358c1b7dc6d28fe845dfbd01929fc33c9da/py_cord-2.6.1-py3-none-any.whl", hash = "sha256:e3d3b528c5e37b0e0825f5b884cbb9267860976c1e4878e28b55da8fd3af834b", size = 1089154 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "pypika-tortoise" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/7b/74dce3354d81988f1d69b689f20b28c3e92f2dcc0c46975bdafd3eb37b18/pypika_tortoise-0.5.0.tar.gz", hash = "sha256:ed0f56761868dc222c03e477578638590b972280b03c7c45cd93345b18b61f58", size = 39701 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/bc/830cfe07a84a9ff75d2ae96696b933744b7f20ef40ad69b002b8cf9265e3/pypika_tortoise-0.5.0-py3-none-any.whl", hash = "sha256:dbdc47eb52ce17407b05ce9f8560ce93b856d7b28beb01971d956b017846691f", size = 45915 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "pytz" -version = "2025.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, -] - -[[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 } -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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "redis" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, -] - -[package.optional-dependencies] -hiredis = [ - { name = "hiredis" }, -] - -[[package]] -name = "rich" -version = "13.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, -] - -[[package]] -name = "sentry-sdk" -version = "2.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/b6/662988ecd2345bf6c3a5c306a9a3590852742eff91d0a78a143398b816f3/sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944", size = 303539 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/7f/0e4459173e9671ba5f75a48dda2442bcc48a12c79e54e5789381c8c6a9bc/sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66", size = 325815 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, -] - -[[package]] -name = "tortoise-orm" -version = "0.24.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiosqlite" }, - { name = "iso8601", marker = "python_full_version < '4.0'" }, - { name = "pypika-tortoise", marker = "python_full_version < '4.0'" }, - { name = "pytz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/e8/ac7cc41c297f20b3642351e5ff16f53beddea37be285f33f30308838d704/tortoise_orm-0.24.2.tar.gz", hash = "sha256:83d2373efafc1c5d668c3c5bd4e1e7f4a219526e850909ffe88f184da3808fdb", size = 126496 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/66/6091aaad68c6e390e85a90cc2d9f9d12f43a577829c8b8dbfe51191a71db/tortoise_orm-0.24.2-py3-none-any.whl", hash = "sha256:d2381b26f48bd2f5c5d978ddf9bb40b2b81b2dbb78dab6a17882a156de8b5c62", size = 165737 }, -] - -[package.optional-dependencies] -accel = [ - { name = "ciso8601", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, - { name = "orjson" }, - { name = "uvloop", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, -] -asyncpg = [ - { name = "asyncpg" }, -] - -[[package]] -name = "typer" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, -] - -[[package]] -name = "yarl" -version = "1.18.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, - { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, - { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, - { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, - { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, - { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, - { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, - { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, - { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, - { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, - { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, - { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, - { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, - { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, - { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, - { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, - { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, - { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, - { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, - { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, - { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, - { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, - { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, - { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, - { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, - { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, - { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, - { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, - { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, - { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, -]