Delete Python code
This commit is contained in:
-166
@@ -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/
|
|
||||||
-162
@@ -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/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.12
|
|
||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"pylint.args": ["--extension-pkg-allow-list=orjson"],
|
|
||||||
"conventionalCommits.scopes": ["shard", "tasks", "kube", "scripts"]
|
|
||||||
}
|
|
||||||
-19
@@ -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" ]
|
|
||||||
@@ -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=<your-username> --docker-password=<your-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
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from payphone import bot
|
|
||||||
from payphone.core import logging
|
|
||||||
|
|
||||||
|
|
||||||
logging.setup("Payphone")
|
|
||||||
|
|
||||||
|
|
||||||
bot.run()
|
|
||||||
-469
@@ -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
|
|
||||||
@@ -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
|
|
||||||
-63
@@ -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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: bot-config
|
|
||||||
data:
|
|
||||||
IS_DEBUG: "false"
|
|
||||||
REDIS_HOST: "redis"
|
|
||||||
REDIS_PORT: "6379"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: bot-secrets
|
|
||||||
type: Opaque
|
|
||||||
data:
|
|
||||||
TOKEN: TVRJMU56ZzVOemMxT1RRM09URTNNekl4TWcuR2J0d0dsLk9IVGJmN2JScVZHa0tTeXNPTzRGUXAzQnV0MmxISUF6dGpuZnhz
|
|
||||||
@@ -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
|
|
||||||
-499
@@ -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)
|
|
||||||
@@ -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:
|
|
||||||
- `<user>`: 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:
|
|
||||||
- `<user>`: 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"<t:{int(ban.expires_at.timestamp())}> (<t:{int(ban.expires_at.timestamp())}:R>)"
|
|
||||||
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}> <t:{int(ban.created_at.timestamp())}:R>. {(f"It expires <t:{int(ban.expires_at.timestamp())}:R>" if not ban.is_expired else f"It expired <t:{int(ban.expires_at.timestamp())}:R>") 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}> <t:{int(warn.created_at.timestamp())}:R>.",
|
|
||||||
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:
|
|
||||||
- `<warn-id>`: 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:
|
|
||||||
- `<ban-id>`: 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))
|
|
||||||
@@ -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**: <t:{int(report.created_at.timestamp())}>\n"
|
|
||||||
),
|
|
||||||
fields=[
|
|
||||||
discord.EmbedField(
|
|
||||||
name="Message Content",
|
|
||||||
value=formatting.trim(report.message_content, 1024),
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
thumbnail=report.user_avatar_url,
|
|
||||||
)
|
|
||||||
@@ -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"<t:{int(expires_at.timestamp())}> <t:{int(expires_at.timestamp())}:R>"
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
|
|
||||||
class Cog(commands.Cog):
|
|
||||||
"""Base cog with Payphone-specific properties."""
|
|
||||||
|
|
||||||
show_in_help: bool = True
|
|
||||||
@@ -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](<https://ko-fi.com/payphonedev>)."
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
@@ -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 <setting>` 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:
|
|
||||||
- `<prefix>`: 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:
|
|
||||||
- `[<channel>]`: 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:
|
|
||||||
- `[<channel>]`: 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))
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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://[email protected]/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]
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 <command>`."
|
|
||||||
),
|
|
||||||
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 <command> 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 <command> 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}")
|
|
||||||
@@ -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
|
|
||||||
@@ -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]}>"
|
|
||||||
@@ -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](<https://top.gg/bot/1212940413795827763/vote>) to help us reach more users!",
|
|
||||||
"[Write a review](<https://top.gg/bot/1212940413795827763#reviews>) 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](<https://ko-fi.com/payphonedev>) 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](<https://payphone.nyeki.dev/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.",
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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 """
|
|
||||||
"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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";"""
|
|
||||||
@@ -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";"""
|
|
||||||
@@ -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";"""
|
|
||||||
@@ -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";"""
|
|
||||||
@@ -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";"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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");;"""
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from payphone.tasks import connect, expire
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 = "."
|
|
||||||
@@ -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}
|
|
||||||
@@ -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")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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 },
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user