Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0dc5a3af6 | ||
|
|
29d24e2054 | ||
|
|
ee01d2160d | ||
|
|
16cbda838e | ||
|
|
ce0cef5db7 | ||
|
|
afcfdad13d | ||
|
|
2823868460 | ||
|
|
78fcfe386d | ||
|
|
5fdea76f37 | ||
|
|
6248dc72f0 | ||
|
|
53e066f868 | ||
|
|
c9b81219e5 | ||
|
|
b0a6950005 | ||
|
|
658dbd9c8d | ||
|
|
275bfa440c | ||
|
|
943225ce1f | ||
|
|
e6b99759ce | ||
|
|
014acf55c6 | ||
|
|
769d36e9c2 | ||
|
|
12c6755897 | ||
|
|
21678239b5 | ||
|
|
d0b5f86740 | ||
|
|
70c531ddac | ||
|
|
08c4534907 | ||
|
|
6aee4f39b0 | ||
|
|
e7aa3247e5 | ||
|
|
5dd8fdcc58 | ||
|
|
a0c0c17747 | ||
|
|
2b3dc00941 | ||
|
|
1c79d4b701 | ||
|
|
384ca1f4e8 | ||
|
|
84ff48ae8c | ||
|
|
be5422345b | ||
|
|
b2297703c8 | ||
|
|
e5ecbbfa10 | ||
|
|
f9a0c8220c | ||
|
|
176280fdf4 | ||
|
|
0c3bbf9a03 | ||
|
|
08d71a5e82 | ||
|
|
86f2a6bf9c | ||
|
|
d1669003cf | ||
|
|
b442be08e2 | ||
|
|
eb057e0531 | ||
|
|
73010fec3d | ||
|
|
676fdcfd96 | ||
|
|
e5fe77c287 | ||
|
|
28d330b1b2 | ||
|
|
ec893c0cf7 | ||
|
|
16085b4bd8 | ||
|
|
ca1ce13887 | ||
|
|
e44ab51adf | ||
|
|
6d8217ed81 | ||
|
|
a75fd9f092 | ||
|
|
0f5f390d68 | ||
|
|
c1a2a6a449 | ||
|
|
82fba989a4 | ||
|
|
3f5ec4f258 | ||
|
|
39120a65fa | ||
|
|
fc9bbcc87c | ||
|
|
497d180669 | ||
|
|
bd09a55a74 | ||
|
|
14a6037101 | ||
|
|
b9dbc17b63 | ||
|
|
325b4cbc16 | ||
|
|
2a2ffd8894 | ||
|
|
134d4e8879 | ||
|
|
291c4d50b2 | ||
|
|
06faca7aa2 | ||
|
|
8e9a23b4d9 | ||
|
|
8f6d28f3b6 | ||
|
|
35f3504c00 | ||
|
|
6c72c27911 | ||
|
|
7d47a9eb35 | ||
|
|
b10484cacc | ||
|
|
7a5943a5ef | ||
|
|
8294fafeba | ||
|
|
c5d699292f | ||
|
|
679096a685 | ||
|
|
9b70714fe3 | ||
|
|
6df7fd272f | ||
|
|
d0aa3d72d5 | ||
|
|
b108d93c55 | ||
|
|
eaa38f3474 | ||
|
|
8868b18be3 | ||
|
|
55ae1c8937 | ||
|
|
5e3201e7c5 | ||
|
|
7d19e928d6 | ||
|
|
c88496a73a | ||
|
|
aa32abfa90 | ||
|
|
6c240cfece | ||
|
|
1f9f094445 | ||
|
|
e28f11d9bb | ||
|
|
408f0223a2 | ||
|
|
2b1a738379 | ||
|
|
cca77aa4a7 | ||
|
|
a92a02484c | ||
|
|
0318f002fc | ||
|
|
4563c49245 | ||
|
|
aba11cb2da | ||
|
|
ee49b0a984 | ||
|
|
3cc37cfc0c | ||
|
|
f785413696 | ||
|
|
cddda58aca | ||
|
|
0af585cf9e | ||
|
|
b6242f5a10 | ||
|
|
9799631b7c | ||
|
|
1678eedf6d | ||
|
|
a6255290b2 | ||
|
|
118dacff20 | ||
|
|
a994c86f98 | ||
|
|
23969006c2 | ||
|
|
a0f592e66f | ||
|
|
5ebbfe6458 | ||
|
|
6843865da3 | ||
|
|
f882ed7fed | ||
|
|
1149dd5bb4 | ||
|
|
54217f1d69 | ||
|
|
8ff7e3af02 | ||
|
|
040b720755 | ||
|
|
91ae88d040 | ||
|
|
31bb63a77a | ||
|
|
2f99b32aea | ||
|
|
d3e192e3c3 | ||
|
|
f98fc37191 | ||
|
|
fcb1a786be | ||
|
|
5fe3878471 | ||
|
|
d53a161716 | ||
|
|
c0b919d33c | ||
|
|
a852cd9844 | ||
|
|
36f15615b6 | ||
|
|
f909a300ad | ||
|
|
da1971060a | ||
|
|
e1c8957fc8 | ||
|
|
43ab107102 | ||
|
|
14f8df2aba | ||
|
|
a67b4f6a3d | ||
|
|
455dc27743 | ||
|
|
d39b06eb12 | ||
|
|
8eaa3f6195 | ||
|
|
f6866e1a11 | ||
|
|
700b770248 | ||
|
|
ac81e3b286 | ||
|
|
323cf7f748 | ||
|
|
281542a5c2 | ||
|
|
77182d9633 | ||
|
|
e472936c51 | ||
|
|
12948dcc15 | ||
|
|
b9410bedd7 | ||
|
|
7eb2a99b69 | ||
|
|
fb7eba11bb | ||
|
|
208e0ddbf5 | ||
|
|
deb76941ea | ||
|
|
25b96cdc2a | ||
|
|
f4f842e3c8 | ||
|
|
6dde25aa72 | ||
|
|
8e0c591b21 | ||
|
|
fe53f2e5c2 | ||
|
|
3c87fdfa69 | ||
|
|
da10b7f194 | ||
|
|
53d61cfbbb | ||
|
|
eb6dcb4604 | ||
|
|
8432cb78fa | ||
|
|
faf9d9d580 | ||
|
|
f4a282447a | ||
|
|
2b4c3d0b8b | ||
|
|
1a446ae3bf | ||
|
|
55431d580e | ||
|
|
09ed3b870e | ||
|
|
634c8eff8c | ||
|
|
8173a508ad | ||
|
|
c284ede5b4 | ||
|
|
7b3fa9905a | ||
|
|
a14ae96a47 | ||
|
|
e35b200d03 | ||
|
|
285f1d4516 | ||
|
|
2030812742 | ||
|
|
e407068a9c | ||
|
|
7270c90b62 | ||
|
|
7119730668 | ||
|
|
fb660c162b | ||
|
|
6ba65eef71 | ||
|
|
d7b8138606 | ||
|
|
66584a8239 | ||
|
|
80ed00f796 | ||
|
|
fd71345cd0 | ||
|
|
7ed2c10448 | ||
|
|
717de3eec5 | ||
|
|
09b875da97 | ||
|
|
0cdbe63c55 | ||
|
|
ed717ef94b | ||
|
|
5806493ca8 | ||
|
|
39c455f617 | ||
|
|
d04af236d2 | ||
|
|
e8a76d5e77 | ||
|
|
91d5bf73c5 | ||
|
|
458f89a3a3 | ||
|
|
4df7c8ce7d | ||
|
|
28f8c8e702 | ||
|
|
79f0ca08b2 | ||
|
|
cd7f4c1cc9 | ||
|
|
84e3b533f0 | ||
|
|
7bb3a3387a | ||
|
|
6555ff672f | ||
|
|
41eac08722 | ||
|
|
689bde0acc | ||
|
|
b96474d0fb | ||
|
|
0f0027da27 | ||
|
|
619223cf5e | ||
|
|
edeaa1335f | ||
|
|
1f002cb7ca | ||
|
|
0ffb62a7dc | ||
|
|
b785eaa6c6 | ||
|
|
5bd9cbd3ee | ||
|
|
ca9c25159e | ||
|
|
6ca2649840 | ||
|
|
d444a4ab89 | ||
|
|
7027d0508c | ||
|
|
293ff8f8de | ||
|
|
a004fe2308 | ||
|
|
f123282699 | ||
|
|
24d2344f37 | ||
|
|
00d61545eb |
+15
-162
@@ -1,166 +1,19 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
/target
|
||||||
__pycache__/
|
/.venv
|
||||||
*.py[cod]
|
/.ansible
|
||||||
*$py.class
|
/.vscode
|
||||||
|
/k8s
|
||||||
|
/playbooks
|
||||||
|
/backup
|
||||||
|
|
||||||
# C extensions
|
.gitignore
|
||||||
*.so
|
.gitlab-ci.yml
|
||||||
|
.python-version
|
||||||
|
|
||||||
# Distribution / packaging
|
ansible.cfg
|
||||||
.Python
|
compose.yml
|
||||||
build/
|
inventory.ini
|
||||||
develop-eggs/
|
pyproject.toml
|
||||||
dist/
|
uv.lock
|
||||||
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
|
.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/
|
|
||||||
+6
-158
@@ -1,162 +1,10 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
/target
|
||||||
__pycache__/
|
/.venv
|
||||||
*.py[cod]
|
/.ansible
|
||||||
*$py.class
|
/backup
|
||||||
|
|
||||||
# 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
|
.env
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
00-vault-pwd.txt
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
reset-ssh.sh
|
||||||
.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/
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: lint
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- rustup component add rustfmt clippy
|
||||||
|
- cargo fmt --check
|
||||||
|
- cargo clippy -- -D warnings
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- cargo build --release
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: rust:latest
|
||||||
|
script:
|
||||||
|
- cargo test --release
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- "target/"
|
||||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"pylint.args": ["--extension-pkg-allow-list=orjson"],
|
"editor.formatOnSave": true,
|
||||||
"conventionalCommits.scopes": ["shard", "tasks", "kube", "scripts"]
|
"ansible.python.interpreterPath": "/home/nyeki/Documents/Repositories/payphone/payphone/.venv/bin/python"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4169
File diff suppressed because it is too large
Load Diff
+61
@@ -0,0 +1,61 @@
|
|||||||
|
[package]
|
||||||
|
name = "payphone"
|
||||||
|
version = "4.4.57"
|
||||||
|
edition = "2024"
|
||||||
|
default-run = "payphone"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.99"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
axum = "0.8.4"
|
||||||
|
clap = { version = "4.5.42", features = ["derive", "env", "string"] }
|
||||||
|
color-thief = "0.2.2"
|
||||||
|
colored = "3.0.0"
|
||||||
|
csv = "1.3.1"
|
||||||
|
deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
humantime = "2.2.0"
|
||||||
|
image = "0.25.6"
|
||||||
|
include_dir = "0.7.4"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
linkify = "0.10.0"
|
||||||
|
log = { version = "0.4.27", features = ["serde"] }
|
||||||
|
notify = "8.2.0"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
open = "5.3.2"
|
||||||
|
rand = "0.9.2"
|
||||||
|
rand_chacha = { version = "0.9.0", features = ["os_rng"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
reqwest = { version = "0.12.23", features = [
|
||||||
|
"gzip",
|
||||||
|
"brotli",
|
||||||
|
"zstd",
|
||||||
|
"native-tls-vendored",
|
||||||
|
] }
|
||||||
|
rgb = "0.8.52"
|
||||||
|
scc = "2.3.4"
|
||||||
|
scylla = { version = "1.3.1", features = ["time-03"] }
|
||||||
|
serde_json = "1.0.143"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
time = { version = "0.3.41", features = [
|
||||||
|
"formatting",
|
||||||
|
"local-offset",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
topgg = { version = "1.4.2", features = ["twilight"] }
|
||||||
|
tower-http = { version = "0.6.6", features = ["fs"] }
|
||||||
|
twilight-cache-inmemory = { version = "0.16.0", features = [
|
||||||
|
"permission-calculator",
|
||||||
|
] }
|
||||||
|
twilight-gateway = "0.16.0"
|
||||||
|
twilight-http = { version = "0.16.0", features = ["simd-json"] }
|
||||||
|
twilight-model = "0.16.0"
|
||||||
|
twilight-util = { version = "0.16.0", features = [
|
||||||
|
"builder",
|
||||||
|
"permission-calculator",
|
||||||
|
"snowflake",
|
||||||
|
] }
|
||||||
|
twilight-validate = "0.16.0"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
|
unidecode = "0.3.0"
|
||||||
+19
-10
@@ -1,19 +1,28 @@
|
|||||||
FROM registry.nyeki.dev/docker/python/with-uv:3.12-alpine AS base
|
# --- Builder stage ---
|
||||||
|
FROM rust:1-bookworm AS builder
|
||||||
|
|
||||||
|
# Install musl target and musl-tools
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y musl-tools build-essential
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pyproject.toml uv.lock /app/
|
|
||||||
|
|
||||||
RUN python3 -m uv sync --no-dev
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY . .
|
||||||
|
|
||||||
COPY . /app/
|
# Build fully static binary
|
||||||
|
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# --- Final stage ---
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
FROM base AS shard
|
# Copy static binary
|
||||||
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/payphone /usr/local/bin/payphone
|
||||||
|
|
||||||
CMD [ "bot.py" ]
|
RUN apt-get update && apt-get install -y ca-certificates \
|
||||||
|
&& update-ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/payphone"]
|
||||||
FROM base AS tasks
|
CMD ["start"]
|
||||||
|
|
||||||
CMD [ "tasks.py" ]
|
|
||||||
|
|||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -1,264 +1,9 @@
|
|||||||
# Payphone
|
# Payphone
|
||||||
|
|
||||||
Payphone is deployed using Kubernetes + Docker. To set up a local kubernetes cluster, you can use
|
To read the documentation, run
|
||||||
`minikube`.
|
|
||||||
|
|
||||||
## Starting a Cluster
|
```shell
|
||||||
|
$ git clone --branch rewrite https://git.moan.dev/nyeki/payphone.git
|
||||||
This section will guide you step by step to create a multi-node cluster or a local development
|
$ cd payphone
|
||||||
cluster and start up Payphone. Make sure to have:
|
$ cargo run -- docs --open
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[ssh_connection]
|
||||||
|
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
timeout = 30
|
||||||
@@ -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
|
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
services:
|
||||||
|
scylla-node-1:
|
||||||
|
image: scylladb/scylla:latest
|
||||||
|
container_name: scylla-node-1
|
||||||
|
command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1
|
||||||
|
ports:
|
||||||
|
- "9042:9042" # CQL
|
||||||
|
volumes:
|
||||||
|
- scylla-data-1:/var/lib/scylla
|
||||||
|
networks:
|
||||||
|
scylla-net:
|
||||||
|
ipv4_address: 172.20.0.2
|
||||||
|
|
||||||
|
scylla-node-2:
|
||||||
|
image: scylladb/scylla:latest
|
||||||
|
container_name: scylla-node-2
|
||||||
|
command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1
|
||||||
|
volumes:
|
||||||
|
- scylla-data-2:/var/lib/scylla
|
||||||
|
networks:
|
||||||
|
scylla-net:
|
||||||
|
ipv4_address: 172.20.0.3
|
||||||
|
|
||||||
|
scylla-node-3:
|
||||||
|
image: scylladb/scylla:latest
|
||||||
|
container_name: scylla-node-3
|
||||||
|
command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1
|
||||||
|
volumes:
|
||||||
|
- scylla-data-3:/var/lib/scylla
|
||||||
|
networks:
|
||||||
|
scylla-net:
|
||||||
|
ipv4_address: 172.20.0.4
|
||||||
|
|
||||||
|
redis-node-1:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-1
|
||||||
|
command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7000:7000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7000", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.2
|
||||||
|
|
||||||
|
redis-node-2:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-2
|
||||||
|
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7001:7001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7001", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.3
|
||||||
|
|
||||||
|
redis-node-3:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-node-3
|
||||||
|
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "7002:7002"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-p", "7002", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
redis-net:
|
||||||
|
ipv4_address: 172.20.1.4
|
||||||
|
|
||||||
|
redis-init:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: redis-init
|
||||||
|
command: redis-cli --cluster create 172.20.1.2:7000 172.20.1.3:7001 172.20.1.4:7002 --cluster-replicas 0 --cluster-yes
|
||||||
|
restart: no
|
||||||
|
depends_on:
|
||||||
|
redis-node-1:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-2:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-node-3:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- redis-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
scylla-data-1:
|
||||||
|
scylla-data-2:
|
||||||
|
scylla-data-3:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
scylla-net:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/24
|
||||||
|
|
||||||
|
redis-net:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.1.0/24
|
||||||
@@ -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
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[init]
|
||||||
|
p1 ansible_host=p1.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||||
|
|
||||||
|
[master]
|
||||||
|
p2 ansible_host=p2.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||||
|
p3 ansible_host=p3.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
p4 ansible_host=p4.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack1
|
||||||
|
p5 ansible_host=p5.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack2
|
||||||
|
p6 ansible_host=p6.vps.nyeki.dev ansible_user=root rack=ge-dc1-rack3
|
||||||
|
|
||||||
|
[scylla]
|
||||||
|
p4
|
||||||
|
p5
|
||||||
|
p6
|
||||||
-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
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: redis-config
|
||||||
|
namespace: redis
|
||||||
|
data:
|
||||||
|
update-node.sh: |
|
||||||
|
#!/bin/sh
|
||||||
|
REDIS_NODES="/data/nodes.conf"
|
||||||
|
sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/${POD_IP}/" ${REDIS_NODES}
|
||||||
|
exec "$@"
|
||||||
|
redis.conf: |+
|
||||||
|
cluster-enabled yes
|
||||||
|
cluster-require-full-coverage no
|
||||||
|
cluster-node-timeout 15000
|
||||||
|
cluster-config-file /data/nodes.conf
|
||||||
|
cluster-migration-barrier 1
|
||||||
|
save ""
|
||||||
|
appendonly no
|
||||||
|
protected-mode no
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: redis-cluster
|
||||||
|
namespace: redis
|
||||||
|
spec:
|
||||||
|
serviceName: redis-cluster
|
||||||
|
replicas: 6
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: redis-cluster
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: redis-cluster
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
name: client
|
||||||
|
- containerPort: 16379
|
||||||
|
name: gossip
|
||||||
|
command: ["/conf/update-node.sh", "redis-server", "/conf/redis.conf"]
|
||||||
|
env:
|
||||||
|
- name: POD_IP
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: status.podIP
|
||||||
|
volumeMounts:
|
||||||
|
- name: conf
|
||||||
|
mountPath: /conf
|
||||||
|
readOnly: false
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
readOnly: false
|
||||||
|
volumes:
|
||||||
|
- name: conf
|
||||||
|
configMap:
|
||||||
|
name: redis-config
|
||||||
|
defaultMode: 0755
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
accessModes: [ "ReadWriteOnce" ]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis-cluster
|
||||||
|
namespace: redis
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 6379
|
||||||
|
targetPort: 6379
|
||||||
|
name: client
|
||||||
|
- port: 16379
|
||||||
|
targetPort: 16379
|
||||||
|
name: gossip
|
||||||
|
selector:
|
||||||
|
app: redis-cluster
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: scylla.scylladb.com/v1alpha1
|
||||||
|
kind: NodeConfig
|
||||||
|
metadata:
|
||||||
|
name: scylladb-nodepool-1
|
||||||
|
spec:
|
||||||
|
localDiskSetup:
|
||||||
|
loopDevices:
|
||||||
|
- name: persistent-volumes
|
||||||
|
imagePath: /var/lib/persistent-volumes.img
|
||||||
|
size: 60Gi
|
||||||
|
filesystems:
|
||||||
|
- device: /dev/loops/persistent-volumes
|
||||||
|
type: xfs
|
||||||
|
mounts:
|
||||||
|
- device: /dev/loops/persistent-volumes
|
||||||
|
mountPoint: /var/lib/persistent-volumes
|
||||||
|
unsupportedOptions:
|
||||||
|
- prjquota
|
||||||
|
placement:
|
||||||
|
nodeSelector:
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
aggregationRule:
|
||||||
|
clusterRoleSelectors:
|
||||||
|
- matchLabels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:aggregate-to-csi-external-provisioner
|
||||||
|
labels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "persistentvolumes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "delete"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "persistentvolumeclaims"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "update"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "storageclasses"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "events"
|
||||||
|
verbs:
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "update"
|
||||||
|
- "patch"
|
||||||
|
- apiGroups:
|
||||||
|
- "snapshot.storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "volumesnapshots"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- apiGroups:
|
||||||
|
- "snapshot.storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "volumesnapshotcontents"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "csinodes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "nodes"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- apiGroups:
|
||||||
|
- "storage.k8s.io"
|
||||||
|
resources:
|
||||||
|
- "csistoragecapacities"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- "list"
|
||||||
|
- "watch"
|
||||||
|
- "create"
|
||||||
|
- "update"
|
||||||
|
- "patch"
|
||||||
|
- "delete"
|
||||||
|
# The GET permissions below are needed for walking up the ownership chain
|
||||||
|
# for CSIStorageCapacity. They are sufficient for deployment via
|
||||||
|
# StatefulSet (only needs to get Pod) and Deployment (needs to get
|
||||||
|
# Pod and then ReplicaSet to find the Deployment).
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- "pods"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
|
- apiGroups:
|
||||||
|
- "apps"
|
||||||
|
resources:
|
||||||
|
- "replicasets"
|
||||||
|
verbs:
|
||||||
|
- "get"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:aggregate-to-csi-external-provisioner-openshift
|
||||||
|
labels:
|
||||||
|
rbac.operator.scylladb.com/aggregate-to-csi-external-provisioner: "true"
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- security.openshift.io
|
||||||
|
resourceNames:
|
||||||
|
- privileged
|
||||||
|
resources:
|
||||||
|
- securitycontextconstraints
|
||||||
|
verbs:
|
||||||
|
- use
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: local-csi-driver
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
kind: StorageClass
|
||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb-local-xfs
|
||||||
|
provisioner: local.csi.scylladb.com
|
||||||
|
volumeBindingMode: WaitForFirstConsumer
|
||||||
|
parameters:
|
||||||
|
csi.storage.k8s.io/fstype: xfs
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
kind: CSIDriver
|
||||||
|
metadata:
|
||||||
|
name: local.csi.scylladb.com
|
||||||
|
spec:
|
||||||
|
attachRequired: false
|
||||||
|
storageCapacity: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
namespace: local-csi-driver
|
||||||
|
name: local-csi-driver
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: local-csi-driver
|
||||||
|
namespace: local-csi-driver
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: scylladb:csi-external-provisioner
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
kind: DaemonSet
|
||||||
|
apiVersion: apps/v1
|
||||||
|
metadata:
|
||||||
|
namespace: local-csi-driver
|
||||||
|
name: local-csi-driver
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: local-csi-driver
|
||||||
|
spec:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/os: linux
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
serviceAccountName: local-csi-driver
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: local-csi-driver
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
image: docker.io/scylladb/local-csi-driver:0.5.0@sha256:46c1d2e9be8edf076d8dea7e82a3bb763d4bdda5adf43c794f617e9c94251079
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- --listen=/csi/csi.sock
|
||||||
|
- --node-name=$(NODE_NAME)
|
||||||
|
- --volumes-dir=/var/lib/persistent-volumes
|
||||||
|
- --v=2
|
||||||
|
env:
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
volumeMounts:
|
||||||
|
- name: kubelet-dir
|
||||||
|
mountPath: /var/lib/kubelet
|
||||||
|
mountPropagation: "Bidirectional"
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: volumes-dir
|
||||||
|
mountPath: /var/lib/persistent-volumes
|
||||||
|
ports:
|
||||||
|
- name: healthz
|
||||||
|
containerPort: 9809
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: healthz
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
periodSeconds: 2
|
||||||
|
failureThreshold: 5
|
||||||
|
- name: csi-driver-registrar
|
||||||
|
image: registry.k8s.io/sig-storage/csi-node-driver-registrar@sha256:fdff3ee285341bc58033b6b2458a5d45fd90ec6922a8ba6ebdd49b0c41e2cd34
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --kubelet-registration-path=/var/lib/kubelet/plugins/local.csi.scylladb.com/csi.sock
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: registration-dir
|
||||||
|
mountPath: /registration
|
||||||
|
- name: liveness-probe
|
||||||
|
image: registry.k8s.io/sig-storage/livenessprobe@sha256:cacee2b5c36dd59d4c7e8469c05c9e4ef53ecb2df9025fa8c10cdaf61bce62f0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --health-port=9809
|
||||||
|
- --v=2
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
- name: csi-provisioner
|
||||||
|
image: registry.k8s.io/sig-storage/csi-provisioner@sha256:ee3b525d5b89db99da3b8eb521d9cd90cb6e9ef0fbb651e98bb37be78d36b5b8
|
||||||
|
imagePullPolicy: Always
|
||||||
|
args:
|
||||||
|
- --csi-address=/csi/csi.sock
|
||||||
|
- --v=2
|
||||||
|
- --node-deployment
|
||||||
|
- --feature-gates=Topology=true
|
||||||
|
- --immediate-topology=false
|
||||||
|
- --enable-capacity
|
||||||
|
- --capacity-ownerref-level=0
|
||||||
|
- --capacity-poll-interval=30s
|
||||||
|
- --default-fstype=xfs
|
||||||
|
env:
|
||||||
|
- name: NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-dir
|
||||||
|
mountPath: /csi
|
||||||
|
volumes:
|
||||||
|
- name: kubelet-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet
|
||||||
|
type: Directory
|
||||||
|
- name: plugin-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet/plugins/local.csi.scylladb.com/
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: registration-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet/plugins_registry/
|
||||||
|
type: Directory
|
||||||
|
- name: volumes-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/persistent-volumes
|
||||||
|
type: Directory
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: scylladb-config
|
||||||
|
data:
|
||||||
|
scylla.yaml: |
|
||||||
|
authenticator: PasswordAuthenticator
|
||||||
|
authorizer: CassandraAuthorizer
|
||||||
|
---
|
||||||
|
apiVersion: scylla.scylladb.com/v1
|
||||||
|
kind: ScyllaCluster
|
||||||
|
metadata:
|
||||||
|
name: scylladb
|
||||||
|
spec:
|
||||||
|
repository: docker.io/scylladb/scylla
|
||||||
|
version: 2025.1.5
|
||||||
|
agentVersion: 3.5.1
|
||||||
|
developerMode: false
|
||||||
|
automaticOrphanedNodeCleanup: true
|
||||||
|
sysctls:
|
||||||
|
- fs.aio-max-nr=30000000
|
||||||
|
datacenter:
|
||||||
|
name: ge-dc1
|
||||||
|
racks:
|
||||||
|
- name: ge-dc1-rack1
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack1
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
- name: ge-dc1-rack2
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack2
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
- name: ge-dc1-rack3
|
||||||
|
members: 1
|
||||||
|
scyllaConfig: scylladb-config
|
||||||
|
storage:
|
||||||
|
capacity: 45Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2
|
||||||
|
memory: 8Gi
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: topology.kubernetes.io/zone
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ge-dc1-rack3
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager-config
|
||||||
|
namespace: scylla-manager
|
||||||
|
data:
|
||||||
|
scylla-manager.yaml: |-
|
||||||
|
http: :5080
|
||||||
|
logger:
|
||||||
|
level: info
|
||||||
|
database:
|
||||||
|
hosts:
|
||||||
|
- scylla-manager-cluster-manager-dc-manager-rack-0
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
namespace: scylla-manager
|
||||||
|
name: scylla-manager-to-scylla-pod
|
||||||
|
spec:
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/managed-by: scylla-operator
|
||||||
|
app.kubernetes.io/name: scylla
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5080
|
||||||
|
- name: metrics
|
||||||
|
port: 5090
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5090
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager
|
||||||
|
namespace: scylla-manager
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: scylla-manager
|
||||||
|
app.kubernetes.io/instance: scylla-manager
|
||||||
|
spec:
|
||||||
|
serviceAccountName: scylla-manager
|
||||||
|
containers:
|
||||||
|
- name: scylla-manager
|
||||||
|
image: docker.io/scylladb/scylla-manager:3.5.1@sha256:6986ecfc8c925c3d59b65bbcb9763d62f7591a00bb30242842aada115929e816
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /usr/bin/scylla-manager
|
||||||
|
args:
|
||||||
|
- --config-file=/mnt/etc/scylla-manager/scylla-manager.yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 20Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/etc/scylla-manager
|
||||||
|
name: scylla-manager-config
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/v1/clusters
|
||||||
|
port: 5080
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
volumes:
|
||||||
|
- configMap:
|
||||||
|
name: scylla-manager-config
|
||||||
|
name: scylla-manager-config
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: scylla.scylladb.com/v1
|
||||||
|
kind: ScyllaCluster
|
||||||
|
metadata:
|
||||||
|
name: scylla-manager-cluster
|
||||||
|
namespace: scylla-manager
|
||||||
|
spec:
|
||||||
|
version: 2025.1.5
|
||||||
|
agentVersion: 3.5.1@sha256:d1b57d08b9949c8faad2048fdf4dc7c502dae81da856c3c6b3a77dd347d5c7fc
|
||||||
|
repository: docker.io/scylladb/scylla
|
||||||
|
agentRepository: docker.io/scylladb/scylla-manager-agent
|
||||||
|
developerMode: true
|
||||||
|
cpuset: true
|
||||||
|
sysctls:
|
||||||
|
- fs.aio-max-nr=30000000
|
||||||
|
datacenter:
|
||||||
|
name: manager-dc
|
||||||
|
racks:
|
||||||
|
- members: 1
|
||||||
|
name: manager-rack
|
||||||
|
placement:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: scylla.scylladb.com/node-type
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- scylla
|
||||||
|
tolerations:
|
||||||
|
- effect: NoSchedule
|
||||||
|
key: scylla-operator.scylladb.com/dedicated
|
||||||
|
operator: Equal
|
||||||
|
value: scyllaclusters
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 1
|
||||||
|
memory: 200Mi
|
||||||
|
requests:
|
||||||
|
cpu: 1
|
||||||
|
memory: 200Mi
|
||||||
|
storage:
|
||||||
|
capacity: 5Gi
|
||||||
|
storageClassName: scylladb-local-xfs
|
||||||
|
---
|
||||||
@@ -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)
|
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/01-setup.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/02-copy-k8s-files.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/03-cert-manager.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/04-prometheus-operator.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/05-scylla-operator.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/06-scylla-cluster.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/07-scylla-manager.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/08-redis.yml
|
||||||
|
uv run ansible-playbook -i inventory.ini playbooks/09-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
30363364303339383037643864396632306537333737383933363164663838333364323837633838
|
||||||
|
3561323032663461663834613634633734636333306638380a396436646332373330393931396530
|
||||||
|
36353439343736656432333437653764653132393364626136396230623038626330313366386263
|
||||||
|
3630663539303665340a623961656435373337363534363863353934326336653361663538616664
|
||||||
|
36313865646530346633626639393739653131313835383631613833393832396434633038343639
|
||||||
|
62656236326662343236303133306137653533633064626161636165646164646165653534633036
|
||||||
|
63623863353438616237323530383033366138656665613331666532316562336232353239323535
|
||||||
|
33326237653463663862383266373534343434383765303237613436643439313233396338376134
|
||||||
|
65303161313930383165636133616236386435663232313531633461383938326131353930353761
|
||||||
|
35303238333863666137313330336366623536613230353639666133333633353566303131393666
|
||||||
|
32313964636138343632373064366166333134343437613937303538613834623934623161626261
|
||||||
|
63303832663438356331363534316230613561613961306561376432646132663733303366393961
|
||||||
|
64373166633938633730643339646433623235306161386338316330336264623031363365376634
|
||||||
|
38306639356236383464653038663732653465626265663939643234306563346330336139373235
|
||||||
|
36616662313139356139363532633664333762623235326237636339323039373131666162613930
|
||||||
|
63336161396635646565393565373736616431643830366163356532383034396636323163623836
|
||||||
|
37613632343563363032303133333930653431343039633239306561303266393861366134643734
|
||||||
|
66326534343464363634383461326330346432613266653066643139636232656638346635343130
|
||||||
|
61383336623763353034316238313536336261616433356461666537633665636430393139663465
|
||||||
|
61643066356466646264616562343939616336343935663939313537356237303264303761376139
|
||||||
|
34623231623264633934363537643132643336313633663430643265633564646363623130396437
|
||||||
|
61383763396633663439626661333630373662616633353232356165663034336131343666643633
|
||||||
|
373138346631323465303932633962613234
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
- name: Install K3s and Apply Security Configurations on All Nodes
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Install curl
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: curl
|
||||||
|
state: present
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Download k3s install script
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: https://get.k3s.io
|
||||||
|
dest: /tmp/k3s_install.sh
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Install Python Kubernetes client
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: python3-kubernetes
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Disable UFW
|
||||||
|
ansible.builtin.command: ufw disable
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Disable password authentication for SSH
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '^PasswordAuthentication'
|
||||||
|
line: 'PasswordAuthentication no'
|
||||||
|
state: present
|
||||||
|
backup: true
|
||||||
|
|
||||||
|
- name: Disable root password login
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '^PermitRootLogin'
|
||||||
|
line: 'PermitRootLogin prohibit-password'
|
||||||
|
state: present
|
||||||
|
backup: true
|
||||||
|
|
||||||
|
- name: Restart SSH to apply config
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: ssh
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Install fail2ban
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: fail2ban
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Configure fail2ban for SSH
|
||||||
|
ansible.builtin.blockinfile:
|
||||||
|
path: /etc/fail2ban/jail.local
|
||||||
|
create: true
|
||||||
|
block: |
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
port = ssh
|
||||||
|
logpath = /var/log/auth.log
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 3600
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Restart fail2ban
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: fail2ban
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Set up K3s on Init Node
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="server --tls-san {{ ansible_host }} --flannel-backend=wireguard-native --cluster-init" \
|
||||||
|
sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure K3s Service is Started and Enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Get Joining Token
|
||||||
|
ansible.builtin.command: cat /var/lib/rancher/k3s/server/node-token
|
||||||
|
register: k3s_token
|
||||||
|
run_once: true
|
||||||
|
tags: skip_ansible_lint
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set up K3s on Master Nodes
|
||||||
|
hosts: master
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="server --flannel-backend=wireguard-native --tls-san {{ ansible_host }}" \
|
||||||
|
K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||||
|
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure k3s service is started and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Set up K3s on Worker Nodes
|
||||||
|
hosts: worker
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Run k3s Installer
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
INSTALL_K3S_EXEC="agent" K3S_URL="https://{{ hostvars[groups['init'][0]].ansible_host }}:6443" \
|
||||||
|
K3S_TOKEN="{{ hostvars[groups['init'][0]].k3s_token.stdout }}" sh /tmp/k3s_install.sh
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/k3s
|
||||||
|
|
||||||
|
- name: Ensure k3s service is started and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s-agent
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Wait for All K3s Nodes to Become Ready
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Wait until all nodes report Ready
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for=condition=Ready node --all --timeout=10m
|
||||||
|
register: wait_nodes
|
||||||
|
retries: 5
|
||||||
|
delay: 30
|
||||||
|
until: wait_nodes.rc == 0
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set Up Node Labels and Taints
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Set Rack Tags to All Nodes
|
||||||
|
loop: "{{ groups['all'] }}"
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: v1
|
||||||
|
kind: Node
|
||||||
|
name: "{{ item }}"
|
||||||
|
definition:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
topology.kubernetes.io/zone: "{{ hostvars[item].rack }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Set Scylla Tags to Scylla Nodes
|
||||||
|
loop: "{{ groups['scylla'] }}"
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: v1
|
||||||
|
kind: Node
|
||||||
|
name: "{{ item }}"
|
||||||
|
definition:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
scylla.scylladb.com/node-type: scylla
|
||||||
|
spec:
|
||||||
|
taints:
|
||||||
|
- key: scylla-operator.scylladb.com/dedicated
|
||||||
|
value: scyllaclusters
|
||||||
|
effect: NoSchedule
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create Containerd Socket Symlink
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Create Containerd Symlink
|
||||||
|
ansible.builtin.command: |
|
||||||
|
ln -s /run/k3s/containerd/containerd.sock /run/containerd/containerd.sock
|
||||||
|
args:
|
||||||
|
creates: /run/containerd/containerd.sock
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
- name: Copy K8s files to node
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
k8s_files_src: "{{ playbook_dir }}/../k8s"
|
||||||
|
k8s_files_dest: /tmp/k8s
|
||||||
|
tasks:
|
||||||
|
- name: Create destination directory for K8s files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ k8s_files_dest }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Copy K8s files to destination
|
||||||
|
ansible.posix.synchronize:
|
||||||
|
src: "{{ k8s_files_src }}/"
|
||||||
|
dest: "{{ k8s_files_dest }}/"
|
||||||
|
recursive: true
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
- name: Set up Cert-Manager
|
||||||
|
hosts: init
|
||||||
|
gather_facts: false
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
cert_manager_ns: cert-manager
|
||||||
|
cert_manager_crds:
|
||||||
|
- certificates.cert-manager.io
|
||||||
|
- issuers.cert-manager.io
|
||||||
|
cert_manager_deployments:
|
||||||
|
- cert-manager
|
||||||
|
- cert-manager-cainjector
|
||||||
|
- cert-manager-webhook
|
||||||
|
tasks:
|
||||||
|
- name: Apply Cert-Manager YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/01-cert-manager.yml"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for condition=established --timeout=60s crd/{{ cert_manager_crds | join(' crd/') }}
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager deployments
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n {{ cert_manager_ns }} rollout status --timeout=10m deployment.apps/{{ item }}
|
||||||
|
loop: "{{ cert_manager_deployments }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: item
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Cert-Manager webhook CA secret
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for i in {1..30}; do
|
||||||
|
kubectl -n {{ cert_manager_ns }} get secret/cert-manager-webhook-ca && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
- name: Set up Prometheus operator
|
||||||
|
hosts: init
|
||||||
|
gather_facts: false
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
prometheus_crds:
|
||||||
|
- prometheuses.monitoring.coreos.com
|
||||||
|
- prometheusrules.monitoring.coreos.com
|
||||||
|
- servicemonitors.monitoring.coreos.com
|
||||||
|
tasks:
|
||||||
|
- name: Apply Prometheus operator server-side
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl apply --server-side -f /tmp/k8s/scylla/02-prometheus-operator.yml -n prometheus-operator
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Prometheus operator CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for='condition=established' crd/{{ prometheus_crds | join(' crd/') }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Prometheus operator deployment
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n=prometheus-operator rollout status --timeout=10m deployment.apps/prometheus-operator
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
- name: Set up Scylla Operator, NodeConfig, and the Local CSI Driver
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
scylla_crds:
|
||||||
|
- scyllaclusters.scylla.scylladb.com
|
||||||
|
- nodeconfigs.scylla.scylladb.com
|
||||||
|
- scyllaoperatorconfigs.scylla.scylladb.com
|
||||||
|
- scylladbmonitorings.scylla.scylladb.com
|
||||||
|
scylla_deployments:
|
||||||
|
- scylla-operator
|
||||||
|
- webhook-server
|
||||||
|
tasks:
|
||||||
|
- name: Apply Scylla Operator YAML
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl apply --server-side -f /tmp/k8s/scylla/03-scylla-operator.yml -n scylla-operator
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Scylla Operator CRDs
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl wait --for='condition=established' crd/{{ scylla_crds | join(' crd/') }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Scylla Operator Deployments
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
kubectl -n scylla-operator rollout status --timeout=10m deployment.apps/{{ item }}
|
||||||
|
loop: "{{ scylla_deployments }}"
|
||||||
|
loop_control:
|
||||||
|
loop_var: item
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Apply Scylla NodeConfig YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/04-scylla-node-config.yml"
|
||||||
|
namespace: scylla-operator
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla NodeConfig to be applied across all nodes
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl wait --for='condition=Reconciled' --timeout=10m nodeconfigs.scylla.scylladb.com/scylladb-nodepool-1
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Find Scylla CSI Driver K8s YAMLs on Remote Host
|
||||||
|
ansible.builtin.find:
|
||||||
|
paths: /tmp/k8s/scylla
|
||||||
|
patterns: "05-*.yml"
|
||||||
|
file_type: file
|
||||||
|
register: scylla_csi_driver_files
|
||||||
|
|
||||||
|
- name: Apply all Scylla CSI Driver K8s YAMLs
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "{{ item.path }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
loop: "{{ scylla_csi_driver_files.files | sort(attribute='path') }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.path }}"
|
||||||
|
|
||||||
|
- name: Wait for Local CSI Driver DaemonSet
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n=local-csi-driver rollout status --timeout=10m daemonset.apps/local-csi-driver
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
- name: Set Up Scylla Cluster
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Create Scylla Namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: scylla
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Apply Scylla Cluster Config
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/scylla/06-scylla-cluster.yml
|
||||||
|
namespace: scylla
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla Cluster Application
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n scylla rollout status statefulset/{{ item }} --timeout=10m
|
||||||
|
loop:
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack1
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack2
|
||||||
|
- scylladb-ge-dc1-ge-dc1-rack3
|
||||||
|
register: rollout
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
until: rollout.rc == 0
|
||||||
|
ignore_errors: true
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
- name: Set Up Scylla Manager
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Apply Scylla Manager YAML
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "/tmp/k8s/scylla/07-scylla-manager.yml"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Scylla Manager Deployment
|
||||||
|
ansible.builtin.command: |
|
||||||
|
kubectl -n=scylla-manager rollout status --timeout=10m deployment.apps/scylla-manager
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
- name: Set Up Redis Cluster
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Create Redis Config Map
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/01-config-map.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Create Redis Nodes
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/02-redis.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Wait for Redis Nodes
|
||||||
|
ansible.builtin.command:
|
||||||
|
kubectl -n=redis rollout status --timeout=10m sts/redis-cluster
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create ClusterIP Service
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: /tmp/k8s/redis/03-cluster-ip.yml
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Grab Node IPs
|
||||||
|
ansible.builtin.shell:
|
||||||
|
echo $(kubectl get pods -l app=redis-cluster -n redis -o json | jq -r '.items | map(.status.podIP) | join(":6379 ")'):6379
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: redis_node_ips
|
||||||
|
changed_when: false
|
||||||
|
tags:
|
||||||
|
- skip_ansible_lint
|
||||||
|
|
||||||
|
- name: Display Node IPs
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ redis_node_ips.stdout }}"
|
||||||
|
|
||||||
|
- name: Check if Redis cluster is already active
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
set -o pipefail
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli -p 6379 cluster info | grep cluster_state
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: redis_cluster_state
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Clear Redis Nodes
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
for ip in {{ redis_node_ips.stdout }}; do
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli -p 6379 -h $ip flushall;
|
||||||
|
done
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when: "'cluster_state:ok' not in redis_cluster_state.stdout"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Activate Redis Cluster
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
kubectl exec redis-cluster-0 -n redis -- redis-cli --cluster create --cluster-replicas 1 {{ redis_node_ips.stdout }} --cluster-yes
|
||||||
|
when: "'cluster_state:ok' not in redis_cluster_state.stdout"
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
- name: Build and Push Payphone
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Get Cargo Project Metadata
|
||||||
|
ansible.builtin.command: cargo metadata --no-deps --format-version 1
|
||||||
|
register: cargo_metadata
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Parse version
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
app_version: "v{{ (cargo_metadata.stdout | from_json).packages[0].version }}"
|
||||||
|
|
||||||
|
- name: Build and Push the Docker image
|
||||||
|
community.docker.docker_image:
|
||||||
|
source: build
|
||||||
|
build:
|
||||||
|
path: "{{ playbook_dir }}/../"
|
||||||
|
name: registry.nyeki.dev/payphone/payphone
|
||||||
|
tag: "{{ app_version }}"
|
||||||
|
push: true
|
||||||
|
|
||||||
|
- name: Deploy Payphone
|
||||||
|
hosts: init
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- 00-vault.yml
|
||||||
|
tasks:
|
||||||
|
- name: Create Payphone Namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: payphone
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Create Bot Token Secret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
namespace: payphone
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: payphone-secrets
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
PAYPHONE_TOKEN: "{{ bot_token }}"
|
||||||
|
PAYPHONE_TOPGG_TOKEN: "{{ topgg_token }}"
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Set Git credentials
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
kubectl create secret docker-registry payphone-git-credentials \
|
||||||
|
--docker-server=registry.nyeki.dev \
|
||||||
|
--docker-username={{ gitlab_username }} \
|
||||||
|
--docker-password={{ gitlab_pat }} \
|
||||||
|
--docker-email={{ gitlab_email }} \
|
||||||
|
-n payphone \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
- name: Apply Payphone Deployment
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
namespace: payphone
|
||||||
|
definition:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: payphone
|
||||||
|
labels:
|
||||||
|
app: payphone
|
||||||
|
spec:
|
||||||
|
serviceName: payphone
|
||||||
|
replicas: "{{ bot_shards }}"
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: payphone
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: payphone
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: payphone
|
||||||
|
image: registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||||
|
imagePullPolicy: "Always"
|
||||||
|
args: [
|
||||||
|
"start",
|
||||||
|
"--shard",
|
||||||
|
"$(SHARD_ID):$(SHARD_COUNT)",
|
||||||
|
"--database-addr",
|
||||||
|
"scylladb-client.scylla",
|
||||||
|
"--database-username",
|
||||||
|
"cassandra",
|
||||||
|
"--database-password",
|
||||||
|
"cassandra",
|
||||||
|
"--redis-urls",
|
||||||
|
"redis://redis-cluster.redis",
|
||||||
|
"--bot-owner-id",
|
||||||
|
"1486270291410817114"
|
||||||
|
]
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: payphone-secrets
|
||||||
|
env:
|
||||||
|
- name: SHARD_ID
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.labels['apps.kubernetes.io/pod-index']
|
||||||
|
- name: SHARD_COUNT
|
||||||
|
value: "{{ bot_shards | string }}"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "1Gi"
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "100Mi"
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: payphone-git-credentials
|
||||||
|
kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||||
|
|
||||||
|
- name: Rollout New Version
|
||||||
|
ansible.builtin.command:
|
||||||
|
kubectl set image -n payphone sts/payphone payphone=registry.nyeki.dev/payphone/payphone:{{ hostvars['localhost'].app_version }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Bump Payphone Version
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: true
|
||||||
|
tasks:
|
||||||
|
- name: Bump Version
|
||||||
|
ansible.builtin.shell: >
|
||||||
|
cargo set-version --bump patch
|
||||||
|
changed_when: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
- name: APT Update
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
tasks:
|
||||||
|
- name: Update APT package index
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: 3600
|
||||||
|
|
||||||
|
- name: Upgrade APT packages
|
||||||
|
ansible.builtin.apt:
|
||||||
|
upgrade: dist
|
||||||
|
autoremove: yes
|
||||||
|
autoclean: yes
|
||||||
+6
-10
@@ -1,15 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "payphone"
|
name = "payphone"
|
||||||
version = "0.115.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
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",]
|
dependencies = [
|
||||||
|
"ansible>=11.9.0",
|
||||||
[dependency-groups]
|
"ansible-lint>=25.8.1",
|
||||||
dev = [ "black>=25.1.0", "python-dotenv>=1.0.1", "pyyaml>=6.0.2",]
|
"requests>=2.32.4",
|
||||||
|
]
|
||||||
[tool.aerich]
|
|
||||||
tortoise_orm = "payphone.db.setup.CONFIG"
|
|
||||||
location = "./payphone/db/migrations"
|
|
||||||
src_folder = "."
|
|
||||||
|
|||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
uv run ansible-playbook -i inventory.ini playbooks/09-payphone.yml --vault-password-file playbooks/00-vault-pwd.txt
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Module"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user