Compare commits
24 Commits
93ce73590e
...
e9c2c456e0
| Author | SHA1 | Date | |
|---|---|---|---|
| e9c2c456e0 | |||
| 21a93d3844 | |||
| 909908c789 | |||
| af2350e3fd | |||
| d9e6a81dac | |||
| 724057a2b7 | |||
| 4f5da676b8 | |||
| 49ffccb0b7 | |||
| c889c41e07 | |||
| 56b09a6de8 | |||
| 7f592493bc | |||
| 7827295637 | |||
| 8fd6cdba66 | |||
| c46bb1cfa5 | |||
| 61e05181ef | |||
| 64a8346f3b | |||
| cf00566297 | |||
| 8c46c38c18 | |||
| bd51a5040b | |||
| e1abd3a0a8 | |||
| c0ddeb8994 | |||
| c28e583205 | |||
| 82a48d2e8b | |||
| 17079f9867 |
134
.dockerignore
Normal file
134
.dockerignore
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# -------------------------------
|
||||||
|
# Системные и скрытые каталоги
|
||||||
|
# -------------------------------
|
||||||
|
.git/
|
||||||
|
.gitea/
|
||||||
|
.hg/
|
||||||
|
.svn/
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Виртуальные окружения, кэш и зависимости
|
||||||
|
# -------------------------------
|
||||||
|
# Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.egg-info/
|
||||||
|
*.eggs/
|
||||||
|
*.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Ruby
|
||||||
|
.bundle/
|
||||||
|
vendor/bundle/
|
||||||
|
|
||||||
|
# Java / JVM
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
|
# Go
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# IDE и редакторы
|
||||||
|
# -------------------------------
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Логи и временные файлы
|
||||||
|
# -------------------------------
|
||||||
|
*.log
|
||||||
|
*.logs
|
||||||
|
*.log.*
|
||||||
|
*.logs.*
|
||||||
|
Logs/
|
||||||
|
Log/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Конфиденциальные файлы и настройки
|
||||||
|
# -------------------------------
|
||||||
|
.env
|
||||||
|
env/
|
||||||
|
*.session
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
|
*.jks
|
||||||
|
credentials/
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Документация, тесты и примеры
|
||||||
|
# -------------------------------
|
||||||
|
docs/
|
||||||
|
examples/
|
||||||
|
tests/
|
||||||
|
test/
|
||||||
|
*.test
|
||||||
|
*.tests
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Базы данных и кэш
|
||||||
|
# -------------------------------
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
*.dump
|
||||||
|
*.sql
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Сборка и артефакты
|
||||||
|
# -------------------------------
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.out
|
||||||
|
*.egg
|
||||||
|
*.wheel
|
||||||
|
*.pyc
|
||||||
|
coverage/
|
||||||
|
.coverage.*
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Файлы проекта
|
||||||
|
# -------------------------------
|
||||||
|
balance.json
|
||||||
135
.gitattributes
vendored
Normal file
135
.gitattributes
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Git LFS: большие бинарные файлы, модели, архивы
|
||||||
|
# =============================================================================
|
||||||
|
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.rar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# ML / Data
|
||||||
|
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pth filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.h5 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pkl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pickle filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tflite filter=lfs diff=lfs merge=lfs -text
|
||||||
|
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Data formats
|
||||||
|
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.parquet filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.arrow filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Автоопределение текста и окончания строк
|
||||||
|
# =============================================================================
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Текстовые файлы
|
||||||
|
# =============================================================================
|
||||||
|
*.py text
|
||||||
|
*.pyi text
|
||||||
|
*.ipynb text
|
||||||
|
*.html text
|
||||||
|
*.css text
|
||||||
|
*.js text
|
||||||
|
*.json text
|
||||||
|
*.md text
|
||||||
|
*.yml text
|
||||||
|
*.yaml text
|
||||||
|
*.xml text
|
||||||
|
*.txt text
|
||||||
|
*.cfg text
|
||||||
|
*.toml text
|
||||||
|
*.ini text
|
||||||
|
*.env text
|
||||||
|
*.c text
|
||||||
|
*.cpp text
|
||||||
|
*.h text
|
||||||
|
*.hpp text
|
||||||
|
*.java text
|
||||||
|
*.sh text
|
||||||
|
*.bat text
|
||||||
|
*.ps1 text
|
||||||
|
*.go text
|
||||||
|
*.rs text
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Бинарные файлы
|
||||||
|
# =============================================================================
|
||||||
|
# Изображения
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.bmp binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.svg text
|
||||||
|
|
||||||
|
# Видео и аудио
|
||||||
|
*.mp4 binary
|
||||||
|
*.mov binary
|
||||||
|
*.avi binary
|
||||||
|
*.mkv binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.wav binary
|
||||||
|
*.flac binary
|
||||||
|
|
||||||
|
# Шрифты
|
||||||
|
*.eot binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.otf binary
|
||||||
|
|
||||||
|
# Архивы и пакеты
|
||||||
|
*.jar binary
|
||||||
|
*.war binary
|
||||||
|
*.ear binary
|
||||||
|
*.egg binary
|
||||||
|
*.whl binary
|
||||||
|
|
||||||
|
# IDE-файлы
|
||||||
|
*.iml binary
|
||||||
|
*.sublime-project binary
|
||||||
|
*.sublime-workspace binary
|
||||||
|
.idea/** binary
|
||||||
|
.vscode/** binary
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GitHub Linguist: указание языка для статистики
|
||||||
|
# =============================================================================
|
||||||
|
*.py linguist-language=Python
|
||||||
|
*.ipynb linguist-language=Jupyter Notebook
|
||||||
|
*.html linguist-language=HTML
|
||||||
|
*.css linguist-language=CSS
|
||||||
|
*.js linguist-language=JavaScript
|
||||||
|
#*.json linguist-language=JSON
|
||||||
|
#*.md linguist-language=Markdown
|
||||||
|
*.yml linguist-language=YAML
|
||||||
|
*.yaml linguist-language=YAML
|
||||||
|
*.c linguist-language=C
|
||||||
|
*.cpp linguist-language=C++
|
||||||
|
*.h linguist-language=C
|
||||||
|
*.hpp linguist-language=C++
|
||||||
|
*.java linguist-language=Java
|
||||||
|
*.go linguist-language=Go
|
||||||
|
*.rs linguist-language=Rust
|
||||||
|
*.sh linguist-language=Shell
|
||||||
|
*.bat linguist-language=Batchfile
|
||||||
|
*.ps1 linguist-language=PowerShell
|
||||||
48
.gitea/workflows/ci.yaml
Normal file
48
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
basic-checks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: "TEST_TOKEN"
|
||||||
|
PREFIX: "!"
|
||||||
|
WELCOME_CHANNEL_ID: "123456789012345678"
|
||||||
|
ADMIN_ROLE_NAME: "Администратор"
|
||||||
|
LOG_LEVEL: "info"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
uses: snok/install-poetry@v1
|
||||||
|
with:
|
||||||
|
version: "1.8.3"
|
||||||
|
virtualenvs-create: true
|
||||||
|
virtualenvs-in-project: true
|
||||||
|
installer-parallel: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
poetry install --no-interaction --no-root
|
||||||
|
|
||||||
|
- name: Basic import checks
|
||||||
|
run: |
|
||||||
|
poetry run python -c "import configs; from bot import Bot; print('Bot class ok')"
|
||||||
|
poetry run python -c "from bot import discbot; print('Global bot instance ok')"
|
||||||
|
|
||||||
|
- name: Load cogs without running bot
|
||||||
|
run: |
|
||||||
|
poetry run python -c "from bot import discbot; import asyncio; asyncio.run(discbot.setup()); print('Cogs loaded')"
|
||||||
149
.gitignore
vendored
Normal file
149
.gitignore
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# ===============================
|
||||||
|
# Системные и скрытые файлы
|
||||||
|
# ===============================
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Python
|
||||||
|
# ===============================
|
||||||
|
# Виртуальные окружения
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Кэш интерпретатора
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Пакеты и сборки
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.eg
|
||||||
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
|
||||||
|
# Poetry
|
||||||
|
poetry.lock
|
||||||
|
.pypoetry/
|
||||||
|
|
||||||
|
# Тестирование и отчеты
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
test/
|
||||||
|
tests/
|
||||||
|
Test/
|
||||||
|
Tests/
|
||||||
|
|
||||||
|
# Логи и базы данных
|
||||||
|
*.log
|
||||||
|
*.logs
|
||||||
|
*.log*
|
||||||
|
*.log.*
|
||||||
|
*.logs.*
|
||||||
|
log/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Базы данных и кэш
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
*.dump
|
||||||
|
*.sql
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Node.js / JS
|
||||||
|
# ===============================
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Ruby
|
||||||
|
# ===============================
|
||||||
|
.bundle/
|
||||||
|
vendor/bundle/
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Java / JVM
|
||||||
|
# ===============================
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Go
|
||||||
|
# ===============================
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# IDE и редакторы
|
||||||
|
# ===============================
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Конфиденциальные файлы и ключи
|
||||||
|
# ===============================
|
||||||
|
.env
|
||||||
|
env
|
||||||
|
*.session
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
|
*.jks
|
||||||
|
credentials/
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Сборка и артефакты
|
||||||
|
# ===============================
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.out
|
||||||
|
*.wheel
|
||||||
|
*.pyc
|
||||||
|
coverage/
|
||||||
|
.coverage.*
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Файлы проекта
|
||||||
|
# ===============================
|
||||||
|
balance.json
|
||||||
26
.idea/Bot.iml
generated
26
.idea/Bot.iml
generated
@@ -1,10 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
|
||||||
</content>
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
<orderEntry type="jdk" jdkName="Python 3.13 (Bot)" jdkType="Python SDK" />
|
</content>
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="jdk" jdkName="Python 3.13 (NotFateKursach)" jdkType="Python SDK" />
|
||||||
</component>
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/configs" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
</module>
|
</module>
|
||||||
12
.idea/misc.xml
generated
12
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.13 (Bot)" />
|
<option name="sdkName" value="Python 3.13 (Bot)" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (Bot)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (NotFateKursach)" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Используем официальный образ Python с подходящей версией
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Устанавливаем Poetry
|
||||||
|
RUN pip install poetry
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем файлы Poetry
|
||||||
|
COPY pyproject.toml poetry.lock* ./
|
||||||
|
|
||||||
|
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера)
|
||||||
|
RUN poetry config virtualenvs.create false
|
||||||
|
|
||||||
|
# Устанавливаем зависимости через Poetry
|
||||||
|
RUN poetry install --no-interaction --no-ansi --no-root
|
||||||
|
|
||||||
|
# Копируем все файлы проекта внутрь контейнера
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Устанавливаем переменную окружения для буферизации
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Команда запуска — запуск скрипта main.py
|
||||||
|
CMD ["python", "main.py"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [2025] [NotFate]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
BIN
assets/photo/default.jpg
Normal file
BIN
assets/photo/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 554 KiB |
4
bot.log
4
bot.log
@@ -1,4 +0,0 @@
|
|||||||
2025-12-08 13:08:26,502:WARNING:discord.client: PyNaCl is not installed, voice will NOT be supported
|
|
||||||
2025-12-08 13:08:26,504:INFO:discord.client: logging in using static token
|
|
||||||
2025-12-08 14:08:52,657:WARNING:discord.client: PyNaCl is not installed, voice will NOT be supported
|
|
||||||
2025-12-08 14:08:52,659:INFO:discord.client: logging in using static token
|
|
||||||
405
bot.py
405
bot.py
@@ -1,405 +0,0 @@
|
|||||||
import discord
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from discord.utils import get
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# --- Настройки и константы ---
|
|
||||||
BOT_TOKEN = '11'
|
|
||||||
WELCOME_CHANNEL_ID = 1342797233250107482 # ID канала для приветствий
|
|
||||||
ADMIN_ROLE_NAME = "Администратор"
|
|
||||||
|
|
||||||
WARNINGS_FILE = "warnings.json"
|
|
||||||
REMINDERS_FILE = "reminders.json"
|
|
||||||
BLACKLIST_FILE = "blacklist.json"
|
|
||||||
|
|
||||||
# --- Логирование ---
|
|
||||||
logging.basicConfig(
|
|
||||||
filename='bot.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s:%(levelname)s:%(name)s: %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Интенты ---
|
|
||||||
intents = discord.Intents.default()
|
|
||||||
intents.message_content = True
|
|
||||||
intents.members = True
|
|
||||||
|
|
||||||
# --- Глобальные переменные ---
|
|
||||||
reminders = []
|
|
||||||
user_warnings = {}
|
|
||||||
blacklist = []
|
|
||||||
|
|
||||||
# --- Хелп команда ---
|
|
||||||
class MyHelpCommand(commands.HelpCommand):
|
|
||||||
async def send_bot_help(self, mapping):
|
|
||||||
channel = self.get_destination()
|
|
||||||
help_text = (
|
|
||||||
"**Доступные команды:**\n"
|
|
||||||
"`!help` — показать это сообщение\n"
|
|
||||||
"`!rules` — показать правила сервера\n"
|
|
||||||
"`!reminder add <минуты> <текст>` — добавить напоминание (только для админов)\n"
|
|
||||||
"`!reminder list` — показать все активные напоминания\n"
|
|
||||||
"`!reminder remove <номер>` — удалить напоминание\n"
|
|
||||||
"`!kick @пользователь [причина]` — исключить участника\n"
|
|
||||||
"`!ban @пользователь [причина]` — забанить участника\n"
|
|
||||||
"`!unban имя#дискриминатор` — разбанить участника\n"
|
|
||||||
"`!mute @пользователь [причина]` — заглушить участника\n"
|
|
||||||
"`!unmute @пользователь` — снять заглушение\n"
|
|
||||||
"`!warn @пользователь [причина]` — выдать предупреждение\n"
|
|
||||||
"`!warnings @пользователь` — посмотреть предупреждения\n"
|
|
||||||
"`!clear <кол-во>` — удалить сообщения\n"
|
|
||||||
"`!blacklist_show` — показать чёрный список (админ)\n"
|
|
||||||
"`!blacklist_add <слово>` — добавить слово в чёрный список (админ)\n"
|
|
||||||
"`!blacklist_remove <слово>` — удалить слово из чёрного списка (админ)\n"
|
|
||||||
)
|
|
||||||
await channel.send(help_text)
|
|
||||||
|
|
||||||
# --- Инициализация бота ---
|
|
||||||
bot = commands.Bot(command_prefix='!', intents=intents, help_command=MyHelpCommand())
|
|
||||||
|
|
||||||
# --- Функции загрузки и сохранения данных ---
|
|
||||||
def load_data():
|
|
||||||
global reminders, user_warnings
|
|
||||||
if os.path.isfile(WARNINGS_FILE):
|
|
||||||
with open(WARNINGS_FILE, 'r', encoding='utf-8') as f:
|
|
||||||
try:
|
|
||||||
user_warnings.update(json.load(f))
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
user_warnings.clear()
|
|
||||||
if os.path.isfile(REMINDERS_FILE):
|
|
||||||
with open(REMINDERS_FILE, 'r', encoding='utf-8') as f:
|
|
||||||
try:
|
|
||||||
reminders.extend(json.load(f))
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
reminders.clear()
|
|
||||||
|
|
||||||
def save_warnings():
|
|
||||||
with open(WARNINGS_FILE, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(user_warnings, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def save_reminders():
|
|
||||||
with open(REMINDERS_FILE, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(reminders, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def load_blacklist_local():
|
|
||||||
global blacklist
|
|
||||||
if os.path.isfile(BLACKLIST_FILE):
|
|
||||||
try:
|
|
||||||
with open(BLACKLIST_FILE, 'r', encoding='utf-8') as f:
|
|
||||||
blacklist = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
blacklist = []
|
|
||||||
else:
|
|
||||||
blacklist = []
|
|
||||||
|
|
||||||
def save_blacklist_local():
|
|
||||||
with open(BLACKLIST_FILE, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(blacklist, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
# --- Проверка и создание ролей ---
|
|
||||||
async def ensure_roles_exist():
|
|
||||||
for guild in bot.guilds:
|
|
||||||
muted_role = get(guild.roles, name="Muted")
|
|
||||||
if not muted_role:
|
|
||||||
try:
|
|
||||||
muted_role = await guild.create_role(name="Muted")
|
|
||||||
for channel in guild.channels:
|
|
||||||
await channel.set_permissions(muted_role, send_messages=False, speak=False)
|
|
||||||
logging.info(f"Создана роль 'Muted' в {guild.name}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при создании роли Muted в {guild.name}: {e}")
|
|
||||||
|
|
||||||
new_member_role = get(guild.roles, name="New Member")
|
|
||||||
if not new_member_role:
|
|
||||||
try:
|
|
||||||
new_member_role = await guild.create_role(name="New Member")
|
|
||||||
logging.info(f"Создана роль 'New Member' в {guild.name}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при создании роли New Member в {guild.name}: {e}")
|
|
||||||
|
|
||||||
# --- События ---
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_ready():
|
|
||||||
print(f'Бот запущен как {bot.user}')
|
|
||||||
if not check_reminders.is_running():
|
|
||||||
check_reminders.start()
|
|
||||||
load_data()
|
|
||||||
load_blacklist_local()
|
|
||||||
await ensure_roles_exist()
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_member_join(member):
|
|
||||||
role = get(member.guild.roles, name="New Member")
|
|
||||||
if role:
|
|
||||||
await member.add_roles(role)
|
|
||||||
channel = bot.get_channel(WELCOME_CHANNEL_ID)
|
|
||||||
if channel:
|
|
||||||
await channel.send(f"Приветствуем {member.mention} на сервере!")
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_message(message):
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
# Проверка на запрещённые слова
|
|
||||||
msg_lower = message.content.lower()
|
|
||||||
if any(word in msg_lower for word in blacklist):
|
|
||||||
try:
|
|
||||||
await message.delete()
|
|
||||||
await message.channel.send(f"{message.author.mention}, ваше сообщение содержит запрещённые слова.")
|
|
||||||
logging.info(f"Удалено сообщение с запрещёнными словами от {message.author} в {message.channel}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при удалении сообщения: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
await bot.process_commands(message)
|
|
||||||
|
|
||||||
# --- Проверка прав администратора ---
|
|
||||||
def is_admin():
|
|
||||||
def predicate(ctx):
|
|
||||||
return get(ctx.author.roles, name=ADMIN_ROLE_NAME) is not None or ctx.author.guild_permissions.administrator
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
# --- Команды модерации и управления ---
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def blacklist_show(ctx):
|
|
||||||
if not blacklist:
|
|
||||||
await ctx.send("Чёрный список пуст.")
|
|
||||||
else:
|
|
||||||
await ctx.send("Чёрный список:\n" + ", ".join(blacklist))
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def blacklist_add(ctx, *, word: str):
|
|
||||||
word = word.lower()
|
|
||||||
if word in blacklist:
|
|
||||||
await ctx.send(f"Слово `{word}` уже в чёрном списке.")
|
|
||||||
else:
|
|
||||||
blacklist.append(word)
|
|
||||||
save_blacklist_local()
|
|
||||||
await ctx.send(f"Слово `{word}` добавлено в чёрный список.")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def blacklist_remove(ctx, *, word: str):
|
|
||||||
word = word.lower()
|
|
||||||
if word not in blacklist:
|
|
||||||
await ctx.send(f"Слово `{word}` отсутствует в чёрном списке.")
|
|
||||||
else:
|
|
||||||
blacklist.remove(word)
|
|
||||||
save_blacklist_local()
|
|
||||||
await ctx.send(f"Слово `{word}` удалено из чёрного списка.")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def rules(ctx):
|
|
||||||
rules_text = (
|
|
||||||
"**Правила сервера:**\n"
|
|
||||||
"1. Уважайте других участников.\n"
|
|
||||||
"2. Запрещена реклама и спам.\n"
|
|
||||||
"3. Не используйте запрещённые слова.\n"
|
|
||||||
"4. Соблюдайте тематику каналов.\n"
|
|
||||||
"5. Выполняйте указания модераторов.\n"
|
|
||||||
)
|
|
||||||
await ctx.send(rules_text)
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def kick(ctx, member: discord.Member, *, reason=None):
|
|
||||||
try:
|
|
||||||
await member.kick(reason=reason)
|
|
||||||
await ctx.send(f"{member} был исключён. Причина: {reason}")
|
|
||||||
logging.info(f"{member} был исключён администратором {ctx.author}. Причина: {reason}")
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send(f"Не удалось исключить {member}.")
|
|
||||||
logging.error(f"Ошибка при исключении: {e}")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def ban(ctx, member: discord.Member, *, reason=None):
|
|
||||||
try:
|
|
||||||
await member.ban(reason=reason)
|
|
||||||
await ctx.send(f"{member} был забанен. Причина: {reason}")
|
|
||||||
logging.info(f"{member} был забанен администратором {ctx.author}. Причина: {reason}")
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send(f"Не удалось забанить {member}.")
|
|
||||||
logging.error(f"Ошибка при бане: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@commands.has_permissions(ban_members=True)
|
|
||||||
async def unban(ctx, *, member_name):
|
|
||||||
banned_users = []
|
|
||||||
async for ban_entry in ctx.guild.bans():
|
|
||||||
banned_users.append(ban_entry)
|
|
||||||
|
|
||||||
# Если указан полный тег с #
|
|
||||||
if '#' in member_name:
|
|
||||||
try:
|
|
||||||
name, discriminator = member_name.split('#')
|
|
||||||
except ValueError:
|
|
||||||
await ctx.send("Неверный формат пользователя. Используйте Имя#Тег.")
|
|
||||||
return
|
|
||||||
|
|
||||||
for ban_entry in banned_users:
|
|
||||||
user = ban_entry.user
|
|
||||||
if (user.name, user.discriminator) == (name, discriminator):
|
|
||||||
try:
|
|
||||||
await ctx.guild.unban(user)
|
|
||||||
await ctx.send(f"Пользователь {user} разбанен.")
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send("Ошибка при разбане.")
|
|
||||||
logging.error(f"Ошибка при разбане: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
await ctx.send(f"Пользователь {member_name} не найден в бан-листе.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Если указан только имя без тега — ищем все совпадения по имени
|
|
||||||
matching = [ban_entry.user for ban_entry in banned_users if ban_entry.user.name.lower() == member_name.lower()]
|
|
||||||
|
|
||||||
if not matching:
|
|
||||||
await ctx.send(f"Пользователь с именем `{member_name}` не найден в бан-листе.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(matching) == 1:
|
|
||||||
user = matching[0]
|
|
||||||
try:
|
|
||||||
await ctx.guild.unban(user)
|
|
||||||
await ctx.send(f"Пользователь {user} разбанен.")
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send("Ошибка при разбане.")
|
|
||||||
logging.error(f"Ошибка при разбане: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Если совпадений несколько — выводим список для выбора
|
|
||||||
msg = "Найдено несколько пользователей с таким именем. Укажите полный тег для разбанивания:\n"
|
|
||||||
for user in matching:
|
|
||||||
msg += f"- {user.name}#{user.discriminator}\n"
|
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def mute(ctx, member: discord.Member, *, reason=None):
|
|
||||||
muted_role = get(ctx.guild.roles, name="Muted")
|
|
||||||
if not muted_role:
|
|
||||||
await ctx.send("Роль Muted не найдена.")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await member.add_roles(muted_role)
|
|
||||||
await ctx.send(f"{member} заглушен. Причина: {reason}")
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send("Не удалось выдать мут.")
|
|
||||||
logging.error(f"Ошибка при муте: {e}")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def unmute(ctx, member: discord.Member):
|
|
||||||
muted_role = get(ctx.guild.roles, name="Muted")
|
|
||||||
if not muted_role:
|
|
||||||
await ctx.send("Роль Muted не найдена.")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await member.remove_roles(muted_role)
|
|
||||||
await ctx.send(f"С мутом снято с {member}.")
|
|
||||||
except Exception as e:
|
|
||||||
await ctx.send("Не удалось снять мут.")
|
|
||||||
logging.error(f"Ошибка при снятии мута: {e}")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def warn(ctx, member: discord.Member, *, reason=None):
|
|
||||||
user_id = str(member.id)
|
|
||||||
if user_id not in user_warnings:
|
|
||||||
user_warnings[user_id] = []
|
|
||||||
user_warnings[user_id].append({"reason": reason or "Без причины", "date": str(datetime.datetime.now())})
|
|
||||||
save_warnings()
|
|
||||||
await ctx.send(f"{member} получил предупреждение. Причина: {reason or 'Без причины'}")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def warnings(ctx, member: discord.Member):
|
|
||||||
user_id = str(member.id)
|
|
||||||
warns = user_warnings.get(user_id, [])
|
|
||||||
if not warns:
|
|
||||||
await ctx.send(f"У пользователя {member} нет предупреждений.")
|
|
||||||
return
|
|
||||||
msg = f"Предупреждения пользователя {member}:\n"
|
|
||||||
for i, w in enumerate(warns, 1):
|
|
||||||
msg += f"{i}. {w['reason']} ({w['date']})\n"
|
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
@is_admin()
|
|
||||||
async def clear(ctx, amount: int):
|
|
||||||
if amount <= 0:
|
|
||||||
await ctx.send("Количество должно быть положительным числом.")
|
|
||||||
return
|
|
||||||
deleted = await ctx.channel.purge(limit=amount + 1)
|
|
||||||
await ctx.send(f"Удалено сообщений: {len(deleted)-1}", delete_after=5)
|
|
||||||
|
|
||||||
# --- Команды напоминаний ---
|
|
||||||
@bot.group()
|
|
||||||
@is_admin()
|
|
||||||
async def reminder(ctx):
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
await ctx.send("Используйте `!reminder add <минуты> <текст>`, `!reminder list` или `!reminder remove <номер>`")
|
|
||||||
|
|
||||||
@reminder.command(name="add")
|
|
||||||
async def reminder_add(ctx, minutes: int, *, text: str):
|
|
||||||
if minutes <= 0:
|
|
||||||
await ctx.send("Время должно быть положительным числом минут.")
|
|
||||||
return
|
|
||||||
remind_time = datetime.datetime.now() + datetime.timedelta(minutes=minutes)
|
|
||||||
reminders.append({
|
|
||||||
"time": remind_time.timestamp(),
|
|
||||||
"channel_id": ctx.channel.id,
|
|
||||||
"user_mention": ctx.author.mention,
|
|
||||||
"text": text
|
|
||||||
})
|
|
||||||
save_reminders()
|
|
||||||
await ctx.send(f"Напоминание добавлено через {minutes} минут: {text}")
|
|
||||||
|
|
||||||
@reminder.command(name="list")
|
|
||||||
async def reminder_list(ctx):
|
|
||||||
if not reminders:
|
|
||||||
await ctx.send("Активных напоминаний нет.")
|
|
||||||
return
|
|
||||||
msg = "Активные напоминания:\n"
|
|
||||||
for i, rem in enumerate(reminders, 1):
|
|
||||||
t = datetime.datetime.fromtimestamp(rem["time"]).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
msg += f"{i}. Через {t} — {rem['text']} (от {rem['user_mention']})\n"
|
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@reminder.command(name="remove")
|
|
||||||
async def reminder_remove(ctx, number: int):
|
|
||||||
if number <= 0 or number > len(reminders):
|
|
||||||
await ctx.send("Неверный номер напоминания.")
|
|
||||||
return
|
|
||||||
removed = reminders.pop(number - 1)
|
|
||||||
save_reminders()
|
|
||||||
await ctx.send(f"Удалено напоминание: {removed['text']}")
|
|
||||||
|
|
||||||
# --- Фоновая задача проверки напоминаний ---
|
|
||||||
@tasks.loop(seconds=30)
|
|
||||||
async def check_reminders():
|
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
to_remove = []
|
|
||||||
for rem in reminders:
|
|
||||||
if rem['time'] <= now:
|
|
||||||
channel = bot.get_channel(rem['channel_id'])
|
|
||||||
if channel:
|
|
||||||
await channel.send(f"{rem['user_mention']} Напоминание: {rem['text']}")
|
|
||||||
to_remove.append(rem)
|
|
||||||
for rem in to_remove:
|
|
||||||
reminders.remove(rem)
|
|
||||||
if to_remove:
|
|
||||||
save_reminders()
|
|
||||||
|
|
||||||
# --- Запуск бота ---
|
|
||||||
bot.run(BOT_TOKEN)
|
|
||||||
3
bot/__init__.py
Normal file
3
bot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .cogs import *
|
||||||
|
from .bot import *
|
||||||
|
from .storage import *
|
||||||
118
bot/bot.py
Normal file
118
bot/bot.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from discord import Intents
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from configs import settings
|
||||||
|
from .help import MyHelpCommand
|
||||||
|
from middleware.loggers import logger
|
||||||
|
from .storage import storage
|
||||||
|
|
||||||
|
__all__ = ("Bot", "discbot")
|
||||||
|
|
||||||
|
|
||||||
|
class Bot(commands.Bot):
|
||||||
|
"""
|
||||||
|
Основной класс Discord-бота с методами настройки и запуска.
|
||||||
|
|
||||||
|
Поддерживает передачу token, prefix, intents и help_command в конструктор.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token: Optional[str] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
intents: Optional[Intents] = None,
|
||||||
|
help_command: Optional[commands.HelpCommand] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
:param token: Токен бота (если None — берётся из settings.BOT_TOKEN).
|
||||||
|
:param prefix: Префикс команд (если None — берётся из settings.PREFIX или '!').
|
||||||
|
:param intents: Intents (если None — создаются стандартные + privileged).
|
||||||
|
:param help_command: Кастомная команда помощи.
|
||||||
|
"""
|
||||||
|
# Intents по умолчанию
|
||||||
|
if intents is None:
|
||||||
|
intents = Intents.default()
|
||||||
|
intents.guilds = True
|
||||||
|
intents.message_content = True # Требует включения в Developer Portal
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
# Префикс по умолчанию
|
||||||
|
command_prefix: str = prefix or getattr(settings, "PREFIX", "!")
|
||||||
|
|
||||||
|
# Help-команда по умолчанию
|
||||||
|
if help_command is None:
|
||||||
|
help_command = MyHelpCommand()
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
command_prefix=command_prefix,
|
||||||
|
intents=intents,
|
||||||
|
help_command=help_command,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем токен и хранилище
|
||||||
|
self._token: Optional[str] = token
|
||||||
|
self.storage = storage # type: ignore[assignment]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Токен бота: сначала из конструктора, затем из settings.
|
||||||
|
"""
|
||||||
|
return self._token or settings.BOT_TOKEN
|
||||||
|
|
||||||
|
async def setup(self) -> None:
|
||||||
|
"""
|
||||||
|
Инициализация бота: логгер, cogs, логирование discord.py.
|
||||||
|
"""
|
||||||
|
logger.setup(start=True)
|
||||||
|
logger.info(text="Настройка бота...", log_type="SYSTEM")
|
||||||
|
|
||||||
|
await self.load_cogs()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.WARNING,
|
||||||
|
format="%(asctime)s:%(levelname)s:%(name)s: %(message)s",
|
||||||
|
)
|
||||||
|
logging.getLogger("discord").setLevel(logging.INFO)
|
||||||
|
|
||||||
|
async def load_cogs(self) -> None:
|
||||||
|
"""
|
||||||
|
Загрузить все модули cogs.
|
||||||
|
"""
|
||||||
|
cogs: list[str] = [
|
||||||
|
"cogs.events",
|
||||||
|
"cogs.moderation",
|
||||||
|
"cogs.blacklist",
|
||||||
|
"cogs.reminders",
|
||||||
|
]
|
||||||
|
for cog in cogs:
|
||||||
|
try:
|
||||||
|
await self.load_extension(cog)
|
||||||
|
logger.info(f"Загружен cog: {cog}", log_type="COGS")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка загрузки {cog}: {e}", log_type="COGS")
|
||||||
|
|
||||||
|
async def start_bot(self, token: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Запуск бота с использованием сохранённого токена или переданного.
|
||||||
|
|
||||||
|
:param token: Токен бота (если None — используется self.token).
|
||||||
|
"""
|
||||||
|
use_token: Optional[str] = token or self.token
|
||||||
|
if not use_token:
|
||||||
|
error: str = "BOT_TOKEN не задан (ни в конструкторе, ни в settings)"
|
||||||
|
logger.error(error)
|
||||||
|
raise ValueError(error)
|
||||||
|
|
||||||
|
logger.info(text="Запуск бота...", log_type="START")
|
||||||
|
await self.start(use_token)
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр — МОЖНО ПЕРЕДАВАТЬ token/prefix ПРЯМО ЗДЕСЬ
|
||||||
|
discbot: Bot = Bot(
|
||||||
|
token=settings.BOT_TOKEN, # кастомный токен
|
||||||
|
prefix=settings.PREFIX, # кастомный префикс
|
||||||
|
)
|
||||||
4
bot/cogs/__init__.py
Normal file
4
bot/cogs/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .events import *
|
||||||
|
from .blacklist import *
|
||||||
|
from .reminders import *
|
||||||
|
from .moderation import *
|
||||||
66
bot/cogs/blacklist.py
Normal file
66
bot/cogs/blacklist.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from ..storage import storage
|
||||||
|
from .moderation import is_admin
|
||||||
|
|
||||||
|
|
||||||
|
class Blacklist(commands.Cog):
|
||||||
|
"""
|
||||||
|
Cog для управления чёрным списком слов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def blacklist_show(self, ctx: commands.Context) -> None:
|
||||||
|
"""
|
||||||
|
Показать текущий чёрный список слов.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
"""
|
||||||
|
if not storage.blacklist:
|
||||||
|
await ctx.send("Чёрный список пуст.")
|
||||||
|
else:
|
||||||
|
await ctx.send("Чёрный список:\n" + ", ".join(storage.blacklist))
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def blacklist_add(self, ctx: commands.Context, *, word: str) -> None:
|
||||||
|
"""
|
||||||
|
Добавить слово в чёрный список.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param word: Слово для добавления.
|
||||||
|
"""
|
||||||
|
word_lower: str = word.lower()
|
||||||
|
if word_lower in storage.blacklist:
|
||||||
|
await ctx.send(f"Слово `{word_lower}` уже в чёрном списке.")
|
||||||
|
return
|
||||||
|
|
||||||
|
storage.blacklist.append(word_lower)
|
||||||
|
storage.save_blacklist()
|
||||||
|
await ctx.send(f"Слово `{word_lower}` добавлено в чёрный список.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def blacklist_remove(self, ctx: commands.Context, *, word: str) -> None:
|
||||||
|
"""
|
||||||
|
Удалить слово из чёрного списка.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param word: Слово для удаления.
|
||||||
|
"""
|
||||||
|
word_lower: str = word.lower()
|
||||||
|
if word_lower not in storage.blacklist:
|
||||||
|
await ctx.send(f"Слово `{word_lower}` отсутствует в чёрном списке.")
|
||||||
|
return
|
||||||
|
|
||||||
|
storage.blacklist.remove(word_lower)
|
||||||
|
storage.save_blacklist()
|
||||||
|
await ctx.send(f"Слово `{word_lower}` удалено из чёрного списка.")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
await bot.add_cog(Blacklist(bot))
|
||||||
129
bot/cogs/events.py
Normal file
129
bot/cogs/events.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
from discord.utils import get
|
||||||
|
|
||||||
|
from configs import settings
|
||||||
|
from middleware import logger
|
||||||
|
from ..storage import storage, Reminder
|
||||||
|
|
||||||
|
|
||||||
|
class Events(commands.Cog):
|
||||||
|
"""
|
||||||
|
Cog с обработчиками событий и фоновой задачей напоминаний.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
self.check_reminders.start()
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self) -> None:
|
||||||
|
"""
|
||||||
|
Событие запуска бота.
|
||||||
|
Загружает данные и создаёт необходимые роли.
|
||||||
|
"""
|
||||||
|
logger.info(text=f"Бот запущен как {self.bot.user}")
|
||||||
|
storage.load_all()
|
||||||
|
await self.ensure_roles_exist()
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_member_join(self, member: discord.Member) -> None:
|
||||||
|
"""
|
||||||
|
Событие вступления нового участника на сервер.
|
||||||
|
|
||||||
|
:param member: Новый участник.
|
||||||
|
"""
|
||||||
|
new_member_role: discord.Role | None = get(member.guild.roles, name="New Member")
|
||||||
|
if new_member_role:
|
||||||
|
await member.add_roles(new_member_role)
|
||||||
|
|
||||||
|
channel: discord.abc.MessageableChannel | None = self.bot.get_channel(
|
||||||
|
settings.WELCOME_CHANNEL_ID
|
||||||
|
)
|
||||||
|
if isinstance(channel, discord.TextChannel):
|
||||||
|
await channel.send(f"Приветствуем {member.mention} на сервере!")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message(self, message: discord.Message) -> None:
|
||||||
|
"""
|
||||||
|
Событие получения сообщения. Проверяет чёрный список слов.
|
||||||
|
|
||||||
|
:param message: Полученное сообщение.
|
||||||
|
"""
|
||||||
|
if message.author.bot or not message.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_lower: str = message.content.lower()
|
||||||
|
if any(word in msg_lower for word in storage.blacklist):
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
await message.channel.send(
|
||||||
|
f"{message.author.mention}, ваше сообщение содержит запрещённые слова."
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.bot.process_commands(message)
|
||||||
|
|
||||||
|
async def ensure_roles_exist(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверяет наличие ролей Muted и New Member и создаёт их при необходимости.
|
||||||
|
"""
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
muted_role: discord.Role | None = get(guild.roles, name="Muted")
|
||||||
|
if muted_role is None:
|
||||||
|
try:
|
||||||
|
muted_role = await guild.create_role(name="Muted")
|
||||||
|
for channel in guild.channels:
|
||||||
|
await channel.set_permissions(
|
||||||
|
muted_role, send_messages=False, speak=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_member_role: discord.Role | None = get(guild.roles, name="New Member")
|
||||||
|
if new_member_role is None:
|
||||||
|
try:
|
||||||
|
await guild.create_role(name="New Member")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@tasks.loop(seconds=30)
|
||||||
|
async def check_reminders(self) -> None:
|
||||||
|
"""
|
||||||
|
Фоновая задача, которая каждые 30 секунд проверяет напоминания
|
||||||
|
и отправляет просроченные.
|
||||||
|
"""
|
||||||
|
now: float = datetime.datetime.now().timestamp()
|
||||||
|
to_remove: list[Reminder] = []
|
||||||
|
|
||||||
|
for rem in storage.reminders:
|
||||||
|
if rem.time <= now:
|
||||||
|
channel = self.bot.get_channel(rem.channel_id)
|
||||||
|
if isinstance(channel, discord.TextChannel):
|
||||||
|
await channel.send(f"{rem.user_mention} Напоминание: {rem.text}")
|
||||||
|
to_remove.append(rem)
|
||||||
|
|
||||||
|
if to_remove:
|
||||||
|
for rem in to_remove:
|
||||||
|
storage.reminders.remove(rem)
|
||||||
|
storage.save_reminders()
|
||||||
|
|
||||||
|
@check_reminders.before_loop
|
||||||
|
async def before_check_reminders(self) -> None:
|
||||||
|
"""
|
||||||
|
Ожидание готовности бота перед стартом фоновой задачи.
|
||||||
|
"""
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
"""
|
||||||
|
Функция для загрузки Cog.
|
||||||
|
|
||||||
|
:param bot: Экземпляр бота.
|
||||||
|
"""
|
||||||
|
await bot.add_cog(Events(bot))
|
||||||
329
bot/cogs/moderation.py
Normal file
329
bot/cogs/moderation.py
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable, Awaitable, Optional
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.utils import get
|
||||||
|
|
||||||
|
from configs import settings
|
||||||
|
from ..storage import storage
|
||||||
|
|
||||||
|
|
||||||
|
# Тип предиката для check (для читаемости, но не обязателен)
|
||||||
|
CheckPredicate = Callable[[commands.Context], Awaitable[bool]]
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin():
|
||||||
|
"""
|
||||||
|
Создаёт декоратор проверки прав администратора:
|
||||||
|
либо есть роль с именем settings.ADMIN_ROLE_NAME,
|
||||||
|
либо у пользователя есть флаг администратора сервера.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def predicate(ctx: commands.Context) -> bool:
|
||||||
|
author = ctx.author
|
||||||
|
if not isinstance(author, discord.Member):
|
||||||
|
return False
|
||||||
|
|
||||||
|
admin_role = get(author.roles, name=settings.ADMIN_ROLE_NAME)
|
||||||
|
return bool(admin_role) or author.guild_permissions.administrator
|
||||||
|
|
||||||
|
return commands.check(predicate)
|
||||||
|
|
||||||
|
|
||||||
|
def require_guild(ctx: commands.Context) -> Optional[discord.Guild]:
|
||||||
|
"""
|
||||||
|
Безопасно получить guild из контекста.
|
||||||
|
|
||||||
|
Если команда вызвана не на сервере (в ЛС), возвращает None.
|
||||||
|
"""
|
||||||
|
return ctx.guild
|
||||||
|
|
||||||
|
|
||||||
|
class Moderation(commands.Cog):
|
||||||
|
"""
|
||||||
|
Cog с модерационными командами:
|
||||||
|
rules, kick, ban, unban, mute, unmute, warn, warnings, clear.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
"""
|
||||||
|
:param bot: Экземпляр бота, к которому привязан cog.
|
||||||
|
"""
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def rules(self, ctx: commands.Context) -> None:
|
||||||
|
"""
|
||||||
|
Показать правила сервера.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
"""
|
||||||
|
rules_text: str = (
|
||||||
|
"**Правила сервера:**\n"
|
||||||
|
"1. Уважайте других участников.\n"
|
||||||
|
"2. Запрещена реклама и спам.\n"
|
||||||
|
"3. Не используйте запрещённые слова.\n"
|
||||||
|
"4. Соблюдайте тематику каналов.\n"
|
||||||
|
"5. Выполняйте указания модераторов.\n"
|
||||||
|
)
|
||||||
|
await ctx.send(rules_text)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def kick(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
member: discord.Member,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Исключить участника с сервера.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник для исключения.
|
||||||
|
:param reason: Причина исключения.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await member.kick(reason=reason)
|
||||||
|
await ctx.send(f"{member} был исключён. Причина: {reason}")
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("Недостаточно прав для исключения этого участника.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def ban(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
member: discord.Member,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Забанить участника на сервере.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник для бана.
|
||||||
|
:param reason: Причина бана.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await member.ban(reason=reason)
|
||||||
|
await ctx.send(f"{member} был забанен. Причина: {reason}")
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("Недостаточно прав для бана этого участника.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.has_permissions(ban_members=True)
|
||||||
|
async def unban(self, ctx: commands.Context, *, member_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Разбанить пользователя по имени или тегу.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member_name: Имя или имя#дискриминатор.
|
||||||
|
"""
|
||||||
|
guild = require_guild(ctx)
|
||||||
|
if guild is None:
|
||||||
|
await ctx.send("Команду можно использовать только на сервере.")
|
||||||
|
return
|
||||||
|
|
||||||
|
banned_users: list[discord.guild.BanEntry] = [
|
||||||
|
ban_entry async for ban_entry in guild.bans()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Вариант с полным тегом
|
||||||
|
if "#" in member_name:
|
||||||
|
try:
|
||||||
|
name, discriminator = member_name.split("#", maxsplit=1)
|
||||||
|
except ValueError:
|
||||||
|
await ctx.send("Неверный формат пользователя. Используйте Имя#Тег.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for ban_entry in banned_users:
|
||||||
|
user = ban_entry.user
|
||||||
|
if (user.name, user.discriminator) == (name, discriminator):
|
||||||
|
try:
|
||||||
|
await guild.unban(user)
|
||||||
|
await ctx.send(f"Пользователь {user} разбанен.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send("Ошибка при разбане пользователя.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.send(f"Пользователь {member_name} не найден в бан-листе.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Вариант только с именем
|
||||||
|
matching = [
|
||||||
|
ban_entry.user
|
||||||
|
for ban_entry in banned_users
|
||||||
|
if ban_entry.user.name.lower() == member_name.lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
if not matching:
|
||||||
|
await ctx.send(
|
||||||
|
f"Пользователь с именем `{member_name}` не найден в бан-листе."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(matching) == 1:
|
||||||
|
user = matching[0]
|
||||||
|
try:
|
||||||
|
await guild.unban(user)
|
||||||
|
await ctx.send(f"Пользователь {user} разбанен.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send("Ошибка при разбане пользователя.")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_lines: list[str] = [
|
||||||
|
"Найдено несколько пользователей с таким именем. "
|
||||||
|
"Укажите полный тег для разбанивания:",
|
||||||
|
]
|
||||||
|
for user in matching:
|
||||||
|
msg_lines.append(f"- {user.name}#{user.discriminator}")
|
||||||
|
await ctx.send("\n".join(msg_lines))
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def mute(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
member: discord.Member,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Выдать участнику мут (роль Muted).
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник, которому выдаётся мут.
|
||||||
|
:param reason: Причина мута.
|
||||||
|
"""
|
||||||
|
guild = require_guild(ctx)
|
||||||
|
if guild is None:
|
||||||
|
await ctx.send("Команду можно использовать только на сервере.")
|
||||||
|
return
|
||||||
|
|
||||||
|
muted_role: Optional[discord.Role] = get(guild.roles, name="Muted")
|
||||||
|
if muted_role is None:
|
||||||
|
await ctx.send("Роль Muted не найдена.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await member.add_roles(muted_role, reason=reason)
|
||||||
|
await ctx.send(f"{member} заглушен. Причина: {reason}")
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("Недостаточно прав для выдачи мута.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def unmute(self, ctx: commands.Context, member: discord.Member) -> None:
|
||||||
|
"""
|
||||||
|
Снять мут с участника.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник, с которого снимается мут.
|
||||||
|
"""
|
||||||
|
guild = require_guild(ctx)
|
||||||
|
if guild is None:
|
||||||
|
await ctx.send("Команду можно использовать только на сервере.")
|
||||||
|
return
|
||||||
|
|
||||||
|
muted_role: Optional[discord.Role] = get(guild.roles, name="Muted")
|
||||||
|
if muted_role is None:
|
||||||
|
await ctx.send("Роль Muted не найдена.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await member.remove_roles(muted_role)
|
||||||
|
await ctx.send(f"Мут снят с {member}.")
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("Недостаточно прав для снятия мута.")
|
||||||
|
except discord.HTTPException:
|
||||||
|
await ctx.send("Не удалось снять мут из-за ошибки Discord.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def warn(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
member: discord.Member,
|
||||||
|
*,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Выдать предупреждение участнику.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник.
|
||||||
|
:param reason: Причина предупреждения.
|
||||||
|
"""
|
||||||
|
user_id: str = str(member.id)
|
||||||
|
storage.user_warnings.setdefault(user_id, []).append(
|
||||||
|
{
|
||||||
|
"reason": reason or "Без причины",
|
||||||
|
"date": datetime.datetime.now().isoformat(
|
||||||
|
sep=" ", timespec="seconds"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
storage.save_warnings()
|
||||||
|
await ctx.send(
|
||||||
|
f"{member} получил предупреждение. Причина: {reason or 'Без причины'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def warnings(self, ctx: commands.Context, member: discord.Member) -> None:
|
||||||
|
"""
|
||||||
|
Показать предупреждения участника.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param member: Участник.
|
||||||
|
"""
|
||||||
|
user_id: str = str(member.id)
|
||||||
|
warns = storage.user_warnings.get(user_id, [])
|
||||||
|
if not warns:
|
||||||
|
await ctx.send(f"У пользователя {member} нет предупреждений.")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines: list[str] = [f"Предупреждения пользователя {member}:"]
|
||||||
|
for i, w in enumerate(warns, 1):
|
||||||
|
lines.append(f"{i}. {w['reason']} ({w['date']})")
|
||||||
|
await ctx.send("\n".join(lines))
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@is_admin()
|
||||||
|
async def clear(self, ctx: commands.Context, amount: int) -> None:
|
||||||
|
"""
|
||||||
|
Очистить указанное количество сообщений в канале.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param amount: Количество сообщений для удаления.
|
||||||
|
"""
|
||||||
|
if amount <= 0:
|
||||||
|
await ctx.send("Количество должно быть положительным числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
deleted = await ctx.channel.purge(limit=amount + 1)
|
||||||
|
await ctx.send(
|
||||||
|
f"Удалено сообщений: {len(deleted) - 1}",
|
||||||
|
delete_after=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
"""
|
||||||
|
Зарегистрировать cog в боте.
|
||||||
|
|
||||||
|
:param bot: Экземпляр бота.
|
||||||
|
"""
|
||||||
|
await bot.add_cog(Moderation(bot))
|
||||||
94
bot/cogs/reminders.py
Normal file
94
bot/cogs/reminders.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from ..storage import storage, Reminder
|
||||||
|
from .moderation import is_admin
|
||||||
|
|
||||||
|
|
||||||
|
class Reminders(commands.Cog):
|
||||||
|
"""
|
||||||
|
Cog для управления напоминаниями: add, list, remove.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@is_admin()
|
||||||
|
async def reminder(self, ctx: commands.Context) -> None:
|
||||||
|
"""
|
||||||
|
Группа команд напоминаний.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send(
|
||||||
|
"Используйте `!reminder add <минуты> <текст>`, "
|
||||||
|
"`!reminder list` или `!reminder remove <номер>`"
|
||||||
|
)
|
||||||
|
|
||||||
|
@reminder.command(name="add")
|
||||||
|
async def reminder_add(
|
||||||
|
self, ctx: commands.Context, minutes: int, *, text: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Добавить новое напоминание.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param minutes: Через сколько минут сработает напоминание.
|
||||||
|
:param text: Текст напоминания.
|
||||||
|
"""
|
||||||
|
if minutes <= 0:
|
||||||
|
await ctx.send("Время должно быть положительным числом минут.")
|
||||||
|
return
|
||||||
|
|
||||||
|
remind_time: datetime = datetime.now() + timedelta(minutes=minutes)
|
||||||
|
storage.reminders.append(
|
||||||
|
Reminder(
|
||||||
|
time=remind_time.timestamp(),
|
||||||
|
channel_id=ctx.channel.id, # type: ignore[assignment]
|
||||||
|
user_mention=ctx.author.mention, # type: ignore[union-attr]
|
||||||
|
text=text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
storage.save_reminders()
|
||||||
|
await ctx.send(f"Напоминание добавлено через {minutes} минут: {text}")
|
||||||
|
|
||||||
|
@reminder.command(name="list")
|
||||||
|
async def reminder_list(self, ctx: commands.Context) -> None:
|
||||||
|
"""
|
||||||
|
Показать список активных напоминаний.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
"""
|
||||||
|
if not storage.reminders:
|
||||||
|
await ctx.send("Активных напоминаний нет.")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg: str = "Активные напоминания:\n"
|
||||||
|
for i, rem in enumerate(storage.reminders, 1):
|
||||||
|
t_str: str = datetime.fromtimestamp(rem.time).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
msg += f"{i}. До {t_str} — {rem.text} (от {rem.user_mention})\n"
|
||||||
|
await ctx.send(msg)
|
||||||
|
|
||||||
|
@reminder.command(name="remove")
|
||||||
|
async def reminder_remove(self, ctx: commands.Context, number: int) -> None:
|
||||||
|
"""
|
||||||
|
Удалить напоминание по номеру.
|
||||||
|
|
||||||
|
:param ctx: Контекст команды.
|
||||||
|
:param number: Порядковый номер напоминания.
|
||||||
|
"""
|
||||||
|
if number <= 0 or number > len(storage.reminders):
|
||||||
|
await ctx.send("Неверный номер напоминания.")
|
||||||
|
return
|
||||||
|
|
||||||
|
removed: Reminder = storage.reminders.pop(number - 1)
|
||||||
|
storage.save_reminders()
|
||||||
|
await ctx.send(f"Удалено напоминание: {removed.text}")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
await bot.add_cog(Reminders(bot))
|
||||||
27
bot/help.py
Normal file
27
bot/help.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from discord.ext.commands import HelpCommand
|
||||||
|
|
||||||
|
from configs import settings
|
||||||
|
|
||||||
|
|
||||||
|
class MyHelpCommand(HelpCommand):
|
||||||
|
"""Кастомная команда help с текстовой справкой."""
|
||||||
|
async def send_bot_help(self, mapping, prefix: str = settings.PREFIX) -> None:
|
||||||
|
channel = self.get_destination()
|
||||||
|
help_text: str = (
|
||||||
|
"**Доступные команды:**\n"
|
||||||
|
f"`{prefix}help` — показать это сообщение\n"
|
||||||
|
f"`{prefix}rules` — показать правила сервера\n"
|
||||||
|
f"`{prefix}reminder add <минуты> <текст>` — добавить напоминание\n"
|
||||||
|
f"`{prefix}reminder list` — список напоминаний\n"
|
||||||
|
f"`{prefix}reminder remove <номер>` — удалить напоминание\n"
|
||||||
|
f"`{prefix}kick @пользователь [причина]` — исключить\n"
|
||||||
|
f"`{prefix}ban @пользователь [причина]` — забанить\n"
|
||||||
|
f"`{prefix}unban имя#дискриминатор` — разбанить\n"
|
||||||
|
f"`{prefix}mute @пользователь [причина]` — заглушить\n"
|
||||||
|
f"`{prefix}unmute @пользователь` — снять заглушение\n"
|
||||||
|
f"`{prefix}warn @пользователь [причина]` — предупреждение\n"
|
||||||
|
f"`{prefix}warnings @пользователь` — список предупреждений\n"
|
||||||
|
f"`{prefix}clear <кол-во>` — очистить чат\n"
|
||||||
|
f"`{prefix}blacklist_show/add/remove` — чёрный список\n"
|
||||||
|
)
|
||||||
|
await channel.send(help_text)
|
||||||
141
bot/storage.py
Normal file
141
bot/storage.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from configs import settings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Reminder:
|
||||||
|
"""
|
||||||
|
Модель напоминания.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
time: Unix-время, когда нужно отправить напоминание.
|
||||||
|
channel_id: ID текстового канала.
|
||||||
|
user_mention: mention пользователя.
|
||||||
|
text: Текст напоминания.
|
||||||
|
"""
|
||||||
|
time: float
|
||||||
|
channel_id: int
|
||||||
|
user_mention: str
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Storage:
|
||||||
|
"""
|
||||||
|
Класс для работы с локальными JSON‑хранилищами:
|
||||||
|
предупреждения, напоминания, чёрный список.
|
||||||
|
"""
|
||||||
|
|
||||||
|
warnings_file: Path = settings.WARNINGS_FILE
|
||||||
|
reminders_file: Path = settings.REMINDERS_FILE
|
||||||
|
blacklist_file: Path = settings.BLACKLIST_FILE
|
||||||
|
|
||||||
|
reminders: List[Reminder] = field(default_factory=list)
|
||||||
|
user_warnings: Dict[str, List[Dict[str, Any]]] = field(default_factory=dict)
|
||||||
|
blacklist: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def load_all(self) -> None:
|
||||||
|
"""
|
||||||
|
Загрузить все данные из файлов.
|
||||||
|
"""
|
||||||
|
self.load_warnings()
|
||||||
|
self.load_reminders()
|
||||||
|
self.load_blacklist()
|
||||||
|
|
||||||
|
def load_warnings(self) -> None:
|
||||||
|
"""
|
||||||
|
Загрузить предупреждения пользователей из JSON‑файла.
|
||||||
|
"""
|
||||||
|
if self.warnings_file.is_file():
|
||||||
|
try:
|
||||||
|
data = json.loads(self.warnings_file.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(data, dict):
|
||||||
|
self.user_warnings = data
|
||||||
|
else:
|
||||||
|
self.user_warnings = {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.user_warnings = {}
|
||||||
|
|
||||||
|
def save_warnings(self) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить предупреждения пользователей в JSON‑файл.
|
||||||
|
"""
|
||||||
|
self.warnings_file.write_text(
|
||||||
|
json.dumps(self.user_warnings, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_reminders(self) -> None:
|
||||||
|
"""
|
||||||
|
Загрузить напоминания из JSON‑файла.
|
||||||
|
"""
|
||||||
|
self.reminders.clear()
|
||||||
|
if self.reminders_file.is_file():
|
||||||
|
try:
|
||||||
|
data = json.loads(self.reminders_file.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(data, list):
|
||||||
|
for item in data:
|
||||||
|
try:
|
||||||
|
self.reminders.append(
|
||||||
|
Reminder(
|
||||||
|
time=float(item["time"]),
|
||||||
|
channel_id=int(item["channel_id"]),
|
||||||
|
user_mention=str(item["user_mention"]),
|
||||||
|
text=str(item["text"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.reminders = []
|
||||||
|
|
||||||
|
def save_reminders(self) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить напоминания в JSON‑файл.
|
||||||
|
"""
|
||||||
|
data: List[Dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"time": r.time,
|
||||||
|
"channel_id": r.channel_id,
|
||||||
|
"user_mention": r.user_mention,
|
||||||
|
"text": r.text,
|
||||||
|
}
|
||||||
|
for r in self.reminders
|
||||||
|
]
|
||||||
|
self.reminders_file.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_blacklist(self) -> None:
|
||||||
|
"""
|
||||||
|
Загрузить чёрный список слов из JSON‑файла.
|
||||||
|
"""
|
||||||
|
self.blacklist.clear()
|
||||||
|
if self.blacklist_file.is_file():
|
||||||
|
try:
|
||||||
|
data = json.loads(self.blacklist_file.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(data, list):
|
||||||
|
self.blacklist = [str(w).lower() for w in data]
|
||||||
|
else:
|
||||||
|
self.blacklist = []
|
||||||
|
except Exception:
|
||||||
|
self.blacklist = []
|
||||||
|
|
||||||
|
def save_blacklist(self) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить чёрный список слов в JSON‑файл.
|
||||||
|
"""
|
||||||
|
self.blacklist_file.write_text(
|
||||||
|
json.dumps(self.blacklist, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
storage: Storage = Storage()
|
||||||
1
configs/__init__.py
Normal file
1
configs/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .config import *
|
||||||
65
configs/config.py
Normal file
65
configs/config.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# config.py
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import field_validator
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class _Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
case_sensitive=False,
|
||||||
|
validate_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
BOT_TOKEN: Optional[str] = None
|
||||||
|
PREFIX: Optional[str] = '!'
|
||||||
|
|
||||||
|
WELCOME_CHANNEL_ID: int = 0
|
||||||
|
ADMIN_ROLE_NAME: str = "Администратор"
|
||||||
|
|
||||||
|
WARNINGS_FILE: Path = Path("warnings.json")
|
||||||
|
REMINDERS_FILE: Path = Path("reminders.json")
|
||||||
|
BLACKLIST_FILE: Path = Path("blacklist.json")
|
||||||
|
|
||||||
|
LOG_FILE_NAME: Path = Path("bot.log")
|
||||||
|
LOG_LEVEL: str = "info"
|
||||||
|
|
||||||
|
@field_validator("BOT_TOKEN")
|
||||||
|
def validate_bot_token(cls, v: Optional[str]) -> str:
|
||||||
|
if not v:
|
||||||
|
raise ValueError("Не задан BOT_TOKEN (проверь .env или переменные окружения)")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("WELCOME_CHANNEL_ID")
|
||||||
|
def validate_channel_id(cls, v: int) -> int:
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("WELCOME_CHANNEL_ID должен быть положительным ID канала")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("ADMIN_ROLE_NAME")
|
||||||
|
def validate_admin_role_name(cls, v: str) -> str:
|
||||||
|
if not v.strip():
|
||||||
|
raise ValueError("ADMIN_ROLE_NAME не может быть пустым")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("LOG_LEVEL")
|
||||||
|
def validate_log_level(cls, v: str) -> str:
|
||||||
|
allowed: set[str] = {"debug", "info", "warning", "error", "critical"}
|
||||||
|
if v.lower() not in allowed:
|
||||||
|
raise ValueError(f"LOG_LEVEL должен быть одним из: {', '.join(allowed)}")
|
||||||
|
return v.lower()
|
||||||
|
|
||||||
|
@field_validator("WARNINGS_FILE", "REMINDERS_FILE", "BLACKLIST_FILE", "LOG_FILE_NAME", mode="before")
|
||||||
|
def validate_paths(cls, v) -> Optional[Path]:
|
||||||
|
return Path(v) if isinstance(v, str) else v
|
||||||
|
|
||||||
|
|
||||||
|
# Создаём экземпляр класса настроек
|
||||||
|
settings = _Settings()
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ("settings",)
|
||||||
18
main.py
Normal file
18
main.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from asyncio import run
|
||||||
|
|
||||||
|
from bot import discbot
|
||||||
|
from configs import settings
|
||||||
|
from middleware import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""
|
||||||
|
Точка входа для асинхронного запуска бота.
|
||||||
|
"""
|
||||||
|
logger.setup()
|
||||||
|
await discbot.start(settings.BOT_TOKEN)
|
||||||
|
await discbot.start_bot()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run(main())
|
||||||
2
middleware/__init__.py
Normal file
2
middleware/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .loggers import *
|
||||||
|
from .validators import *
|
||||||
1
middleware/loggers/__init__.py
Normal file
1
middleware/loggers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .logs import *
|
||||||
194
middleware/loggers/logs.py
Normal file
194
middleware/loggers/logs.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from functools import wraps
|
||||||
|
from sys import stderr as console
|
||||||
|
from inspect import iscoroutinefunction
|
||||||
|
from typing import Any, Callable, Optional, TypeVar, cast, Final
|
||||||
|
|
||||||
|
from loguru import logger as logs
|
||||||
|
|
||||||
|
from configs.config import settings # экземпляр настроек
|
||||||
|
|
||||||
|
__all__ = ("logger", "log")
|
||||||
|
|
||||||
|
F = TypeVar("F", bound=Callable[..., Any])
|
||||||
|
|
||||||
|
|
||||||
|
class _Logger:
|
||||||
|
"""
|
||||||
|
Обёртка над loguru с:
|
||||||
|
- единым форматом сообщений;
|
||||||
|
- выводом в консоль;
|
||||||
|
- файлами в ./logs:
|
||||||
|
- logs/bot.log — все уровни (DEBUG+)
|
||||||
|
- logs/debug.log — только DEBUG
|
||||||
|
- logs/info.log — только INFO
|
||||||
|
- logs/warning.log — только WARNING
|
||||||
|
- logs/error.log — только ERROR
|
||||||
|
- logs/critical.log — только CRITICAL
|
||||||
|
"""
|
||||||
|
|
||||||
|
_log_format: Final[str] = (
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <red>|</red> "
|
||||||
|
"<blue>{extra[system]}-{extra[log_type]}</blue> <red>|</red> "
|
||||||
|
"{extra[user]} <red>|</red> <level>{message}</level>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, system_name: str = "DISCORD_BOT") -> None:
|
||||||
|
self.system_name: str = system_name
|
||||||
|
self._setup_done: bool = False
|
||||||
|
|
||||||
|
def setup(self, start: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Настроить loguru: консоль + файлы в каталоге logs/.
|
||||||
|
Вызывать один раз при старте приложения.
|
||||||
|
"""
|
||||||
|
if self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
logs.remove()
|
||||||
|
|
||||||
|
# Директория для логов
|
||||||
|
log_dir: Path = Path("logs")
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Консольный вывод
|
||||||
|
logs.add(
|
||||||
|
sink=console,
|
||||||
|
format=self._log_format,
|
||||||
|
colorize=True,
|
||||||
|
level=settings.LOG_LEVEL.upper(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Общий лог (все уровни)
|
||||||
|
logs.add(
|
||||||
|
sink=log_dir / "bot.log",
|
||||||
|
rotation="100 MB",
|
||||||
|
retention="7 days",
|
||||||
|
format=self._log_format,
|
||||||
|
level="DEBUG",
|
||||||
|
enqueue=True,
|
||||||
|
backtrace=True,
|
||||||
|
diagnose=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отдельные файлы по уровням
|
||||||
|
for level_name in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
||||||
|
logs.add(
|
||||||
|
sink=log_dir / f"{level_name.lower()}.log",
|
||||||
|
rotation="10 MB",
|
||||||
|
retention="7 days",
|
||||||
|
format=self._log_format,
|
||||||
|
level=level_name,
|
||||||
|
filter=lambda rec, lvl=level_name: rec["level"].name == lvl,
|
||||||
|
enqueue=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._setup_done = True
|
||||||
|
|
||||||
|
if start:
|
||||||
|
self.log_entry(
|
||||||
|
level="INFO",
|
||||||
|
text="Запуск Discord‑бота...",
|
||||||
|
log_type="START",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_user(user: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Вернуть строку пользователя для логов.
|
||||||
|
"""
|
||||||
|
return user or "@System"
|
||||||
|
|
||||||
|
def log_entry(
|
||||||
|
self,
|
||||||
|
level: str,
|
||||||
|
text: str,
|
||||||
|
log_type: str = "BOT",
|
||||||
|
user: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Записать строку лога.
|
||||||
|
|
||||||
|
:param level: Уровень (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
||||||
|
:param text: Текст сообщения.
|
||||||
|
:param log_type: Категория/подсистема (например, SYSTEM, COGS, DB).
|
||||||
|
:param user: Опционально — строка пользователя.
|
||||||
|
"""
|
||||||
|
actual_user: str = self.format_user(user)
|
||||||
|
logs.bind(
|
||||||
|
system=self.system_name,
|
||||||
|
user=actual_user,
|
||||||
|
log_type=log_type,
|
||||||
|
).log(level, text)
|
||||||
|
|
||||||
|
def log(
|
||||||
|
self,
|
||||||
|
level: str = "INFO",
|
||||||
|
log_type: str = "",
|
||||||
|
text: Optional[str] = None,
|
||||||
|
) -> Callable[[F], F]:
|
||||||
|
"""
|
||||||
|
Декоратор для логирования вызовов функций/корутин.
|
||||||
|
|
||||||
|
:param level: Уровень сообщения.
|
||||||
|
:param log_type: Категория (например, HANDLER, TASK).
|
||||||
|
:param text: Кастомный текст (по умолчанию 'Вызов <имя функции>').
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: F) -> F:
|
||||||
|
is_coroutine: bool = iscoroutinefunction(func)
|
||||||
|
action_text: str = text or f"Вызов {func.__name__}"
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
self.log_entry(level=level, text=f"[START] {action_text}", log_type=log_type)
|
||||||
|
try:
|
||||||
|
result: Any = func(*args, **kwargs)
|
||||||
|
self.log_entry(level=level, text=f"[SUCCESS] {action_text}", log_type=log_type)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self.log_entry(
|
||||||
|
level="ERROR",
|
||||||
|
text=f"[ERROR] {action_text} | Exception: {e!r}",
|
||||||
|
log_type=log_type,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
self.log_entry(level=level, text=f"[START] {action_text}", log_type=log_type)
|
||||||
|
try:
|
||||||
|
result: Any = await func(*args, **kwargs)
|
||||||
|
self.log_entry(level=level, text=f"[SUCCESS] {action_text}", log_type=log_type)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self.log_entry(
|
||||||
|
level="ERROR",
|
||||||
|
text=f"[ERROR] {action_text} | Exception: {e!r}",
|
||||||
|
log_type=log_type,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return cast(F, async_wrapper if is_coroutine else sync_wrapper)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def debug(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None:
|
||||||
|
self.log_entry(level="DEBUG", text=text, log_type=log_type, user=user)
|
||||||
|
|
||||||
|
def info(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None:
|
||||||
|
self.log_entry(level="INFO", text=text, log_type=log_type, user=user)
|
||||||
|
|
||||||
|
def warning(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None:
|
||||||
|
self.log_entry(level="WARNING", text=text, log_type=log_type, user=user)
|
||||||
|
|
||||||
|
def error(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None:
|
||||||
|
self.log_entry(level="ERROR", text=text, log_type=log_type, user=user)
|
||||||
|
|
||||||
|
def critical(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None:
|
||||||
|
self.log_entry(level="CRITICAL", text=text, log_type=log_type, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр
|
||||||
|
logger: _Logger = _Logger(system_name="DISCORD_BOT")
|
||||||
|
log = logger.log
|
||||||
2
middleware/validators/__init__.py
Normal file
2
middleware/validators/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .email_vld import *
|
||||||
|
from .url_vld import *
|
||||||
24
middleware/validators/email_vld.py
Normal file
24
middleware/validators/email_vld.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from email_validator import validate_email, EmailNotValidError, ValidatedEmail
|
||||||
|
|
||||||
|
# Настройка экспорта из этого модуля
|
||||||
|
__all__ = ("valid_email",)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_email(e_mail: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Валидация почты через библиотеку.
|
||||||
|
|
||||||
|
:param e_mail: Получаемая почта.
|
||||||
|
:return: Нормализированная почта.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Провека почты на валидность
|
||||||
|
email: ValidatedEmail = validate_email(e_mail)
|
||||||
|
|
||||||
|
except EmailNotValidError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Возвращение строки с нормализированной почтой
|
||||||
|
return email.normalized
|
||||||
53
middleware/validators/url_vld.py
Normal file
53
middleware/validators/url_vld.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from re import Pattern, compile
|
||||||
|
|
||||||
|
from ..loggers import logger
|
||||||
|
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("valid_url", "url_to_text",)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_url(url: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли строка валидной ссылкой (URL).
|
||||||
|
|
||||||
|
:param url: Строка для проверки.
|
||||||
|
:return: True, если строка является валидным URL, иначе False.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url_pattern: Pattern[str] = compile(
|
||||||
|
r'^(https?://)?' # Протокол (http или https, необязателен)
|
||||||
|
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}' # Домен
|
||||||
|
r'(:\d+)?' # Порт (необязателен)
|
||||||
|
r'(/[-a-zA-Z0-9@:%_+.~#?&/=]*)?$' # Путь, параметры и фрагменты
|
||||||
|
)
|
||||||
|
return bool(url_pattern.match(url))
|
||||||
|
|
||||||
|
except ValueError as error:
|
||||||
|
# Перебрасываем ошибку выше для дальнейшей обработки или уведомления
|
||||||
|
logger.error(text=f'Ошибка валидации ссылки: {error}')
|
||||||
|
raise error
|
||||||
|
|
||||||
|
|
||||||
|
def url_to_text(text: str, url: str) -> str:
|
||||||
|
"""
|
||||||
|
Преобразует текст в HTML ссылку с указанным URL.
|
||||||
|
|
||||||
|
Эта функция генерирует HTML-ссылку с переданным текстом и URL, используя тег `<а>`, и делает ссылку жирной.
|
||||||
|
|
||||||
|
:param text: Текст, который будет отображаться для ссылки.
|
||||||
|
:param url: URL, который будет привязан к тексту.
|
||||||
|
:return: Строка с HTML кодом для ссылки, если URL валиден.
|
||||||
|
:raises ValueError: Если URL невалиден.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not valid_url(url): # Проверяем, является ли URL валидным
|
||||||
|
raise ValueError(f"Переданный URL '{url}' невалиден.")
|
||||||
|
|
||||||
|
# Генерация HTML-ссылки
|
||||||
|
return f'<b><a href="{url}">{text}</a></b>'
|
||||||
|
|
||||||
|
except ValueError as error:
|
||||||
|
# Перебрасываем ошибку выше для дальнейшей обработки или уведомления
|
||||||
|
logger.error(text=f'Ошибка валидации ссылки в текст: {error}')
|
||||||
|
raise error
|
||||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[project]
|
||||||
|
name = "notfatekursach"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "None"
|
||||||
|
authors = [
|
||||||
|
{name = "NotFate"}
|
||||||
|
]
|
||||||
|
license = {text = "MIT"}
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11,<4.0"
|
||||||
|
dependencies = [
|
||||||
|
"discord (>=2.3.2,<3.0.0)",
|
||||||
|
"loguru (>=0.7.3,<0.8.0)",
|
||||||
|
"email-validator (>=2.3.0,<3.0.0)",
|
||||||
|
"pydantic (>=2.12.5,<3.0.0)",
|
||||||
|
"pydantic-settings (>=2.12.0,<3.0.0)"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
Reference in New Issue
Block a user