Compare commits
222
Commits
aca2ce0301
...
rewrite
| 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
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
/target
|
||||
/.venv
|
||||
/.ansible
|
||||
/.vscode
|
||||
/k8s
|
||||
/playbooks
|
||||
/backup
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.python-version
|
||||
|
||||
# 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
|
||||
ansible.cfg
|
||||
compose.yml
|
||||
inventory.ini
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
k8s/
|
||||
.vscode/
|
||||
.git/
|
||||
+6
-158
@@ -1,162 +1,10 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
/target
|
||||
/.venv
|
||||
/.ansible
|
||||
/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
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
00-vault-pwd.txt
|
||||
|
||||
# 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/
|
||||
reset-ssh.sh
|
||||
@@ -0,0 +1,38 @@
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
lint:
|
||||
stage: lint
|
||||
image: rust:latest
|
||||
script:
|
||||
- rustup component add rustfmt clippy
|
||||
- cargo fmt --check
|
||||
- cargo clippy -- -D warnings
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: rust:latest
|
||||
script:
|
||||
- cargo build --release
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: rust:latest
|
||||
script:
|
||||
- cargo test --release
|
||||
artifacts:
|
||||
paths:
|
||||
- "target/"
|
||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"pylint.args": ["--extension-pkg-allow-list=orjson"],
|
||||
"conventionalCommits.scopes": ["shard", "tasks", "kube", "scripts"]
|
||||
"editor.formatOnSave": true,
|
||||
"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
|
||||
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/*
|
||||
|
||||
|
||||
FROM base AS tasks
|
||||
|
||||
CMD [ "tasks.py" ]
|
||||
ENTRYPOINT ["/usr/local/bin/payphone"]
|
||||
CMD ["start"]
|
||||
|
||||
@@ -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 is deployed using Kubernetes + Docker. To set up a local kubernetes cluster, you can use
|
||||
`minikube`.
|
||||
To read the documentation, run
|
||||
|
||||
## Starting a Cluster
|
||||
|
||||
This section will guide you step by step to create a multi-node cluster or a local development
|
||||
cluster and start up Payphone. Make sure to have:
|
||||
|
||||
1. The Discord bot's token, either for development or production depending on what you're setting
|
||||
up the cluster for.
|
||||
2. You'll probably need at least 5 CPU cores (6 recommended) across all of your machines + 1 core
|
||||
per shard, 8 GB of ram + 512Mb of RAM per shard, and 30Gb of disk space available. It'll still
|
||||
run with less, but it'll be way slower. In multi-node environment, these specifications are
|
||||
cluster-wide, meaning that if you have 3 nodes with 3 cores, that makes it 9 cores in total and
|
||||
therefore suitable for a multi-shard deployment. Do not run nodes with less than 2 CPU cores, 2
|
||||
Gb (ideally 4 Gb) of RAM, and 15 Gb of disk space available.
|
||||
|
||||
### Kubernetes Setup
|
||||
|
||||
First of all, you need to install Kubernetes in the machine where you'll set up either a node or a
|
||||
whole cluster.
|
||||
|
||||
Make sure to have Docker installed in your machine before running the following steps. You can
|
||||
check how to install Docker in the
|
||||
[Docker documentation](https://docs.docker.com/get-started/get-docker/). If you're setting up a new
|
||||
node for production, it's better if you only install the Docker Engine without Docker Desktop.
|
||||
|
||||
#### For Development
|
||||
|
||||
Ideally, use `minikube` for local development. This'll allow you to run a complete cluster locally.
|
||||
Make sure to have downloaded and installed minikube first. On Windows, it's available with
|
||||
`chocolatey` running `choco install minikube`. You can read the full set up guide for other
|
||||
installation methods at the [official minikube documentation](https://minikube.sigs.k8s.io/docs/).
|
||||
|
||||
After `minikube` is installed, you need to set up a local cluster. This'll create a virtual machine
|
||||
in a Docker container to act as nodes in your cluster. Run the following command to start a 3-node
|
||||
local cluster:
|
||||
|
||||
```sh
|
||||
minikube start --network-plugin=cni --cni=calico --nodes=3
|
||||
```
|
||||
|
||||
This'll create 3 docker containers and start them, and set them up to use kubectl connecting them
|
||||
automatically. Management `kubectl` commands can only be ran from the master/control-panel node,
|
||||
but with `kubectl` you don't need to worry about that since running `kubectl` from your machine
|
||||
will work as if you were in a master node.
|
||||
|
||||
`kubectl` comes integrated with `Docker Desktop`. You can enable Kubernetes in Docker Desktop at
|
||||
Docker Desktop > Settings > Kubernetes > Enable Kubernetes.
|
||||
|
||||
#### For Production
|
||||
|
||||
These steps are required in order to set up a new `microk8s` node. These installation steps assume
|
||||
they're being ran in an Ubuntu 22.4+ server with at least 2 CPU cores, 4 GB RAM, and 25 GB disk
|
||||
space, and `snap` installed. You also need root privileges to follow these steps.
|
||||
|
||||
First of all, install `microk8s`.
|
||||
|
||||
```sh
|
||||
sudo snap install microk8s --classic
|
||||
```
|
||||
|
||||
After the installation, check that `microk8s` is up and running using the following command:
|
||||
|
||||
```sh
|
||||
microk8s status --wait-ready
|
||||
```
|
||||
|
||||
> [!INFO] In case the last command fails to run because of missing permissions, you'll be guided
|
||||
> on how to give your current user the required permissions to run the command. Follow those steps
|
||||
> and re-run the command afterwards.
|
||||
|
||||
Then, you need to enable the required extensions for `microk8s` to work properly. Run the following
|
||||
commands to enable them:
|
||||
|
||||
```sh
|
||||
microk8s enable dns
|
||||
microk8s enable ha-cluster
|
||||
microk8s enable community
|
||||
|
||||
sudo systemctl enable iscsid # Needed to run the following command.
|
||||
|
||||
microk8s enable openebs
|
||||
```
|
||||
|
||||
The rest of the guide uses `kubectl`, but `microk8s` has `kubectl` as a subcommand, so either:
|
||||
|
||||
1. You create an alias in `.bash_aliases`, appending `alias kubectl='microk8s kubectl'` to your
|
||||
`~/.bash_aliases` file. If you don't have one, create an empty file and add the command in it.
|
||||
After updating the file, run `source ~/.bash_aliases` to apply the changes.
|
||||
|
||||
2. Use `microk8s kubectl` instead of `kubectl` in the following steps. This'll require you to type
|
||||
`microk8s` at the front of all `kubectl` commands that are run from now on.
|
||||
|
||||
To add an alias, run the following command:
|
||||
|
||||
```sh
|
||||
echo "alias kubectl='microk8s kubectl'" >> ~/.bash_aliases && source ~/.bash_aliases
|
||||
```
|
||||
|
||||
##### Add a Node to the Cluster
|
||||
|
||||
To add a new node to the cluster, run the following command on a master node:
|
||||
|
||||
```sh
|
||||
microk8s add-node
|
||||
```
|
||||
|
||||
You'll get a command to paste in your new node. Copy the one that ends with `--worker` and paste it
|
||||
in your new node. Wait a few minutes until it connects, and then you're ready to go! No need to
|
||||
follow the deployment steps if the cluster is already up and running.
|
||||
|
||||
##### SSH Security
|
||||
|
||||
To secure the access to the server, you'll need to follow the following steps:
|
||||
|
||||
###### Disable Password Authentication
|
||||
|
||||
Having SSH password authentication leaves an open door for brute force attacks. Before disabling
|
||||
it, make sure that the server has your SSH key to prevent from getting locked out after proceeding.
|
||||
|
||||
After you have made sure your keys have been properly configured, edit the SSH configuration file:
|
||||
|
||||
```sh
|
||||
nano /etc/ssh/ssh_config
|
||||
```
|
||||
|
||||
In the file, find the following configurations:
|
||||
1. `ChallengeResponseAuthentication`
|
||||
2. `PasswordAuthentication`
|
||||
3. `UsePAM`
|
||||
|
||||
If they're set in the file, set their value to `no`. After that, restart the SSH service:
|
||||
|
||||
```sh
|
||||
systemctl restart ssh
|
||||
```
|
||||
|
||||
###### Set Up `fail2ban`
|
||||
|
||||
The next step to secure SSH is to set up `fail2ban`, which will rate limit failed attempts to log
|
||||
in. First, install it in the server:
|
||||
|
||||
```sh
|
||||
apt update
|
||||
apt install fail2ban
|
||||
```
|
||||
|
||||
Next, make a copy of the configuration file:
|
||||
|
||||
```sh
|
||||
cd /etc/fail2ban
|
||||
cp jail.conf jail.local
|
||||
```
|
||||
|
||||
You now need to update it to enable SSH protection:
|
||||
|
||||
```sh
|
||||
nano jail.local
|
||||
```
|
||||
|
||||
Find the `[ssh]` section and add the following line (or update it if it already exists):
|
||||
|
||||
```toml
|
||||
enabled = true
|
||||
```
|
||||
|
||||
After updating that line, close the file, enable, and start the service:
|
||||
|
||||
```sh
|
||||
systemctl enable fail2ban
|
||||
systemctl start fail2ban
|
||||
|
||||
# Check the status of the service to make sure it was enabled properly. You can exit the status
|
||||
# screen pressing "q".
|
||||
systemctl status fail2ban
|
||||
```
|
||||
|
||||
## Deploying Payphone
|
||||
|
||||
The following steps show how to pull and install Payphone on a running cluster of nodes.
|
||||
|
||||
### Building and Publishing the Payphone Image
|
||||
|
||||
Payphone's container images are hosted here at `git.nyeki.dev`. To login, run the following
|
||||
command:
|
||||
|
||||
```sh
|
||||
docker login git.nyeki.dev
|
||||
```
|
||||
|
||||
A new browser tab will open and you'll be prompted for log in. Once you accept, you'll be logged in
|
||||
to the container registry.
|
||||
|
||||
When you have made an update and want to update the production deployment's image, you need to
|
||||
build and push the image to the container registry:
|
||||
|
||||
```sh
|
||||
docker build -t git.nyeki.dev/payphone/payphone:latest .
|
||||
docker push git.nyeki.dev/payphone/payphone:latest
|
||||
```
|
||||
|
||||
### Starting Payphone in the Cluster
|
||||
|
||||
#### Setting Up Config Files
|
||||
|
||||
First of all, you'll need to set up the environment variables that Payphone needs. For this,
|
||||
you'll need:
|
||||
|
||||
1. A Discord bot token, base64 encoded. You can encode your token at
|
||||
[www.base64encode.org](https://www.base64encode.org/).
|
||||
2. A suggestions channel that the bot can write to (its ID).
|
||||
3. A non-expiring invite to the official server.
|
||||
|
||||
First of all, open `k8s/config.yml`. At the bottom of the file, you'll find a `data` object with
|
||||
some environment variables definitions. Set `SUGGESTIONS_CHANNEL_ID` and `SERVER_INVITE` and save
|
||||
the file.
|
||||
|
||||
Then, after you've base64-encoded your token, open `k8s/secrets.yml` and in a similar `data` object
|
||||
at the bottom of the file, set `TOKEN` to the base64-encoded Discord bot token and save the
|
||||
changes.
|
||||
|
||||
After the files are updated with their new values, run the following commands to apply these
|
||||
changes:
|
||||
|
||||
```sh
|
||||
kubectl apply -f k8s/config.yml
|
||||
kubectl apply -f k8s/secrets.yml
|
||||
```
|
||||
|
||||
If you see a ConfigMap and a Secret being created, that means you've successfully applied the
|
||||
changes.
|
||||
|
||||
#### Setting Up Redis and PostgreSQL
|
||||
|
||||
Now, you can initialize Redis. This step is pretty straightforward and you can forget about it
|
||||
later.
|
||||
|
||||
```sh
|
||||
kubectl apply -f k8s/redis.yml
|
||||
kubectl apply -f k8s/psql.yml
|
||||
```
|
||||
|
||||
#### Setting Up Payphone
|
||||
|
||||
Finally, you need to start the bot's instance. First, log into your `registry.nyeki.dev` account from
|
||||
`kubectl` (using the same credentials that in `git.nyeki.dev`) with the following command:
|
||||
|
||||
```sh
|
||||
kubectl create secret docker-registry regcred --docker-server=https://registry.nyeki.dev/v1/ --docker-username=<your-username> --docker-password=<your-password>
|
||||
```
|
||||
|
||||
After that command is ran, the next step is to update the amount of shards. To set the amount,
|
||||
open `k8s/bot.yml` and set the total number of shards in both `spec.replicas` and
|
||||
`spec.template.spec.containers.bot.env.TOTAL_SHARDS.value`.
|
||||
|
||||
Once it's done, you can save and close the file and run the final command.
|
||||
|
||||
```sh
|
||||
kubectl apply -f k8s/tasks.yml
|
||||
kubectl apply -f k8s/bot.yml
|
||||
```shell
|
||||
$ git clone --branch rewrite https://git.moan.dev/nyeki/payphone.git
|
||||
$ cd payphone
|
||||
$ cargo run -- docs --open
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
name = "payphone"
|
||||
version = "0.113.0"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [ "aerich>=0.8.2", "aiohttp>=3.10.10", "orjson>=3.10.11", "py-cord>=2.6.1", "redis[hiredis]>=5.2.0", "rich>=13.9.4", "sentry-sdk>=2.22.0", "toml>=0.10.2", "tortoise-orm[accel,asyncpg]>=0.21.7", "typer>=0.13.0",]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [ "black>=25.1.0", "python-dotenv>=1.0.1", "pyyaml>=6.0.2",]
|
||||
|
||||
[tool.aerich]
|
||||
tortoise_orm = "payphone.db.setup.CONFIG"
|
||||
location = "./payphone/db/migrations"
|
||||
src_folder = "."
|
||||
dependencies = [
|
||||
"ansible>=11.9.0",
|
||||
"ansible-lint>=25.8.1",
|
||||
"requests>=2.32.4",
|
||||
]
|
||||
|
||||
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