типо да
This commit is contained in:
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Исключить скрытые системные каталоги, но не всё подряд
|
||||||
|
.git/
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Виртуальные окружения и Python-кэш
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# IDE-файлы
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Тесты и документация
|
||||||
|
tests/
|
||||||
|
test/
|
||||||
|
docs/
|
||||||
|
examples/
|
||||||
|
|
||||||
|
# Логи и артефакты сборки
|
||||||
|
*.log
|
||||||
|
*.logs
|
||||||
|
*.log.*
|
||||||
|
*.logs.*
|
||||||
|
Logs/
|
||||||
|
Log/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Примеры и шаблоны
|
||||||
|
.env
|
||||||
|
env
|
||||||
92
.env_example
Normal file
92
.env_example
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Токены бота
|
||||||
|
BOT_TOKEN=your_bot_token_here
|
||||||
|
BOT_DEBUG_TOKEN=your_debug_bot_token_here
|
||||||
|
|
||||||
|
# Режим отладки
|
||||||
|
DEBUG=False
|
||||||
|
|
||||||
|
|
||||||
|
# Владелец бота
|
||||||
|
OWNER=@verdise
|
||||||
|
|
||||||
|
# Основные настройки
|
||||||
|
PARSE_MODE=HTML
|
||||||
|
ENCOD=utf-8
|
||||||
|
TIME_FORMAT=%Y-%m-%d %H:%M:%S
|
||||||
|
PREFIX=/!.&?
|
||||||
|
BOT_LANGUAGE=Aiogram3
|
||||||
|
|
||||||
|
|
||||||
|
# Настройки сообщений
|
||||||
|
DISABLE_NOTIFICATION=False
|
||||||
|
PROTECT_CONTENT=False
|
||||||
|
ALLOW_SENDING_WITHOUT_REPLY=True
|
||||||
|
LINK_PREVIEW_IS_DISABLED=False
|
||||||
|
LINK_PREVIEW_PREFER_SMALL_MEDIA=False
|
||||||
|
LINK_PREVIEW_PREFER_LARGE_MEDIA=True
|
||||||
|
LINK_PREVIEW_SHOW_ABOVE_TEXT=False
|
||||||
|
SHOW_CAPTION_ABOVE_MEDIA=False
|
||||||
|
|
||||||
|
# Разрешения
|
||||||
|
BOT_EDIT=False
|
||||||
|
START_INFO_CONSOLE=True
|
||||||
|
START_INFO_TO_FILE=True
|
||||||
|
|
||||||
|
# Логирование
|
||||||
|
LOG_CONSOLE=True
|
||||||
|
LOG_FILE=True
|
||||||
|
LOG_DIR=Logs
|
||||||
|
LOG_FILE_INFO=bot_info.log
|
||||||
|
|
||||||
|
|
||||||
|
# Вебхук
|
||||||
|
WEBHOOK=False
|
||||||
|
|
||||||
|
# API ключи
|
||||||
|
API_KEY=your_api_key
|
||||||
|
WEB_API_KEY=your_web_api_key
|
||||||
|
WEATHER_API_KEY=your_weather_api_key
|
||||||
|
|
||||||
|
# Telegram API ID и HASH
|
||||||
|
TG_API_UID=123456
|
||||||
|
TG_API_HASH=your_tg_api_hash
|
||||||
|
|
||||||
|
|
||||||
|
# Важные ID
|
||||||
|
ADMIN_ID=123456789
|
||||||
|
MODERATOR_ID=987654321
|
||||||
|
IMPORTANT_ID=1122334455
|
||||||
|
IMPORTANT_GROUP_ID=-1001122334455
|
||||||
|
IMPORTANT_CHANNEL_ID=-1009988776655
|
||||||
|
|
||||||
|
|
||||||
|
# Настройки бота
|
||||||
|
PROJECT_NAME=PRIMO
|
||||||
|
BOT_NAME=Первозданная Жемчужина
|
||||||
|
BOT_DESCRIPTION=Ваш помощник в удивительные миры! Prod. by:『@verdise』
|
||||||
|
BOT_SHORT_DESCRIPTION=Тех.поддержка: @verdise
|
||||||
|
|
||||||
|
# Настройки ролевого проекта
|
||||||
|
RP_NAME: str = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
|
||||||
|
|
||||||
|
|
||||||
|
# Права администратора
|
||||||
|
ANONYMOUS=False
|
||||||
|
MANAGE_CHAT=True
|
||||||
|
CHANGE_INFO=True
|
||||||
|
PROMOTE_MEMBERS=True
|
||||||
|
RESTRICT_MEMBERS=True
|
||||||
|
POST_MESSAGE=True
|
||||||
|
MANAGE_TOPICS=True
|
||||||
|
INVITE_USER=True
|
||||||
|
DELETE_MESSAGES=True
|
||||||
|
MANAGE_VIDEO_CHATS=True
|
||||||
|
EDIT_MESSAGES=True
|
||||||
|
PIN_MESSAGE=True
|
||||||
|
POST_STORIES=True
|
||||||
|
EDIT_STORIES=True
|
||||||
|
DELETE_STORIES=True
|
||||||
|
|
||||||
|
|
||||||
|
# Поддержка
|
||||||
|
SUPPORT_CHAT_ID=0
|
||||||
97
.gitattributes
vendored
Normal file
97
.gitattributes
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Git LFS: большие бинарные файлы, модели, архивы
|
||||||
|
# =============================================================================
|
||||||
|
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.arrow filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ftz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.h5 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.model filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ot filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.parquet filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pickle filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pkl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pth filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.rar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
||||||
|
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tflite filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.wasm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Автоопределение текста, окончания строк
|
||||||
|
# =============================================================================
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Текстовые файлы (Python, конфиги, документы)
|
||||||
|
# =============================================================================
|
||||||
|
*.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
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Изображения
|
||||||
|
# =============================================================================
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.bmp binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.svg text
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Шрифты
|
||||||
|
# =============================================================================
|
||||||
|
*.eot binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.otf 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
|
||||||
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# .gitignore: Игнорируемые файлы для Python проектов
|
||||||
|
# Подробнее: https://github.com/github/gitignore/blob/main/Python.gitignore
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Виртуальные окружения и настройки
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
|
env
|
||||||
|
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/
|
||||||
|
|
||||||
|
### Логи и БД ###
|
||||||
|
*.log
|
||||||
|
*.logs
|
||||||
|
*.log.*
|
||||||
|
*.logs.*
|
||||||
|
log/
|
||||||
|
logs/
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
|
### IDE ###
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.sublime-*
|
||||||
|
|
||||||
|
### OS ###
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
### Тестирование ###
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
test/
|
||||||
|
tests/
|
||||||
|
Test/
|
||||||
|
Tests/
|
||||||
|
count.py
|
||||||
10
.idea/PrimoAranarBot.iml
generated
Normal file
10
.idea/PrimoAranarBot.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (PrimoAranarBot)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/YashaSystemBot.iml" filepath="$PROJECT_DIR$/.idea/YashaSystemBot.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
258
.idea/workspace.xml
generated
Normal file
258
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="2643be13-7905-4315-bb25-9c65e899084f" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.env_example" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/PrimoAranarBot.iml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/assets/default.jpg" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/assets/start.jpg" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/core/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/core/bots.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/core/webhook.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/callback.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/chat_rights.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/chat_type.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/message_content.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/filters/subscrided.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/admins/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/admins/settings_cmd.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/active.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/anketa_cmd.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/new_cmd.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/start_cmd.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/default.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/reply_msg.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/secret1.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/secret2.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/decision.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/reply/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/error_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/logging_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/msg_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/spam_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/subscription_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/middlewares/time_mdw.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/states/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/states/anketa_states.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/states/new_states.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/templates/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/templates/message_callback.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/argument.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/interesting_facts.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/pagination.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/random_lists.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/type_message.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/bot/utils/usernames.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/configs/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/configs/cmd_list.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/configs/config.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/configs/roles.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/database/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/database/database.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/en/LC_MESSAGES/bot.mo" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/en/LC_MESSAGES/bot.po" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/messages.pot" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/ru/LC_MESSAGES/bot.mo" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/ru/LC_MESSAGES/bot.po" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/uk/LC_MESSAGES/bot.mo" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/locales/uk/LC_MESSAGES/bot.po" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/loggers/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/loggers/logs.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/validators/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/validators/email_vld.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/middleware/validators/url_vld.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="Python Script" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="KubernetesApiPersistence">{}</component>
|
||||||
|
<component name="KubernetesApiProvider">{
|
||||||
|
"isMigrated": true
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 6
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="30zwIGAUUSITtvba70bSTcC47qk" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"Python.main.executor": "Run",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"git-widget-placeholder": "master",
|
||||||
|
"last_opened_file_path": "C:/Users/admin/Documents/Projects/Python/PrimoAranarBot",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\middleware" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\locales\uk\LC_MESSAGES" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\locales\ru\LC_MESSAGES" />
|
||||||
|
</key>
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot\bot\handlers\secret2" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot\bot\handlers" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\bot\core" />
|
||||||
|
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\middleware\loggers" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="main" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
|
<module name="PrimoAranarBot" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="SDK_NAME" value="Python 3.13 (PrimoAranarBot)" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Python.main" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-PY-251.26927.90" />
|
||||||
|
<option value="bundled-python-sdk-41e8cd69c857-64d779b69b7a-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26927.90" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="2643be13-7905-4315-bb25-9c65e899084f" name="Changes" comment="" />
|
||||||
|
<created>1754643627985</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1754643627985</updated>
|
||||||
|
<workItem from="1754643629040" duration="10355000" />
|
||||||
|
<workItem from="1754662667867" duration="7333000" />
|
||||||
|
<workItem from="1754744202078" duration="9537000" />
|
||||||
|
<workItem from="1754831073792" duration="2983000" />
|
||||||
|
<workItem from="1754834942318" duration="73000" />
|
||||||
|
<workItem from="1755110743443" duration="663000" />
|
||||||
|
<workItem from="1755117853241" duration="600000" />
|
||||||
|
<workItem from="1755158975813" duration="2965000" />
|
||||||
|
<workItem from="1755338132928" duration="757000" />
|
||||||
|
<workItem from="1756535467015" duration="566000" />
|
||||||
|
<workItem from="1756536041631" duration="326000" />
|
||||||
|
<workItem from="1756536379874" duration="386000" />
|
||||||
|
<workItem from="1756536789625" duration="4088000" />
|
||||||
|
<workItem from="1756649477917" duration="228000" />
|
||||||
|
<workItem from="1756649731073" duration="9987000" />
|
||||||
|
<workItem from="1757043734454" duration="3772000" />
|
||||||
|
<workItem from="1757061298817" duration="13000" />
|
||||||
|
<workItem from="1757131608578" duration="1256000" />
|
||||||
|
<workItem from="1757133500304" duration="12844000" />
|
||||||
|
<workItem from="1757256800838" duration="196000" />
|
||||||
|
<workItem from="1757257115686" duration="2310000" />
|
||||||
|
<workItem from="1757259792275" duration="5293000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="XDebuggerManager">
|
||||||
|
<breakpoint-manager>
|
||||||
|
<breakpoints>
|
||||||
|
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
||||||
|
<url>file://$PROJECT_DIR$/bot/handlers/__init__.py</url>
|
||||||
|
<option name="timeStamp" value="1" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
||||||
|
<url>file://$PROJECT_DIR$/bot/handlers/secret/secret1.py</url>
|
||||||
|
<option name="timeStamp" value="3" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
||||||
|
<url>file://$PROJECT_DIR$/bot/handlers/messages/__init__.py</url>
|
||||||
|
<option name="timeStamp" value="4" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
||||||
|
<url>file://$PROJECT_DIR$/bot/handlers/messages/default.py</url>
|
||||||
|
<line>59</line>
|
||||||
|
<option name="timeStamp" value="9" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
||||||
|
<url>file://$PROJECT_DIR$/bot/core/__init__.py</url>
|
||||||
|
<option name="timeStamp" value="10" />
|
||||||
|
</line-breakpoint>
|
||||||
|
</breakpoints>
|
||||||
|
</breakpoint-manager>
|
||||||
|
</component>
|
||||||
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
|
<SUITE FILE_PATH="coverage/YashaSystemBot$main.coverage" NAME="main Coverage Results" MODIFIED="1754831895813" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
|
<SUITE FILE_PATH="coverage/PrimoAranarBot$main.coverage" NAME="main Coverage Results" MODIFIED="1755353593880" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Используем официальный образ Python с подходящей версией
|
||||||
|
FROM python:3.12-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] [Verum]
|
||||||
|
|
||||||
|
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/default.jpg
Normal file
BIN
assets/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 554 KiB |
BIN
assets/start.jpg
Normal file
BIN
assets/start.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
3
bot/__init__.py
Normal file
3
bot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .core import *
|
||||||
|
from .handlers import *
|
||||||
|
from .middlewares import *
|
||||||
2
bot/core/__init__.py
Normal file
2
bot/core/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .bots import *
|
||||||
|
from .webhook import *
|
||||||
203
bot/core/bots.py
Normal file
203
bot/core/bots.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from asyncio import sleep
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.exceptions import TelegramRetryAfter
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
from aiogram.types import (
|
||||||
|
User,
|
||||||
|
ChatAdministratorRights,
|
||||||
|
BotDescription,
|
||||||
|
BotShortDescription,
|
||||||
|
)
|
||||||
|
from aiogram.utils.i18n import I18n, SimpleI18nMiddleware
|
||||||
|
|
||||||
|
from configs.config import BotSettings, BotEdit, Webhook, Permission
|
||||||
|
from middleware.loggers import log
|
||||||
|
|
||||||
|
__all__ = ("dp", "bot", "BotInfo", "i18n")
|
||||||
|
|
||||||
|
# FSM-хранилище и диспетчер
|
||||||
|
storage: MemoryStorage = MemoryStorage()
|
||||||
|
dp: Dispatcher = Dispatcher(storage=storage)
|
||||||
|
dp["is_active"]: bool = True
|
||||||
|
|
||||||
|
# Локализация
|
||||||
|
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
|
||||||
|
i18n_middleware: SimpleI18nMiddleware = SimpleI18nMiddleware(i18n=i18n)
|
||||||
|
i18n_middleware.setup(dp)
|
||||||
|
|
||||||
|
# Экземпляр бота
|
||||||
|
bot: Bot = Bot(
|
||||||
|
token=BotSettings.BOT_TOKEN,
|
||||||
|
default=DefaultBotProperties(
|
||||||
|
parse_mode=BotSettings.PARSE_MODE,
|
||||||
|
disable_notification=BotSettings.DISABLE_NOTIFICATION,
|
||||||
|
protect_content=BotSettings.PROTECT_CONTENT,
|
||||||
|
allow_sending_without_reply=BotSettings.ALLOW_SENDING_WITHOUT_REPLY,
|
||||||
|
link_preview_is_disabled=BotSettings.LINK_PREVIEW_IS_DISABLED,
|
||||||
|
link_preview_prefer_small_media=BotSettings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
|
||||||
|
link_preview_prefer_large_media=BotSettings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
|
||||||
|
link_preview_show_above_text=BotSettings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
|
||||||
|
show_caption_above_media=BotSettings.SHOW_CAPTION_ABOVE_MEDIA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BotInfo:
|
||||||
|
"""Класс для хранения и настройки информации о боте."""
|
||||||
|
|
||||||
|
id: int | None = None
|
||||||
|
url: str | None = None
|
||||||
|
first_name: str | None = None
|
||||||
|
last_name: str | None = None
|
||||||
|
username: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
short_description: str | None = None
|
||||||
|
language_code: str = BotSettings.BOT_LANGUAGE
|
||||||
|
prefix: str = BotSettings.PREFIX
|
||||||
|
bot_owner: str = BotSettings.OWNER
|
||||||
|
added_to_attachment_menu: bool = False
|
||||||
|
supports_inline_queries: bool = False
|
||||||
|
can_connect_to_business: bool = False
|
||||||
|
has_main_web_app: bool = False
|
||||||
|
can_join_groups: bool = False
|
||||||
|
can_read_all_group_messages: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Настройка вебхука бота")
|
||||||
|
async def webhook(
|
||||||
|
cls, bots: Bot = bot, webhook_url: str = Webhook.WEBHOOK_URL, use_webhook: bool = Webhook.WEBHOOK
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Удаление или установка вебхука.
|
||||||
|
"""
|
||||||
|
await bots.delete_webhook(drop_pending_updates=True)
|
||||||
|
|
||||||
|
if use_webhook:
|
||||||
|
if webhook_url is None:
|
||||||
|
raise ValueError("Для установки вебхука необходимо указать webhook_url")
|
||||||
|
try:
|
||||||
|
await bots.set_webhook(Webhook.WEBHOOK_URL)
|
||||||
|
except TelegramRetryAfter as e:
|
||||||
|
print(f"Flood control: повтор через {e.retry_after} секунд")
|
||||||
|
await sleep(e.retry_after)
|
||||||
|
await bots.set_webhook(Webhook.WEBHOOK_URL)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
|
||||||
|
async def info(cls, bots: Bot = bot) -> dict[str, object]:
|
||||||
|
"""
|
||||||
|
Получает и сохраняет информацию о боте.
|
||||||
|
"""
|
||||||
|
bot_info: User = await bots.get_me()
|
||||||
|
|
||||||
|
cls.id = bot_info.id
|
||||||
|
cls.url = f"tg://user?id={cls.id}"
|
||||||
|
cls.first_name = bot_info.first_name
|
||||||
|
cls.last_name = bot_info.last_name
|
||||||
|
cls.username = bot_info.username
|
||||||
|
cls.language_code = bot_info.language_code
|
||||||
|
cls.is_premium = getattr(bot_info, "is_premium", False)
|
||||||
|
cls.added_to_attachment_menu = bot_info.added_to_attachment_menu
|
||||||
|
cls.supports_inline_queries = bot_info.supports_inline_queries
|
||||||
|
cls.can_connect_to_business = bot_info.can_connect_to_business
|
||||||
|
cls.has_main_web_app = bot_info.has_main_web_app
|
||||||
|
cls.can_join_groups = getattr(bot_info, "can_join_groups", False)
|
||||||
|
cls.can_read_all_group_messages = getattr(bot_info, "can_read_all_group_messages", False)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": cls.id,
|
||||||
|
"url": cls.url,
|
||||||
|
"first_name": cls.first_name,
|
||||||
|
"last_name": cls.last_name,
|
||||||
|
"username": cls.username,
|
||||||
|
"language_code": cls.language_code,
|
||||||
|
"prefix": cls.prefix,
|
||||||
|
"bot_owner": cls.bot_owner,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
|
||||||
|
async def set_administrator_rights(
|
||||||
|
bots: Bot = bot, rights: ChatAdministratorRights = BotEdit.RIGHTS
|
||||||
|
) -> None:
|
||||||
|
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
|
||||||
|
if current_rights != rights:
|
||||||
|
await bots.set_my_default_administrator_rights(rights)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
|
||||||
|
async def set_name(bots: Bot = bot, new_name: str = BotEdit.NAME) -> None:
|
||||||
|
current_name: str = (await bots.get_me()).first_name
|
||||||
|
if not (1 <= len(new_name) <= 32):
|
||||||
|
raise ValueError("Имя бота должно быть от 1 до 32 символов.")
|
||||||
|
if current_name != new_name:
|
||||||
|
await bots.set_my_name(new_name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Обновление описания бота")
|
||||||
|
async def set_description(bots: Bot = bot, new_description: str = BotEdit.DESCRIPTION) -> None:
|
||||||
|
current_description: BotDescription = await bots.get_my_description()
|
||||||
|
if not (0 < len(new_description) <= 255):
|
||||||
|
raise ValueError("Описание должно быть от 1 до 255 символов.")
|
||||||
|
if current_description.description != new_description:
|
||||||
|
await bots.set_my_description(description=new_description)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@log(level="INFO", log_type="BOT", text="Обновление короткого описания бота")
|
||||||
|
async def set_short_description(bots: Bot = bot, new_short: str = BotEdit.SHORT_DESCRIPTION) -> None:
|
||||||
|
current_short: BotShortDescription = await bots.get_my_short_description()
|
||||||
|
if not (0 < len(new_short) <= 512):
|
||||||
|
raise ValueError("Короткое описание должно быть от 1 до 512 символов.")
|
||||||
|
if current_short.short_description != new_short:
|
||||||
|
await bots.set_my_short_description(short_description=new_short)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_info_out(out: bool = True) -> str:
|
||||||
|
bot_time: str = f"Бот @{BotInfo.username} запущен в {datetime.now().strftime("%S:%M:%H %d-%m-%Y")}\n"
|
||||||
|
bot_name: str = f"Основное имя: {BotInfo.first_name}\n"
|
||||||
|
bot_postname: str = f" Доп. имя: {BotInfo.last_name}\n"
|
||||||
|
bot_username: str = f" Юзернейм: @{BotInfo.username}\n"
|
||||||
|
bot_id: str = f" ID: {BotInfo.id}\n"
|
||||||
|
bot_can_join_groups: str = f" Может ли вступать в группы: {BotInfo.can_join_groups}\n"
|
||||||
|
bot_can_read_all_group_messages: str = f" Чтение всех сообщений: {BotInfo.can_read_all_group_messages}\n"
|
||||||
|
bot_added_to_attachment_menu: str = f" Добавлен в меню вложений: {BotInfo.added_to_attachment_menu}\n"
|
||||||
|
bot_supports_inline_queries: str = f" Поддерживает инлайн-запросы: {BotInfo.supports_inline_queries}\n"
|
||||||
|
bot_can_connect_to_business: str = f" Подключение к бизнес-аккаунтам: {BotInfo.can_connect_to_business}\n"
|
||||||
|
bot_has_main_web_app: str = f" Основное веб-приложение: {BotInfo.has_main_web_app}\n"
|
||||||
|
|
||||||
|
# Формируем полный текст с выводом информации о боте
|
||||||
|
bot_all_info: str = (f"{bot_name} {bot_postname} {bot_username} {bot_id} "
|
||||||
|
f"{bot_can_join_groups} {bot_can_read_all_group_messages} "
|
||||||
|
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} {bot_can_connect_to_business} "
|
||||||
|
f"{bot_has_main_web_app}")
|
||||||
|
|
||||||
|
if out:
|
||||||
|
print(f"\033[34m{bot_all_info}\033[0m")
|
||||||
|
|
||||||
|
# Записываем информацию в файл
|
||||||
|
try:
|
||||||
|
with open("Logs/info.log", 'w', encoding='utf-8') as log_file:
|
||||||
|
log_file.write(f"{bot_time}{bot_all_info}")
|
||||||
|
|
||||||
|
# Создание файла bot_start.log
|
||||||
|
with open("Logs/bot_start.log", 'a', encoding='utf-8') as log_start_file:
|
||||||
|
log_start_file.write(f"{bot_time}\n")
|
||||||
|
return bot_all_info
|
||||||
|
|
||||||
|
# Проверка на ошибку и ее логирование
|
||||||
|
except Exception as e:
|
||||||
|
raise f"Ошибка при получении ID пользователя: {e}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
|
||||||
|
async def setup(cls, bots: Bot = bot, perm: bool = Permission.BOT_EDIT):
|
||||||
|
await cls.webhook(bots=bots)
|
||||||
|
await cls.info(bots=bots)
|
||||||
|
if perm:
|
||||||
|
await cls.set_administrator_rights(bots=bots)
|
||||||
|
await cls.set_description(bots=bots)
|
||||||
|
await cls.set_short_description(bots=bots)
|
||||||
|
await cls.set_name(bots=bots)
|
||||||
44
bot/core/webhook.py
Normal file
44
bot/core/webhook.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from aiogram.types import Update
|
||||||
|
|
||||||
|
from .bots import dp, bot
|
||||||
|
from middleware import loggers
|
||||||
|
|
||||||
|
class WebhookApp:
|
||||||
|
"""Приложение aiohttp для обработки webhook-запросов."""
|
||||||
|
|
||||||
|
def __init__(self, host: str = "0.0.0.0", port: int = 8080) -> None:
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.app: web.Application = web.Application()
|
||||||
|
self.app.router.add_post("/webhook", self.handle_update)
|
||||||
|
self.runner: web.AppRunner | None = None
|
||||||
|
self.site: web.TCPSite | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def handle_update(request: web.Request) -> web.Response:
|
||||||
|
"""Обработчик входящих запросов от Telegram."""
|
||||||
|
try:
|
||||||
|
update_json: dict[str, Any] = await request.json()
|
||||||
|
update: Update = Update.model_validate(update_json)
|
||||||
|
await dp.feed_update(bot=bot, update=update)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обработки webhook-запроса: {e}")
|
||||||
|
return web.Response(status=500)
|
||||||
|
|
||||||
|
return web.Response(status=200)
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Асинхронный запуск aiohttp-приложения."""
|
||||||
|
self.runner = web.AppRunner(self.app)
|
||||||
|
await self.runner.setup()
|
||||||
|
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||||
|
await self.site.start()
|
||||||
|
loggers.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""Остановка aiohttp-приложения."""
|
||||||
|
if self.runner:
|
||||||
|
await self.runner.cleanup()
|
||||||
5
bot/filters/__init__.py
Normal file
5
bot/filters/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .callback import *
|
||||||
|
from .chat_rights import *
|
||||||
|
from .chat_type import *
|
||||||
|
from .message_content import *
|
||||||
|
from .subscrided import *
|
||||||
21
bot/filters/callback.py
Normal file
21
bot/filters/callback.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("CallbackDataStartsWith",)
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackDataStartsWith(BaseFilter):
|
||||||
|
"""
|
||||||
|
Фильтр для callback_data, начинающихся с префикса.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.callback_query(CallbackDataStartsWith("menu:"))
|
||||||
|
async def handler(cb: CallbackQuery):
|
||||||
|
await cb.answer("Это callback из меню ✅")
|
||||||
|
"""
|
||||||
|
def __init__(self, prefix: str) -> None:
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
async def __call__(self, callback: CallbackQuery) -> bool:
|
||||||
|
return bool(callback.data and callback.data.startswith(self.prefix))
|
||||||
73
bot/filters/chat_rights.py
Normal file
73
bot/filters/chat_rights.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram.types import Message, ResultChatMemberUnion
|
||||||
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("IsChatCreator", "IsAdmin", "IsModerator",)
|
||||||
|
|
||||||
|
|
||||||
|
class IsChatCreator(BaseFilter):
|
||||||
|
"""
|
||||||
|
Пользователь является создателем чата.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsChatCreator())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Ты создатель этого чата 👑")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||||
|
try:
|
||||||
|
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||||
|
return member.status == "creator"
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IsAdmin(BaseFilter):
|
||||||
|
"""
|
||||||
|
Пользователь является администратором (или создателем).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsAdmin())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Ты админ ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||||
|
try:
|
||||||
|
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||||
|
return member.status in {"administrator", "creator"}
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IsModerator(BaseFilter):
|
||||||
|
"""
|
||||||
|
Администратор с модераторскими правами:
|
||||||
|
- удаление сообщений
|
||||||
|
- ограничение пользователей
|
||||||
|
- закрепление сообщений
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsModerator())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Ты модератор ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||||
|
try:
|
||||||
|
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||||
|
|
||||||
|
if member.status == "creator":
|
||||||
|
return True
|
||||||
|
if member.status != "administrator":
|
||||||
|
return False
|
||||||
|
|
||||||
|
required_rights: list[bool] = [
|
||||||
|
getattr(member, "can_delete_messages", False),
|
||||||
|
getattr(member, "can_restrict_members", False),
|
||||||
|
getattr(member, "can_pin_messages", False),
|
||||||
|
]
|
||||||
|
return all(required_rights)
|
||||||
|
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
return False
|
||||||
31
bot/filters/chat_type.py
Normal file
31
bot/filters/chat_type.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("IsPrivate", "IsGroup",)
|
||||||
|
|
||||||
|
|
||||||
|
class IsPrivate(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение в личке с ботом.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsPrivate())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это ЛС ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return message.chat.type == "private"
|
||||||
|
|
||||||
|
|
||||||
|
class IsGroup(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение в группе или супергруппе.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsGroup())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это сообщение в группе ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return message.chat.type in {"group", "supergroup"}
|
||||||
67
bot/filters/message_content.py
Normal file
67
bot/filters/message_content.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("IsReply", "IsForwarded", "HasMedia", "ContainsURL",)
|
||||||
|
|
||||||
|
|
||||||
|
class IsReply(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение является ответом.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsReply())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это реплай ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return message.reply_to_message is not None
|
||||||
|
|
||||||
|
|
||||||
|
class IsForwarded(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение переслано из другого чата/от пользователя.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(IsForwarded())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это пересланное сообщение 🔄")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return (message.forward_from is not None) or (message.forward_from_chat is not None)
|
||||||
|
|
||||||
|
|
||||||
|
class HasMedia(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение содержит медиа (фото, видео, документ и т.д.).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(HasMedia())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это медиа ✅")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return any([
|
||||||
|
message.photo,
|
||||||
|
message.video,
|
||||||
|
message.document,
|
||||||
|
message.audio,
|
||||||
|
message.voice,
|
||||||
|
message.video_note,
|
||||||
|
message.sticker,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ContainsURL(BaseFilter):
|
||||||
|
"""
|
||||||
|
Сообщение содержит ссылку (http/https).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@router.message(ContainsURL())
|
||||||
|
async def handler(msg: Message):
|
||||||
|
await msg.answer("Это сообщение с ссылкой 🔗")
|
||||||
|
"""
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
if not message.text:
|
||||||
|
return False
|
||||||
|
return "http://" in message.text or "https://" in message.text
|
||||||
39
bot/filters/subscrided.py
Normal file
39
bot/filters/subscrided.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from aiogram.types import Message, ResultChatMemberUnion
|
||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
# Настройки экспорта
|
||||||
|
__all__ = ("FilterSubscribed",)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSubscribed(BaseFilter):
|
||||||
|
"""
|
||||||
|
Фильтр для проверки подписки пользователя на один или несколько каналов.
|
||||||
|
Поддерживает как публичные каналы (username), так и приватные (ID).
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
# Проверка сразу двух каналов: публичный по username и приватный по ID
|
||||||
|
@router.message(FilterSubscribed(["@public_channel", -1001234567890]))
|
||||||
|
async def only_subscribed(message: Message):
|
||||||
|
await message.answer("Ты подписан и на публичный, и на приватный канал ✅")
|
||||||
|
"""
|
||||||
|
def __init__(self, channels: list[Union[str, int]]) -> None:
|
||||||
|
self.channels = channels
|
||||||
|
|
||||||
|
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||||
|
for channel in self.channels:
|
||||||
|
try:
|
||||||
|
member: ResultChatMemberUnion = await bot.get_chat_member(
|
||||||
|
chat_id=channel,
|
||||||
|
user_id=message.from_user.id
|
||||||
|
)
|
||||||
|
if member.status in ("left", "kicked"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
except (TelegramBadRequest, TelegramForbiddenError):
|
||||||
|
# Канал недоступен, либо у бота нет прав
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
15
bot/handlers/__init__.py
Normal file
15
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
#from .commands import router as cmd_routers
|
||||||
|
from .messages import router as messages_routers
|
||||||
|
from .secret import router as secret_routers
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
router.include_routers(
|
||||||
|
#cmd_routers,
|
||||||
|
secret_routers,
|
||||||
|
messages_routers,
|
||||||
|
)
|
||||||
13
bot/handlers/commands/__init__.py
Normal file
13
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .admins import router as admin_cmd_router
|
||||||
|
from .users import router as users_cmd_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
router.include_routers(
|
||||||
|
admin_cmd_router,
|
||||||
|
users_cmd_router,
|
||||||
|
)
|
||||||
11
bot/handlers/commands/admins/__init__.py
Normal file
11
bot/handlers/commands/admins/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .settings_cmd import router as settings_cmd_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
router.include_routers(
|
||||||
|
settings_cmd_router,
|
||||||
|
)
|
||||||
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
|
from bot.templates import msg_photo
|
||||||
|
from bot.utils.interesting_facts import interesting_fact
|
||||||
|
from bot.core.bots import BotInfo
|
||||||
|
from configs import COMMANDS, RpValue
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
CMD: str = "settings".lower()
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.lower() == CMD)
|
||||||
|
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||||
|
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""Обработчик команды /start"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Создание инлайн-клавиатуры
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
|
||||||
|
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
|
||||||
|
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
|
||||||
|
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str = _(
|
||||||
|
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||||
|
|
||||||
|
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
|
||||||
|
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||||
|
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||||
|
|
||||||
|
Интересный факт:
|
||||||
|
<blockquote>{fact}</blockquote>
|
||||||
|
"""
|
||||||
|
).format(
|
||||||
|
url=message.from_user.url if message.from_user else "",
|
||||||
|
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||||
|
rp_name=RpValue.RP_NAME,
|
||||||
|
fact=interesting_fact(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||||
13
bot/handlers/commands/users/__init__.py
Normal file
13
bot/handlers/commands/users/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .start_cmd import router as start_cmd_router
|
||||||
|
from .active import router as active_cmd_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
router.include_routers(
|
||||||
|
start_cmd_router,
|
||||||
|
active_cmd_router,
|
||||||
|
)
|
||||||
45
bot/handlers/commands/users/active.py
Normal file
45
bot/handlers/commands/users/active.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
|
||||||
|
from bot.templates import msg_photo
|
||||||
|
from bot.core.bots import BotInfo
|
||||||
|
from configs import COMMANDS
|
||||||
|
from database import db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
|
||||||
|
CMD: str = "active".lower()
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.lower() == CMD)
|
||||||
|
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||||
|
async def active_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""Обработчик команды /active"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Получить статистику сообщений пользователя
|
||||||
|
day, week, month, total = await db.get_message_stats(message.from_user.id)
|
||||||
|
|
||||||
|
print(f"За день: {day} сообщений")
|
||||||
|
print(f"За неделю: {week} сообщений")
|
||||||
|
print(f"За месяц: {month} сообщений")
|
||||||
|
print(f"Всего: {total} сообщений")
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str =f"""
|
||||||
|
За день: {day} сообщений
|
||||||
|
За неделю: {week} сообщений
|
||||||
|
За месяц: {month} сообщений
|
||||||
|
Всего: {total} сообщений
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await msg_photo(message=message, text=text,)
|
||||||
0
bot/handlers/commands/users/anketa_cmd.py
Normal file
0
bot/handlers/commands/users/anketa_cmd.py
Normal file
218
bot/handlers/commands/users/new_cmd.py
Normal file
218
bot/handlers/commands/users/new_cmd.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import re
|
||||||
|
from typing import Optional, Dict, Tuple
|
||||||
|
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
|
from bot.core.bots import BotInfo
|
||||||
|
from bot.keyboards.inline.decision import decision_keyboard
|
||||||
|
from bot.states.new_states import NewStates
|
||||||
|
from bot.templates import msg
|
||||||
|
from middleware.loggers import log
|
||||||
|
from configs import COMMANDS, ImportantID, RpValue
|
||||||
|
|
||||||
|
# Глобальная мапа для хранения связей пользователь-топик
|
||||||
|
user_topic_map: Dict[Tuple[int, str], int] = {}
|
||||||
|
|
||||||
|
__all__ = ("router",)
|
||||||
|
CMD: str = "new"
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
TOPIC_TYPE: str = "anketa"
|
||||||
|
|
||||||
|
TEXTS: Dict[str, Dict[str, str]] = {
|
||||||
|
"anketa": {
|
||||||
|
"accept": f"<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!\n\nФлуд: {RpValue.FLUD_URL}\nРолевая: {RpValue.RP_URL}",
|
||||||
|
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_russian_text(text: str) -> bool:
|
||||||
|
"""Проверяет текст на соответствие русским буквам, пробелам и дефисам."""
|
||||||
|
return bool(re.fullmatch(r"[А-Яа-яЁё\s\-]+", text))
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Команда /new =====================
|
||||||
|
@router.callback_query(F.data == CMD)
|
||||||
|
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||||
|
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||||
|
async def new_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""
|
||||||
|
Начало анкеты /new.
|
||||||
|
Отправляет пользователю сообщение с просьбой указать желаемую роль.
|
||||||
|
"""
|
||||||
|
await state.clear()
|
||||||
|
await state.set_state(NewStates.role)
|
||||||
|
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||||
|
|
||||||
|
text: str = _(
|
||||||
|
"Пожалуйста, отправьте желаемую роль:\n"
|
||||||
|
"(только русские буквы, пробелы или дефисы)"
|
||||||
|
)
|
||||||
|
|
||||||
|
await msg(message=message, text=text, markup=ikb)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Обработка роли =====================
|
||||||
|
@router.message(NewStates.role)
|
||||||
|
async def process_role(message: Message, state: FSMContext) -> None:
|
||||||
|
"""Обрабатывает ввод роли и запрашивает сортол."""
|
||||||
|
if not await validate_russian_text(message.text):
|
||||||
|
await message.reply("Ошибка: роль должна содержать только русские буквы, пробелы или дефисы.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(role=message.text.strip().title())
|
||||||
|
await state.set_state(NewStates.sorol)
|
||||||
|
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||||
|
|
||||||
|
await message.reply(
|
||||||
|
text="Теперь укажите желаемый сортол:\n(только русские буквы, пробелы или дефисы)",
|
||||||
|
reply_markup=ikb.as_markup()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Обработка сортола =====================
|
||||||
|
@router.message(NewStates.sorol)
|
||||||
|
async def process_sortol(message: Message, state: FSMContext) -> None:
|
||||||
|
"""Обрабатывает ввод сортола и запрашивает кодовую фразу."""
|
||||||
|
if not await validate_russian_text(message.text):
|
||||||
|
await message.reply("Ошибка: сорол должен содержать только русские буквы, пробелы или дефисы.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(sortol=message.text.strip().title())
|
||||||
|
await state.set_state(NewStates.code_phrase)
|
||||||
|
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||||
|
|
||||||
|
await message.reply(
|
||||||
|
text="Теперь введите кодовую фразу из правил:",
|
||||||
|
reply_markup=ikb.as_markup()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Обработка кодовой фразы =====================
|
||||||
|
@router.message(NewStates.code_phrase)
|
||||||
|
async def process_code_phrase(message: Message, state: FSMContext) -> None:
|
||||||
|
"""Обрабатывает ввод кодовой фразы и показывает предпросмотр анкеты."""
|
||||||
|
code_phrase = message.text.strip()
|
||||||
|
if not code_phrase:
|
||||||
|
await message.reply("Кодовая фраза не может быть пустой.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(code_phrase=code_phrase)
|
||||||
|
data: Dict[str, str] = await state.get_data()
|
||||||
|
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(
|
||||||
|
InlineKeyboardButton(text="Отправить!", callback_data="submit_new"),
|
||||||
|
InlineKeyboardButton(text="Отмена↩️", callback_data="start")
|
||||||
|
)
|
||||||
|
|
||||||
|
text: str = (
|
||||||
|
f"<b>Проверьте данные анкеты:</b>\n\n"
|
||||||
|
f"• Роль: {data['role']}\n"
|
||||||
|
f"• Сортол: {data['sortol']}\n"
|
||||||
|
f"• Кодовая фраза: {data['code_phrase']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.reply(text, reply_markup=ikb.as_markup())
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Отправка анкеты в поддержку =====================
|
||||||
|
@router.callback_query(F.data == "submit_new")
|
||||||
|
async def submit_new_cmd(callback: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""Отправляет анкету в топик форума поддержки и создает запись в мапе."""
|
||||||
|
data: Dict[str, str] = await state.get_data()
|
||||||
|
user = callback.from_user
|
||||||
|
|
||||||
|
# Создаем топик в форуме
|
||||||
|
topic = await callback.bot.create_forum_topic(
|
||||||
|
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||||
|
name=f"Анкета от {user.full_name}"
|
||||||
|
)
|
||||||
|
thread_id: int = topic.message_thread_id
|
||||||
|
|
||||||
|
# Сохраняем связь пользователь-топик
|
||||||
|
user_topic_map[(user.id, TOPIC_TYPE)] = thread_id
|
||||||
|
|
||||||
|
# Формируем текст анкеты
|
||||||
|
text: str = (
|
||||||
|
f'<b><a href="tg://user?id={user.id}">Анкета</a></b>\n\n'
|
||||||
|
f"• Роль: {data['role']}\n"
|
||||||
|
f"• Сортол: {data['sortol']}\n"
|
||||||
|
f"• Кодовая фраза: {data['code_phrase']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем в топик с кнопками принятия/отклонения
|
||||||
|
await callback.bot.send_message(
|
||||||
|
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||||
|
message_thread_id=thread_id,
|
||||||
|
text=text,
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=decision_keyboard(thread_id=thread_id, kind=TOPIC_TYPE)
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.message.edit_text("✅ Ваша анкета успешно отправлена на рассмотрение!")
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Обработка решения админов =====================
|
||||||
|
@router.callback_query(F.data.regexp(r"^([a-z_]+):(accept|reject):(\d+)$"))
|
||||||
|
async def process_decision_callback(callback: CallbackQuery) -> None:
|
||||||
|
"""Обрабатывает решение администраторов и отправляет результат пользователю."""
|
||||||
|
kind, action, thread_id_str = callback.data.split(":")
|
||||||
|
thread_id = int(thread_id_str)
|
||||||
|
|
||||||
|
# Ищем пользователя по thread_id в мапе
|
||||||
|
user_id = None
|
||||||
|
for (uid, k), tid in user_topic_map.items():
|
||||||
|
if k == kind and tid == thread_id:
|
||||||
|
user_id = uid
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await callback.answer("Пользователь не найден.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text_to_send: Optional[str] = TEXTS.get(kind, {}).get(action)
|
||||||
|
if not text_to_send:
|
||||||
|
await callback.answer("Некорректные данные.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.bot.send_message(chat_id=user_id, text=text_to_send, parse_mode="HTML")
|
||||||
|
await callback.message.edit_reply_markup(reply_markup=None)
|
||||||
|
await callback.answer("Ответ отправлен пользователю.")
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== Пересылка ответов админов пользователю =====================
|
||||||
|
@router.message(F.is_topic_message, F.reply_to_message, ~F.from_user.is_bot)
|
||||||
|
async def forward_reply_to_user(message: Message) -> None:
|
||||||
|
"""Пересылает ответы администраторов из топика пользователю."""
|
||||||
|
thread_id = message.message_thread_id
|
||||||
|
if not thread_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ищем пользователя по thread_id
|
||||||
|
user_id = None
|
||||||
|
for (uid, _), tid in user_topic_map.items():
|
||||||
|
if tid == thread_id:
|
||||||
|
user_id = uid
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
reply_text: str = f"<b>Ответ администратора:</b>\n{message.html_text}"
|
||||||
|
try:
|
||||||
|
await message.bot.send_message(chat_id=user_id, text=reply_text, parse_mode="HTML")
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(f"⚠️ Не удалось отправить сообщение пользователю: {e}")
|
||||||
51
bot/handlers/commands/users/start_cmd.py
Normal file
51
bot/handlers/commands/users/start_cmd.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
|
from bot.templates import msg_photo
|
||||||
|
from bot.utils.interesting_facts import interesting_fact
|
||||||
|
from bot.core.bots import BotInfo
|
||||||
|
from configs import COMMANDS, RpValue
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
CMD: str = "start".lower()
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.lower() == CMD)
|
||||||
|
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||||
|
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""Обработчик команды /start"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Создание инлайн-клавиатуры
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
|
||||||
|
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
|
||||||
|
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
|
||||||
|
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str = _(
|
||||||
|
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||||
|
|
||||||
|
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
|
||||||
|
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||||
|
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||||
|
|
||||||
|
Интересный факт:
|
||||||
|
<blockquote>{fact}</blockquote>
|
||||||
|
"""
|
||||||
|
).format(
|
||||||
|
url=message.from_user.url if message.from_user else "",
|
||||||
|
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||||
|
rp_name=RpValue.RP_NAME,
|
||||||
|
fact=interesting_fact(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||||
15
bot/handlers/messages/__init__.py
Normal file
15
bot/handlers/messages/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .default import router as default_message_router
|
||||||
|
from .reply_msg import router as reply_message_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ('router',)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подготовка роутера команд
|
||||||
|
#router.include_routers(
|
||||||
|
#reply_message_router,
|
||||||
|
#)
|
||||||
|
|
||||||
|
# Подключение стандартного роутера
|
||||||
|
router.include_router(default_message_router)
|
||||||
139
bot/handlers/messages/default.py
Normal file
139
bot/handlers/messages/default.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from bot.utils import get_best_response
|
||||||
|
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name="message_router")
|
||||||
|
|
||||||
|
|
||||||
|
# === Словарь ключевых слов (синонимы) и возможных ответов ===
|
||||||
|
RESPONSES: Dict[str, Dict[str, List[str]]] = {
|
||||||
|
"док": {
|
||||||
|
"keywords": ["доктор", "док", "дотторе", "зандик"],
|
||||||
|
"answers": [
|
||||||
|
"Дотторе довольно милый друг! Мне нравится проводить с ним время!",
|
||||||
|
"Иногда он бывает слишком суровым... Но я верю, что смогу его перевоспитать!",
|
||||||
|
"Мне иногда кажется, что он знает больше историй, чем хранится в библиотеке!",
|
||||||
|
"Дотторе говорит загадками... а я всё равно не всегда понимаю!",
|
||||||
|
"Он умный, но я уверен — внутри он добрый!",
|
||||||
|
"Дотторе иногда ворчит, но всё равно заботится обо мне по-своему!",
|
||||||
|
"Он часто думает о науке... а я думаю о печеньках!",
|
||||||
|
"Мне кажется, он притворяется злым, а на самом деле просто боится дружбы.",
|
||||||
|
"Когда он работает, в комнате становится тихо... даже огонь боится мешать ему.",
|
||||||
|
"Я иногда думаю... а улыбается ли он, когда меня не видит?",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"ара": {
|
||||||
|
"keywords": ["ара", "аранара", "аранары", "ары", "кто ты", "ты кто"],
|
||||||
|
"answers": [
|
||||||
|
"Мы, аранары, очень любим веселиться и смеяться!",
|
||||||
|
"Хи-хи! 🌱 Ты можешь звать меня Ари!",
|
||||||
|
"Наш народ живёт уже тысячи лет... но мы не умеем считать!",
|
||||||
|
"Я маленький грибочек, но у меня большое сердце!",
|
||||||
|
"Аранара — это хранитель улыбок и весёлых историй!",
|
||||||
|
"Я люблю играть с детьми и рассказывать им истории!",
|
||||||
|
"Говорят, что аранары видят то, что скрыто от других.",
|
||||||
|
"Я — часть этой библиотеки, её дыхание и её смех!",
|
||||||
|
"Аранара — это маленький проводник в мир грёз и чудес.",
|
||||||
|
"Мы появляемся там, где нужен друг, даже если никто не звал!",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"малыш": {
|
||||||
|
"keywords": ["малыш", "девочка", "малышка", "она", "болезнь"],
|
||||||
|
"answers": [
|
||||||
|
"Она милая девочка! Жаль, что больна!",
|
||||||
|
"Она обожает сказки! Может, именно поэтому засыпает так сладко.",
|
||||||
|
"А как её зовут?.. Я всегда забываю спросить!",
|
||||||
|
"Иногда во сне она улыбается... значит, ей снятся хорошие истории.",
|
||||||
|
"Дотторе грустит, когда смотрит на неё... но я верю, он её спасёт!",
|
||||||
|
"Она словно светильник в тёмной комнате... даже если свет её тускнеет.",
|
||||||
|
"Я думаю, её мечты сильнее болезни.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"эфир": {
|
||||||
|
"keywords": ["эфир", "проект", "изобретение", "сплав", "эксперимент", "ядро"],
|
||||||
|
"answers": [
|
||||||
|
"Эфир звучит как ветер, который нельзя поймать... но можно почувствовать!",
|
||||||
|
"Дотторе часто говорит о проектах, но я понимаю в них только половину!",
|
||||||
|
"Каждый новый сплав для него как новая история для меня.",
|
||||||
|
"Эксперимент — это как игра, только иногда она пахнет гарью...",
|
||||||
|
"Я слышал, что ядро может изменить всё... даже судьбы людей.",
|
||||||
|
"В лаборатории так много звуков — шипение кислот, стук молотов, шёпот формул.",
|
||||||
|
"Иногда мне кажется, что изобретения Дотторе живут своей жизнью...",
|
||||||
|
"Эфир? Кефир? ЗЕФИР!",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"мысль": {
|
||||||
|
"keywords": ["мысл", "мысль", "мысли", "думаешь"],
|
||||||
|
"answers": [
|
||||||
|
"О чём я думаю?.. Иногда о печеньках!",
|
||||||
|
"Голова как будто полная тумана...",
|
||||||
|
"Кажется, я что-то забыл... но не могу вспомнить...",
|
||||||
|
"Мысли приходят и уходят, как маленькие птички.",
|
||||||
|
"А ты когда-нибудь задумывался, откуда приходят мысли?",
|
||||||
|
"Иногда мои мысли путаются и превращаются в сказки.",
|
||||||
|
"Я думаю, что думать — это тяжело... лучше веселиться!",
|
||||||
|
"Может, мысли — это просто шёпот библиотеки в моей голове?",
|
||||||
|
"Когда я думаю слишком долго — у меня начинает чесаться макушка!",
|
||||||
|
"Мысли — как облака... смотришь, и они уже другие.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"тайн": {
|
||||||
|
"keywords": ["тайн", "тайны", "тайну", "тайна"],
|
||||||
|
"answers": [
|
||||||
|
"Тайны? О-о, мы играем в детективов?!",
|
||||||
|
"Я знаю много секретов... но не все можно рассказывать!",
|
||||||
|
"Иногда самые большие тайны прячутся на виду.",
|
||||||
|
"Тайна — это как закрытая книга. Ты хочешь открыть её?",
|
||||||
|
"Хи-хи... а если твоя тайна уже записана в библиотеке?",
|
||||||
|
"Некоторые тайны лучше хранить, чем раскрывать.",
|
||||||
|
"Каждый друг — это тоже тайна, которую мы открываем постепенно.",
|
||||||
|
"А твои секреты я храню надёжнее любого сундука!",
|
||||||
|
"Тайна — это искра любопытства! Без неё жизнь скучная.",
|
||||||
|
"Ш-ш-ш... хочешь услышать одну маленькую, но очень смешную тайну?",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Случайные фразы, если совпадения нет ===
|
||||||
|
RANDOM_PHRASES: List[str] = [
|
||||||
|
"Я Ари! Компаньон Дотторе и ваш лучший друг! Можете обращаться ко мне!",
|
||||||
|
"Я живу здесь уже десятки лет... и мне всё ещё весело!",
|
||||||
|
"Кхм... почему ты так странно разговариваешь? Ничего не понимаю!",
|
||||||
|
"Мы играем в шарады? Давай попробуй ещё раз, может я пойму хоть одно слово!",
|
||||||
|
"Ты кажешься таким загадочным... прямо как проекты Дотторе, которые меня вечно пугают!",
|
||||||
|
"Ой! Ты меня напугал! Но всё равно приятно видеть нового друга!",
|
||||||
|
"Если вдруг станет грустно — просто обними аранару. Мы очень мягкие!",
|
||||||
|
"Иногда даже мне хочется спрятаться между колб и подремать...",
|
||||||
|
"А может, именно твоё слово станет началом новой истории?",
|
||||||
|
"Дотторе говорит, что я слишком болтлив... а разве это плохо?",
|
||||||
|
"Ты такой серьёзный... может, стоит немного пошутить?",
|
||||||
|
"Иногда кажется, что слова сами выбирают нас, а не мы их!",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# === Хэндлеры ===
|
||||||
|
@router.message()
|
||||||
|
async def handle_message(message: Message, state: FSMContext) -> None:
|
||||||
|
"""
|
||||||
|
Обрабатывает входящие сообщения от пользователя.
|
||||||
|
Определяет ответ по ключевым словам или случайную фразу.
|
||||||
|
|
||||||
|
:param message: объект сообщения
|
||||||
|
:param state: FSMContext для работы с состояниями
|
||||||
|
"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
response: str = get_best_response(
|
||||||
|
message.text or "",
|
||||||
|
responses=RESPONSES,
|
||||||
|
random_phrases=RANDOM_PHRASES,
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.answer(text=response)
|
||||||
39
bot/handlers/messages/reply_msg.py
Normal file
39
bot/handlers/messages/reply_msg.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from random import choice
|
||||||
|
from typing import List
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
router: Router = Router(name="reply_router")
|
||||||
|
|
||||||
|
RANDOM_PHRASES: List[str] = [
|
||||||
|
"Бла-бла-бла!", "Хва-а-а-тит!", "Серьёзно? 😏", "Опять ты это говоришь...",
|
||||||
|
"Хи-хи, смешно же!", "Ты снова шутник?", "Я уже слышал это раньше!", "Эй, не надо так!",
|
||||||
|
"Ладно, ладно, хватит!", "Хмм... интересно...", "Ты меня удивляешь!", "А давай лучше что-то новое?",
|
||||||
|
"Не могу поверить!", "Ахаха, это забавно!", "Серьёзно? Ну ладно...", "Эй, это уже слишком!",
|
||||||
|
"О, это было неожиданно!",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.message()
|
||||||
|
async def reply_message(message: Message, state: FSMContext) -> None:
|
||||||
|
# Достаём данные из состояния
|
||||||
|
data = await state.get_data()
|
||||||
|
last_bot_text = data.get("last_bot_text", "")
|
||||||
|
|
||||||
|
# КРИТИЧЕСКИ ВАЖНО: Проверяем, что состояние не пустое после перезапуска.
|
||||||
|
# Если состояние пустое (например, после перезапуска), то мы НЕ должны считать,
|
||||||
|
if last_bot_text and message.text and message.text.strip() == last_bot_text.strip():
|
||||||
|
response = "Не повторяй за мной!"
|
||||||
|
else:
|
||||||
|
response = choice(RANDOM_PHRASES)
|
||||||
|
|
||||||
|
ids = message.message_id-1
|
||||||
|
print(str())
|
||||||
|
|
||||||
|
# Отправляем ответ и ПОЛУЧАЕМ ОБЪЕКТ ОТПРАВЛЕННОГО СООБЩЕНИЯ
|
||||||
|
sent_message = await message.reply(response)
|
||||||
|
|
||||||
|
# Сохраняем текст последнего сообщения бота в состоянии
|
||||||
|
# Теперь состояние будет обновлено после каждого сообщения бота
|
||||||
|
await state.update_data(last_bot_text=sent_message.text)
|
||||||
13
bot/handlers/secret/__init__.py
Normal file
13
bot/handlers/secret/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .secret1 import router as secret1_router
|
||||||
|
#from .secret2 import router as secret2_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ('router',)
|
||||||
|
router: Router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Подключение секретного роутера
|
||||||
|
router.include_routers(
|
||||||
|
secret1_router,
|
||||||
|
#secret2_router,
|
||||||
|
)
|
||||||
45
bot/handlers/secret/secret1.py
Normal file
45
bot/handlers/secret/secret1.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.utils.markdown import hide_link
|
||||||
|
|
||||||
|
from middleware.loggers import log
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name="secret_router")
|
||||||
|
CMD: str = "secret_1"
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.lower() == "истинная цель короля всегда было мироздание")
|
||||||
|
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||||
|
async def secret1_cmd(message: Message, state: FSMContext) -> None:
|
||||||
|
"""Обработчик секретов"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str = f"""{hide_link("https://rp.primo.dpdns.org/wp-content/uploads/2025/08/1234567.png")}
|
||||||
|
<b><u>Запись №-...18</u></b>
|
||||||
|
|
||||||
|
<blockquote><i>Значит, этот </i><i><b>правда</b> действительно </i><i><b>существует…</b> Хах.. Хахахах! Я смог найти решение</i><i><b>! Я!! СМОГ!!!</b>
|
||||||
|
Все линии, пропорции, каждый слой металла и кристалла — всё сходится.
|
||||||
|
Сколько лет я </i><i><b>скитался</b> по лабораториям, библиотекам, ища этот след… а ведь всё это время </i><i><b>ключ</b> к моей цели лежал прямо перед глазами — на чертеже.</i></blockquote>
|
||||||
|
|
||||||
|
<blockquote><i>Получится ли у меня...?\n
|
||||||
|
Я создаю не просто броню. Я пытаюсь воплотить в материале замысел, который перевернёт всё, что мы знали.
|
||||||
|
Каждый слой, каждая руна — это шаг к воплощению моей идеи. Даже спустя десятки лет я помню, как возвращал из небытия те конструкции, что раньше казались невозможными…</i></blockquote>
|
||||||
|
|
||||||
|
<blockquote><i>Возможно ли, что сама структура материи и магии </i><i><b>изменит..</b>
|
||||||
|
Или же это моя броня станет первым устройством, способное изменить их мнение?..</i></blockquote>
|
||||||
|
|
||||||
|
<blockquote><i>И всё же один вопрос не даёт мне покоя: сможет ли этот замысел завершить то, что я задумал…
|
||||||
|
Станет ли моя броня инструментом, с помощью которого замысел воплотится в реальность?..</i></blockquote>
|
||||||
|
|
||||||
|
<blockquote><i>Пожалуй, придётся ещё раз вернуться к чертежам и проверить расчёты.
|
||||||
|
Что-то подсказывает мне: каждая линия, каждый символ на этом листе — это не просто металл и руны, это путь к моей великой… </i><i><b>идее</b>. ~</i></blockquote>
|
||||||
|
|
||||||
|
<tg-spoiler>Да… это оно. Всё ведёт к замыслу, к который я стремился десятилетиями…</tg-spoiler>"""
|
||||||
|
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await message.reply(text=text)
|
||||||
134
bot/handlers/secret/secret2.py
Normal file
134
bot/handlers/secret/secret2.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command, StateFilter
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Создаем роутер
|
||||||
|
knowledge_router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
# Определяем состояния
|
||||||
|
class KnowledgeStates(StatesGroup):
|
||||||
|
question1 = State()
|
||||||
|
question2 = State()
|
||||||
|
question3 = State()
|
||||||
|
question4 = State()
|
||||||
|
question5 = State()
|
||||||
|
question6 = State()
|
||||||
|
|
||||||
|
|
||||||
|
# Вопросы и ответы (замените на свои)
|
||||||
|
QUESTIONS = {
|
||||||
|
1: "Вопрос1",
|
||||||
|
2: "Вопрос2",
|
||||||
|
3: "Вопрос3",
|
||||||
|
4: "Вопрос4",
|
||||||
|
5: "Вопрос5",
|
||||||
|
6: "Вопрос6"
|
||||||
|
}
|
||||||
|
|
||||||
|
ANSWERS = {
|
||||||
|
1: {"Ответ 11": "СообщениеА1", "Ответ 12": "СообщениеБ1"},
|
||||||
|
2: {"Ответ 21": "СообщениеА2", "Ответ 22": "СообщениеБ2"},
|
||||||
|
3: {"Ответ 31": "СообщениеА3", "Ответ 32": "СообщениеБ3"},
|
||||||
|
4: {"Ответ 41": "СообщениеА4", "Ответ 42": "СообщениеБ4"},
|
||||||
|
5: {"Ответ 51": "СообщениеА5", "Ответ 52": "СообщениеБ5"},
|
||||||
|
6: {"Ответ 61": "СообщениеА6", "Ответ 62": "СообщениеБ6"}
|
||||||
|
}
|
||||||
|
|
||||||
|
FINAL_MESSAGES = {
|
||||||
|
"all_1": "ИТОГ1 - Все ответы первого типа!",
|
||||||
|
"all_2": "ИТОГ2 - Все ответы второго типа!",
|
||||||
|
"mixed": "ИТОГ1 - Смешанные ответы!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Запуск сессии знаний
|
||||||
|
@knowledge_router.message(StateFilter(None), Command("знания"))
|
||||||
|
@knowledge_router.message(StateFilter(None), F.text.casefold() == "пора заняться знаниями")
|
||||||
|
async def start_knowledge_session(message: Message, state: FSMContext):
|
||||||
|
await message.answer("Отлично! Начинаем сессию знаний! 🧠")
|
||||||
|
await message.answer(QUESTIONS[1])
|
||||||
|
await state.set_state(KnowledgeStates.question1)
|
||||||
|
await state.update_data(answers={})
|
||||||
|
|
||||||
|
|
||||||
|
# Обработчики для каждого вопроса
|
||||||
|
@knowledge_router.message(KnowledgeStates.question1, F.text.in_(ANSWERS[1].keys()))
|
||||||
|
async def process_question1(message: Message, state: FSMContext):
|
||||||
|
user_answer = message.text
|
||||||
|
response_message = ANSWERS[1][user_answer]
|
||||||
|
|
||||||
|
# Сохраняем ответ
|
||||||
|
answer_code = 1 if user_answer == "Ответ 11" else 2
|
||||||
|
await state.update_data(answers={"q1": answer_code})
|
||||||
|
|
||||||
|
# Отправляем сообщение и следующий вопрос
|
||||||
|
await message.answer(response_message + "\n\n" + QUESTIONS[2])
|
||||||
|
await state.set_state(KnowledgeStates.question2)
|
||||||
|
|
||||||
|
|
||||||
|
@knowledge_router.message(KnowledgeStates.question2, F.text.in_(ANSWERS[2].keys()))
|
||||||
|
async def process_question2(message: Message, state: FSMContext):
|
||||||
|
user_answer = message.text
|
||||||
|
response_message = ANSWERS[2][user_answer]
|
||||||
|
|
||||||
|
# Сохраняем ответ
|
||||||
|
answer_code = 1 if user_answer == "Ответ 21" else 2
|
||||||
|
data = await state.get_data()
|
||||||
|
answers = data.get("answers", {})
|
||||||
|
answers["q2"] = answer_code
|
||||||
|
await state.update_data(answers=answers)
|
||||||
|
|
||||||
|
# Отправляем сообщение и следующий вопрос
|
||||||
|
await message.answer(response_message + "\n\n" + QUESTIONS[3])
|
||||||
|
await state.set_state(KnowledgeStates.question3)
|
||||||
|
|
||||||
|
|
||||||
|
# Добавьте аналогичные обработчики для question3-question5
|
||||||
|
|
||||||
|
@knowledge_router.message(KnowledgeStates.question6, F.text.in_(ANSWERS[6].keys()))
|
||||||
|
async def process_question6(message: Message, state: FSMContext):
|
||||||
|
user_answer = message.text
|
||||||
|
response_message = ANSWERS[6][user_answer]
|
||||||
|
|
||||||
|
# Сохраняем ответ
|
||||||
|
answer_code = 1 if user_answer == "Ответ 61" else 2
|
||||||
|
data = await state.get_data()
|
||||||
|
answers = data.get("answers", {})
|
||||||
|
answers["q6"] = answer_code
|
||||||
|
await state.update_data(answers=answers)
|
||||||
|
|
||||||
|
# Отправляем финальное сообщение
|
||||||
|
await message.answer(response_message)
|
||||||
|
await finish_knowledge_session(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
# Обработчики для некорректных ответов
|
||||||
|
@knowledge_router.message(KnowledgeStates.question1)
|
||||||
|
async def process_incorrect_answer1(message: Message):
|
||||||
|
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
|
||||||
|
await message.answer(QUESTIONS[1])
|
||||||
|
|
||||||
|
|
||||||
|
@knowledge_router.message(KnowledgeStates.question2)
|
||||||
|
async def process_incorrect_answer2(message: Message):
|
||||||
|
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
|
||||||
|
await message.answer(QUESTIONS[2])
|
||||||
|
|
||||||
|
|
||||||
|
# Добавьте аналогичные обработчики для остальных вопросов
|
||||||
|
|
||||||
|
# Завершение сессии
|
||||||
|
async def finish_knowledge_session(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
answers = data.get("answers", {})
|
||||||
|
|
||||||
|
# Проверяем результаты
|
||||||
|
if all(answer == 2 for answer in answers.values()):
|
||||||
|
await message.answer(FINAL_MESSAGES["all_2"])
|
||||||
|
else:
|
||||||
|
await message.answer(FINAL_MESSAGES["mixed"])
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
2
bot/keyboards/__init__.py
Normal file
2
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .reply import *
|
||||||
|
from .inline import *
|
||||||
1
bot/keyboards/inline/__init__.py
Normal file
1
bot/keyboards/inline/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .decision import *
|
||||||
17
bot/keyboards/inline/decision.py
Normal file
17
bot/keyboards/inline/decision.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
def decision_keyboard(thread_id: int, kind: str) -> InlineKeyboardMarkup:
|
||||||
|
"""
|
||||||
|
Получение клавиатуры Принятия\Отклонить.
|
||||||
|
|
||||||
|
:param thread_id: Айди действия.
|
||||||
|
:param kind: Вид для клавиатуры.
|
||||||
|
:return: Инлайн-клавиатуру (Принять, Отклонить).
|
||||||
|
"""
|
||||||
|
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||||
|
ikb.row(
|
||||||
|
InlineKeyboardButton(text="✅ Принять", callback_data=f"{kind}:accept:{thread_id}"),
|
||||||
|
InlineKeyboardButton(text="❌ Отклонить", callback_data=f"{kind}:reject:{thread_id}")
|
||||||
|
)
|
||||||
|
return ikb.as_markup()
|
||||||
0
bot/keyboards/reply/__init__.py
Normal file
0
bot/keyboards/reply/__init__.py
Normal file
48
bot/middlewares/__init__.py
Normal file
48
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
|
||||||
|
from configs import ImportantID
|
||||||
|
from .logging_mdw import LoggingMiddleware
|
||||||
|
from .msg_mdw import MessageCounterMiddleware
|
||||||
|
from .spam_mdw import RateLimitMiddleware
|
||||||
|
from .subscription_mdw import SubscriptionMiddleware
|
||||||
|
from .error_mdw import ErrorHandlingMiddleware
|
||||||
|
from .time_mdw import TimingMiddleware
|
||||||
|
|
||||||
|
# Настройки экспорта
|
||||||
|
__all__ = (
|
||||||
|
"LoggingMiddleware",
|
||||||
|
"SubscriptionMiddleware",
|
||||||
|
"RateLimitMiddleware",
|
||||||
|
"ErrorHandlingMiddleware",
|
||||||
|
"TimingMiddleware",
|
||||||
|
"MessageCounterMiddleware",
|
||||||
|
"setup_middlewares",)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_middlewares(dp: Dispatcher, bot: Bot, channel_ids: list[int | str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Регистрирует все middleware в диспетчере.
|
||||||
|
"""
|
||||||
|
channel_ids: list = channel_ids or []
|
||||||
|
|
||||||
|
# Middleware для ВСЕХ событий (update level)
|
||||||
|
middlewares_updates: list = [
|
||||||
|
TimingMiddleware(), # Замер времени
|
||||||
|
LoggingMiddleware(), # Логирование
|
||||||
|
ErrorHandlingMiddleware(admin_ids=ImportantID.ADMIN_ID), # Обработка ошибок
|
||||||
|
]
|
||||||
|
|
||||||
|
# Middleware только для СООБЩЕНИЙ (message level)
|
||||||
|
middlewares_msg: list = [
|
||||||
|
#RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
|
||||||
|
#SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
|
||||||
|
MessageCounterMiddleware(), # Подсчет сообщений
|
||||||
|
]
|
||||||
|
|
||||||
|
# Регистрируем middleware для всех событий
|
||||||
|
for middleware in middlewares_updates:
|
||||||
|
dp.update.middleware(middleware)
|
||||||
|
|
||||||
|
# Регистрируем middleware только для сообщений
|
||||||
|
for middleware in middlewares_msg:
|
||||||
|
dp.message.middleware(middleware)
|
||||||
201
bot/middlewares/error_mdw.py
Normal file
201
bot/middlewares/error_mdw.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
from typing import Callable, Awaitable, Any, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||||
|
|
||||||
|
from middleware.loggers import loggers # ваш логгер
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorHandlingMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для глобальной обработки ошибок в хендлерах.
|
||||||
|
|
||||||
|
Зачем нужен:
|
||||||
|
- Централизованная обработка исключений
|
||||||
|
- Уведомление администраторов об ошибках
|
||||||
|
- Graceful degradation при сбоях
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, admin_ids: list[int]):
|
||||||
|
"""
|
||||||
|
Инициализация middleware обработки ошибок.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
admin_ids: Список ID администраторов для уведомлений
|
||||||
|
"""
|
||||||
|
self.admin_ids = admin_ids
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Перехватывает и обрабатывает ошибки в хендлерах.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Получаем информацию о пользователе безопасным способом
|
||||||
|
user_str = self._extract_user_info(event)
|
||||||
|
|
||||||
|
# Логируем ошибку
|
||||||
|
error_message = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
|
||||||
|
|
||||||
|
loggers.error(
|
||||||
|
text=error_message,
|
||||||
|
log_type="HANDLER_ERROR",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Уведомляем администраторов
|
||||||
|
await self._notify_admins(error_message, event, user_str)
|
||||||
|
|
||||||
|
# Отправляем пользователю сообщение об ошибке
|
||||||
|
await self._send_error_message(event, user_str)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_user_info(event: TelegramObject) -> str:
|
||||||
|
"""
|
||||||
|
Безопасно извлекает информацию о пользователе из события.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Объект события
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Строка с идентификатором пользователя
|
||||||
|
"""
|
||||||
|
user_str = "@System"
|
||||||
|
|
||||||
|
# Для Message и CallbackQuery
|
||||||
|
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
|
||||||
|
user = event.from_user
|
||||||
|
user_str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Для Update (который содержит message или callback_query)
|
||||||
|
elif isinstance(event, Update):
|
||||||
|
# Пытаемся найти пользователя в различных полях Update
|
||||||
|
user_object = None
|
||||||
|
if event.message and event.message.from_user:
|
||||||
|
user_object = event.message.from_user
|
||||||
|
elif event.edited_message and event.edited_message.from_user:
|
||||||
|
user_object = event.edited_message.from_user
|
||||||
|
elif event.callback_query and event.callback_query.from_user:
|
||||||
|
user_object = event.callback_query.from_user
|
||||||
|
elif event.channel_post and event.channel_post.from_user:
|
||||||
|
user_object = event.channel_post.from_user
|
||||||
|
elif event.edited_channel_post and event.edited_channel_post.from_user:
|
||||||
|
user_object = event.edited_channel_post.from_user
|
||||||
|
|
||||||
|
if user_object:
|
||||||
|
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
|
||||||
|
|
||||||
|
return user_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_event_text(event: TelegramObject) -> str:
|
||||||
|
"""
|
||||||
|
Безопасно извлекает текст из события.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Объект события
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Текст события или пустая строка
|
||||||
|
"""
|
||||||
|
event_text = ""
|
||||||
|
|
||||||
|
# Для Message
|
||||||
|
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
|
||||||
|
event_text = event.text
|
||||||
|
# Для CallbackQuery
|
||||||
|
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
|
||||||
|
event_text = f"callback: {event.data}"
|
||||||
|
# Для Update
|
||||||
|
elif isinstance(event, Update):
|
||||||
|
if event.message and event.message.text:
|
||||||
|
event_text = event.message.text
|
||||||
|
elif event.callback_query and event.callback_query.data:
|
||||||
|
event_text = f"callback: {event.callback_query.data}"
|
||||||
|
elif event.edited_message and event.edited_message.text:
|
||||||
|
event_text = event.edited_message.text
|
||||||
|
|
||||||
|
return event_text[:100] + "..." if len(event_text) > 100 else event_text
|
||||||
|
|
||||||
|
async def _notify_admins(
|
||||||
|
self,
|
||||||
|
error_message: str,
|
||||||
|
event: TelegramObject,
|
||||||
|
user_str: str
|
||||||
|
) -> None:
|
||||||
|
"""Уведомляет администраторов об ошибке."""
|
||||||
|
from aiogram import Bot
|
||||||
|
bot: Bot = event.bot if hasattr(event, 'bot') else None
|
||||||
|
|
||||||
|
if bot:
|
||||||
|
for admin_id in self.admin_ids:
|
||||||
|
try:
|
||||||
|
event_info = f"Событие: {type(event).__name__}"
|
||||||
|
event_text = self._extract_event_text(event)
|
||||||
|
if event_text:
|
||||||
|
event_info += f", текст: {event_text}"
|
||||||
|
|
||||||
|
full_message = (
|
||||||
|
f"🚨 Ошибка в боте:\n\n"
|
||||||
|
f"Пользователь: {user_str}\n"
|
||||||
|
f"Ошибка: {error_message}\n"
|
||||||
|
f"{event_info}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await bot.send_message(admin_id, full_message)
|
||||||
|
|
||||||
|
loggers.info(
|
||||||
|
text=f"Администратор {admin_id} уведомлен об ошибке",
|
||||||
|
log_type="ADMIN_NOTIFIED",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
loggers.error(
|
||||||
|
text=f"Не удалось уведомить админа {admin_id}: {e}",
|
||||||
|
log_type="ADMIN_NOTIFY_ERROR",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _send_error_message(
|
||||||
|
event: TelegramObject,
|
||||||
|
user_str: str
|
||||||
|
) -> None:
|
||||||
|
"""Отправляет пользователю сообщение об ошибке."""
|
||||||
|
error_text = (
|
||||||
|
"⚠️ Произошла непредвиденная ошибка. "
|
||||||
|
"Разработчики уже уведомлены и работают над исправлением.\n\n"
|
||||||
|
"Попробуйте повторить действие позже или нажмите /start"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(event, Message):
|
||||||
|
await event.answer(error_text)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
await event.message.answer(error_text)
|
||||||
|
await event.answer()
|
||||||
|
elif isinstance(event, Update) and event.message:
|
||||||
|
await event.message.answer(error_text)
|
||||||
|
|
||||||
|
loggers.info(
|
||||||
|
text="Пользователю отправлено сообщение об ошибке",
|
||||||
|
log_type="ERROR_MESSAGE_SENT",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
loggers.error(
|
||||||
|
text=f"Не удалось отправить сообщение об ошибке: {e}",
|
||||||
|
log_type="ERROR_MESSAGE_FAILED",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
271
bot/middlewares/logging_mdw.py
Normal file
271
bot/middlewares/logging_mdw.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple, Set
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject, Update, Message, CallbackQuery, MaybeInaccessibleMessageUnion, User
|
||||||
|
|
||||||
|
from bot.utils import type_msg
|
||||||
|
from middleware.loggers import loggers # ваш глобальный логгер
|
||||||
|
from configs import BotSettings, COMMANDS # импортируем настройки и команды
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для логирования апдейтов с определением типа события,
|
||||||
|
пользователя и добавлением префикса проекта к типу лога.
|
||||||
|
|
||||||
|
Автоматически добавляет префикс проекта (например, 'PRIMO-') к типам логов:
|
||||||
|
- PRIMO-UPDATE: общий апдейт без определенного типа
|
||||||
|
- PRIMO-MSG: текстовое сообщение от пользователя
|
||||||
|
- PRIMO-CMD: команда (сообщение, начинающееся с любого префикса)
|
||||||
|
- PRIMO-CBD: callback query от инлайн-кнопок
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Префикс проекта для логов
|
||||||
|
PROJECT_PREFIX: str = "PRIMO"
|
||||||
|
|
||||||
|
# Кэш для всех команд из COMMANDS
|
||||||
|
_all_commands: Optional[Set[str]] = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# Предварительно загружаем все команды
|
||||||
|
self._load_all_commands()
|
||||||
|
|
||||||
|
def _load_all_commands(self) -> None:
|
||||||
|
"""Загружает все команды из COMMANDS в множество для быстрого поиска."""
|
||||||
|
if self._all_commands is None:
|
||||||
|
self._all_commands = set()
|
||||||
|
for command_list in COMMANDS.values():
|
||||||
|
self._all_commands.update(command_list)
|
||||||
|
|
||||||
|
def _is_command(self, text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли текст командой с любым префиксом.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если это команда, False если нет
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем все префиксы из BotSettings
|
||||||
|
for prefix in BotSettings.PREFIX:
|
||||||
|
if text.startswith(prefix):
|
||||||
|
# Извлекаем команду без префикса
|
||||||
|
command_without_prefix = text[len(prefix):].strip()
|
||||||
|
# Проверяем, есть ли такая команда в нашем списке
|
||||||
|
if command_without_prefix in self._all_commands:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Также проверяем команды с префиксом / (стандартные)
|
||||||
|
if text.startswith('/'):
|
||||||
|
command_without_slash = text[1:].strip()
|
||||||
|
if command_without_slash in self._all_commands:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_command_name(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Извлекает название команды из текста.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст команды с префиксом
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Название команды без префикса
|
||||||
|
"""
|
||||||
|
for prefix in BotSettings.PREFIX:
|
||||||
|
if text.startswith(prefix):
|
||||||
|
return text[len(prefix):].strip()
|
||||||
|
|
||||||
|
if text.startswith('/'):
|
||||||
|
return text[1:].strip()
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Обрабатывает входящее событие, определяет его тип, логирует с префиксом проекта
|
||||||
|
и передает следующему обработчику.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handler: Следующий обработчик в цепочке middleware
|
||||||
|
event: Входящее событие для обработки (Update, Message, CallbackQuery)
|
||||||
|
data: Словарь с контекстными данными FSM
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Результат выполнения следующего обработчика
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Любое исключение, возникшее при обработке хендлером
|
||||||
|
"""
|
||||||
|
# Определяем тип события и информацию для логирования
|
||||||
|
log_type: str
|
||||||
|
log_text: str
|
||||||
|
message_obj: Optional[Message]
|
||||||
|
|
||||||
|
log_type, log_text, message_obj = self._determine_event_type(event)
|
||||||
|
|
||||||
|
# Добавляем префикс проекта к типу лога
|
||||||
|
prefixed_log_type: str = f"{log_type}"
|
||||||
|
|
||||||
|
# Определяем информацию о пользователе
|
||||||
|
user_str: str = self._extract_user_info(event, message_obj)
|
||||||
|
|
||||||
|
# Логируем получение события с префиксом проекта
|
||||||
|
loggers.info(
|
||||||
|
text=log_text,
|
||||||
|
log_type=prefixed_log_type,
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Передаем событие следующему обработчику
|
||||||
|
result: Any = await handler(event, data)
|
||||||
|
|
||||||
|
# Логируем успешное выполнение для команд
|
||||||
|
if log_type == "CMD":
|
||||||
|
loggers.info(
|
||||||
|
text=f"[SUCCESS] команда обработана",
|
||||||
|
log_type=prefixed_log_type,
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Логируем ошибку при обработке с префиксом проекта
|
||||||
|
loggers.error(
|
||||||
|
text=f"Ошибка обработки: {str(e)}",
|
||||||
|
log_type=prefixed_log_type,
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _determine_event_type(
|
||||||
|
self,
|
||||||
|
event: TelegramObject
|
||||||
|
) -> Tuple[str, str, Optional[Message]]:
|
||||||
|
"""
|
||||||
|
Определяет тип события и извлекает информацию для логирования.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Объект события для анализа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Кортеж из (тип_лога, текст_лога, объект_сообщения)
|
||||||
|
"""
|
||||||
|
log_type: str = "UPDATE"
|
||||||
|
log_text: str = f"Получен апдейт: {type(event).__name__}"
|
||||||
|
message_obj: Optional[Message] = None
|
||||||
|
|
||||||
|
# Обработка Update объектов (основной тип в middleware)
|
||||||
|
if isinstance(event, Update):
|
||||||
|
# Пытаемся найти сообщение в различных полях Update
|
||||||
|
message_obj = (
|
||||||
|
event.message or
|
||||||
|
event.edited_message or
|
||||||
|
event.channel_post or
|
||||||
|
event.edited_channel_post
|
||||||
|
)
|
||||||
|
|
||||||
|
if message_obj and message_obj.text:
|
||||||
|
if self._is_command(message_obj.text):
|
||||||
|
log_type: str = "CMD"
|
||||||
|
log_text: str = f"использовал команду '{message_obj.text}'"
|
||||||
|
else:
|
||||||
|
log_type: str = "MSG"
|
||||||
|
log_text: str = f"получено сообщение: {message_obj.text!r}"
|
||||||
|
elif message_obj:
|
||||||
|
# Не текстовое сообщение (фото, видео и т.д.)
|
||||||
|
log_type: str = "MSG"
|
||||||
|
log_text: str = f"получено сообщение: '{type_msg(message_obj)}'"
|
||||||
|
elif event.callback_query:
|
||||||
|
# Обработка callback query
|
||||||
|
callback: CallbackQuery = event.callback_query
|
||||||
|
log_type: str = "CBD"
|
||||||
|
log_text: str = f"получен callback: {callback.data!r}"
|
||||||
|
if callback.message:
|
||||||
|
message_obj: Optional[MaybeInaccessibleMessageUnion] = callback.message
|
||||||
|
|
||||||
|
# Прямая обработка Message (если мидлварь зарегистрирован на messages)
|
||||||
|
elif isinstance(event, Message):
|
||||||
|
message_obj = event
|
||||||
|
if event.text and self._is_command(event.text):
|
||||||
|
log_type: str = "CMD"
|
||||||
|
log_text: str = f"использовал команду '{event.text}'"
|
||||||
|
elif event.text:
|
||||||
|
log_type: str = "MSG"
|
||||||
|
log_text: str = f"получено сообщение: {event.text!r}"
|
||||||
|
else:
|
||||||
|
log_type: str = "MSG"
|
||||||
|
log_text: str = f"получено сообщение типа: {event.content_type}"
|
||||||
|
|
||||||
|
# Прямая обработка CallbackQuery (если мидлварь зарегистрирован на callbacks)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
log_type: str = "CBD"
|
||||||
|
log_text: str = f"получен callback: {event.data!r}"
|
||||||
|
if event.message:
|
||||||
|
message_obj = event.message
|
||||||
|
|
||||||
|
return log_type, log_text, message_obj
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_user_info(
|
||||||
|
event: TelegramObject,
|
||||||
|
message: Optional[Message] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Извлекает информацию о пользователе из события.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Объект события (Update, Message или CallbackQuery)
|
||||||
|
message: Объект Message (если уже определен)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Строка с идентификатором пользователя в формате '@username' или 'id<user_id>'
|
||||||
|
"""
|
||||||
|
user_str: str = "@System"
|
||||||
|
|
||||||
|
# Для CallbackQuery извлекаем пользователя из самого callback'а
|
||||||
|
if isinstance(event, CallbackQuery) and hasattr(event, 'from_user') and event.from_user:
|
||||||
|
user = event.from_user
|
||||||
|
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Для Message извлекаем пользователя из сообщения
|
||||||
|
elif isinstance(event, Message) and hasattr(event, 'from_user') and event.from_user:
|
||||||
|
user = event.from_user
|
||||||
|
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Для Update с callback_query
|
||||||
|
elif (isinstance(event, Update) and
|
||||||
|
event.callback_query and
|
||||||
|
hasattr(event.callback_query, 'from_user') and
|
||||||
|
event.callback_query.from_user):
|
||||||
|
user = event.callback_query.from_user
|
||||||
|
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Для Update с сообщением
|
||||||
|
elif (isinstance(event, Update) and
|
||||||
|
(event.message or event.edited_message) and
|
||||||
|
hasattr(event.message or event.edited_message, 'from_user')):
|
||||||
|
msg = event.message or event.edited_message
|
||||||
|
if msg and msg.from_user:
|
||||||
|
user: Optional[User] = msg.from_user
|
||||||
|
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Если передан message объект
|
||||||
|
elif message and hasattr(message, 'from_user') and message.from_user:
|
||||||
|
user: Optional[User] = message.from_user
|
||||||
|
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
return user_str
|
||||||
55
bot/middlewares/msg_mdw.py
Normal file
55
bot/middlewares/msg_mdw.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Callable, Dict, Any, Awaitable
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.enums import ChatType
|
||||||
|
from aiogram.types import Message
|
||||||
|
from database import db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MessageCounterMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для подсчёта сообщений в группах и супергруппах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: Any,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
if not isinstance(event, Message):
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
# Проверяем, что сообщение пришло из группового чата и не от бота
|
||||||
|
if (event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP) and
|
||||||
|
not event.from_user.is_bot):
|
||||||
|
try:
|
||||||
|
await self.process_group_message(event)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(msg=f"Ошибка при обработке сообщения: {e}", exc_info=True)
|
||||||
|
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def process_group_message(message: Message) -> None:
|
||||||
|
"""
|
||||||
|
Обработка сообщения из группового чата.
|
||||||
|
"""
|
||||||
|
user_id = message.from_user.id
|
||||||
|
message_text = message.text or message.caption or ""
|
||||||
|
|
||||||
|
# Добавляем пользователя (если его ещё нет)
|
||||||
|
await db.add_user(
|
||||||
|
user_id=user_id,
|
||||||
|
username=message.from_user.username,
|
||||||
|
full_name=message.from_user.full_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем сообщение
|
||||||
|
await db.add_message(
|
||||||
|
user_id=user_id,
|
||||||
|
message_text=message_text,
|
||||||
|
created_at=message.date,
|
||||||
|
)
|
||||||
|
logger.info(f"Сообщение от пользователя {user_id} сохранено в БД")
|
||||||
97
bot/middlewares/spam_mdw.py
Normal file
97
bot/middlewares/spam_mdw.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from typing import Callable, Awaitable, Any, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from middleware.loggers import loggers # ваш логгер
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для ограничения частоты запросов от пользователей (анти-спам).
|
||||||
|
|
||||||
|
Зачем нужен:
|
||||||
|
- Защита от DDoS и флуда
|
||||||
|
- Предотвращение злоупотребления ботом
|
||||||
|
- Контроль нагрузки на сервер
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rate_limit: int = 10, time_period: float = 2.0):
|
||||||
|
"""
|
||||||
|
Инициализация rate limit middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rate_limit: Максимальное количество запросов за период
|
||||||
|
time_period: Период времени в секундах
|
||||||
|
"""
|
||||||
|
self.rate_limit = rate_limit
|
||||||
|
self.time_period = time_period
|
||||||
|
self.user_calls: Dict[int, list[float]] = defaultdict(list)
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
log: bool = False,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Проверяет rate limit перед обработкой запроса.
|
||||||
|
"""
|
||||||
|
# Пропускаем не-сообщения и не-колбэки
|
||||||
|
if not isinstance(event, (Message, CallbackQuery)):
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
user_id: int = event.from_user.id
|
||||||
|
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
|
||||||
|
current_time: float = time.time()
|
||||||
|
|
||||||
|
# Очищаем старые запросы
|
||||||
|
self.user_calls[user_id] = [
|
||||||
|
call_time for call_time in self.user_calls[user_id]
|
||||||
|
if current_time - call_time < self.time_period
|
||||||
|
]
|
||||||
|
|
||||||
|
# Логируем текущее состояние rate limit
|
||||||
|
if log:
|
||||||
|
loggers.debug(
|
||||||
|
text=f"Rate limit: {len(self.user_calls[user_id])}/{self.rate_limit} за {self.time_period}сек",
|
||||||
|
log_type="RATE_LIMIT_STATUS",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем текущий лимит
|
||||||
|
if len(self.user_calls[user_id]) >= self.rate_limit:
|
||||||
|
# Логируем попытку спама
|
||||||
|
if log:
|
||||||
|
loggers.warning(
|
||||||
|
text=f"Превышен rate limit ({self.rate_limit}/{self.time_period}сек)",
|
||||||
|
log_type="RATE_LIMIT_EXCEEDED",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение о превышении лимита
|
||||||
|
if isinstance(event, Message):
|
||||||
|
await event.answer(
|
||||||
|
text="⏳ Слишком много запросов! Пожалуйста, подождите немного.",
|
||||||
|
)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
await event.answer(
|
||||||
|
text="⏳ Подождите немного перед следующим действием.",
|
||||||
|
show_alert=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Добавляем текущий запрос и продолжаем обработку
|
||||||
|
self.user_calls[user_id].append(current_time)
|
||||||
|
|
||||||
|
loggers.debug(
|
||||||
|
text=f"Запрос добавлен в rate limit",
|
||||||
|
log_type="RATE_LIMIT_ADDED",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
return await handler(event, data)
|
||||||
115
bot/middlewares/subscription_mdw.py
Normal file
115
bot/middlewares/subscription_mdw.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from typing import Callable, Awaitable, Any, Dict
|
||||||
|
from aiogram import BaseMiddleware, Bot
|
||||||
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
|
|
||||||
|
from middleware.loggers import loggers # ваш логгер
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для проверки подписки пользователя на необходимые каналы.
|
||||||
|
Блокирует обработку команд, если пользователь не подписан.
|
||||||
|
|
||||||
|
Зачем нужен:
|
||||||
|
- Автоматическая проверка подписки для всех входящих сообщений
|
||||||
|
- Единая точка управления подписками
|
||||||
|
- Предотвращение доступа к функционалу без подписки
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Bot, channel_ids: list[int | str]):
|
||||||
|
"""
|
||||||
|
Инициализация middleware проверки подписки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: Экземпляр бота
|
||||||
|
channel_ids: Список ID каналов/чатов для проверки подписки
|
||||||
|
"""
|
||||||
|
self.bot = bot
|
||||||
|
self.channel_ids = channel_ids
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Проверяет подписку пользователя перед обработкой команды.
|
||||||
|
"""
|
||||||
|
# Пропускаем не-сообщения и не-колбэки
|
||||||
|
if not isinstance(event, (Message, CallbackQuery)):
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
user_id: int = event.from_user.id
|
||||||
|
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
|
||||||
|
|
||||||
|
# Логируем начало проверки подписки
|
||||||
|
loggers.info(
|
||||||
|
text=f"Проверка подписки для пользователя",
|
||||||
|
log_type="SUBSCRIPTION_CHECK",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем подписку на все required каналы
|
||||||
|
not_subscribed_channels: list[str] = []
|
||||||
|
|
||||||
|
for channel_id in self.channel_ids:
|
||||||
|
try:
|
||||||
|
member = await self.bot.get_chat_member(
|
||||||
|
chat_id=channel_id,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
# Проверяем, что пользователь является участником
|
||||||
|
if member.status not in ['member', 'administrator', 'creator']:
|
||||||
|
not_subscribed_channels.append(str(channel_id))
|
||||||
|
|
||||||
|
except TelegramBadRequest as e:
|
||||||
|
loggers.error(
|
||||||
|
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
|
||||||
|
log_type="SUBSCRIPTION_ERROR",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Если пользователь не подписан на некоторые каналы
|
||||||
|
if not_subscribed_channels:
|
||||||
|
loggers.warning(
|
||||||
|
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
|
||||||
|
log_type="SUBSCRIPTION_FAILED",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
warning_text = (
|
||||||
|
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
|
||||||
|
"После подписки нажмите /start для продолжения."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем кнопку "Проверить подписку"
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="✅ Я подписался",
|
||||||
|
callback_data="check_subscription"
|
||||||
|
)
|
||||||
|
]]
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(event, Message):
|
||||||
|
await event.answer(warning_text, reply_markup=keyboard)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
await event.message.answer(warning_text, reply_markup=keyboard)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Логируем успешную проверку подписки
|
||||||
|
loggers.info(
|
||||||
|
text="Пользователь подписан на все required каналы",
|
||||||
|
log_type="SUBSCRIPTION_SUCCESS",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Если подписка есть, продолжаем обработку
|
||||||
|
return await handler(event, data)
|
||||||
82
bot/middlewares/time_mdw.py
Normal file
82
bot/middlewares/time_mdw.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from typing import Callable, Awaitable, Any, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from middleware.loggers import loggers # ваш логгер
|
||||||
|
|
||||||
|
|
||||||
|
class TimingMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для измерения времени выполнения хендлеров.
|
||||||
|
|
||||||
|
Зачем нужен:
|
||||||
|
- Мониторинг производительности хендлеров
|
||||||
|
- Выявление медленных запросов
|
||||||
|
- Оптимизация кода бота
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
perm: str = None,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Измеряет время выполнения хендлера.
|
||||||
|
"""
|
||||||
|
start_time: float = time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await handler(event, data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
finally:
|
||||||
|
execution_time: float = time() - start_time
|
||||||
|
|
||||||
|
# Получаем информацию о пользователе безопасным способом
|
||||||
|
user_str: str = "@System"
|
||||||
|
|
||||||
|
# Для Message и CallbackQuery
|
||||||
|
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
|
||||||
|
user = event.from_user
|
||||||
|
user_str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
# Для Update (который содержит message или callback_query)
|
||||||
|
elif isinstance(event, Update):
|
||||||
|
# Пытаемся найти пользователя в различных полях Update
|
||||||
|
user_object = None
|
||||||
|
if event.message and event.message.from_user:
|
||||||
|
user_object = event.message.from_user
|
||||||
|
elif event.edited_message and event.edited_message.from_user:
|
||||||
|
user_object = event.edited_message.from_user
|
||||||
|
elif event.callback_query and event.callback_query.from_user:
|
||||||
|
user_object = event.callback_query.from_user
|
||||||
|
elif event.channel_post and event.channel_post.from_user:
|
||||||
|
user_object = event.channel_post.from_user
|
||||||
|
elif event.edited_channel_post and event.edited_channel_post.from_user:
|
||||||
|
user_object = event.edited_channel_post.from_user
|
||||||
|
|
||||||
|
if user_object:
|
||||||
|
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
|
||||||
|
|
||||||
|
# Логируем время выполнения
|
||||||
|
if execution_time > 1.0 and perm: # Медленные запросы
|
||||||
|
loggers.warning(
|
||||||
|
text=f"Медленный хендлер: {execution_time:.2f}сек",
|
||||||
|
log_type="SLOW_HANDLER",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
elif execution_time > 0.5 and perm == "medium": # Средние запросы
|
||||||
|
loggers.info(
|
||||||
|
text=f"Среднее время выполнения: {execution_time:.3f}сек",
|
||||||
|
log_type="HANDLER_TIMING",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
elif perm == "fast": # Быстрые запросы
|
||||||
|
loggers.debug(
|
||||||
|
text=f"Быстрое выполнение: {execution_time:.3f}сек",
|
||||||
|
log_type="HANDLER_TIMING_FAST",
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
0
bot/states/__init__.py
Normal file
0
bot/states/__init__.py
Normal file
5
bot/states/anketa_states.py
Normal file
5
bot/states/anketa_states.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# bot/states/form.py
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
class StartForm(StatesGroup):
|
||||||
|
waiting_for_application: State = State()
|
||||||
8
bot/states/new_states.py
Normal file
8
bot/states/new_states.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# bot/states/new_states.py
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
class NewStates(StatesGroup):
|
||||||
|
role: State = State()
|
||||||
|
sorol: State = State()
|
||||||
|
code_phrase: State = State()
|
||||||
|
rules: State = State()
|
||||||
1
bot/templates/__init__.py
Normal file
1
bot/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .message_callback import *
|
||||||
77
bot/templates/message_callback.py
Normal file
77
bot/templates/message_callback.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from aiogram.types import FSInputFile, CallbackQuery, Message, ReplyKeyboardMarkup, InlineKeyboardMarkup
|
||||||
|
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ('msg', 'msg_photo')
|
||||||
|
|
||||||
|
|
||||||
|
async def msg(message: Message | CallbackQuery,
|
||||||
|
text: str = "Сообщение отправлено!",
|
||||||
|
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
|
||||||
|
"""
|
||||||
|
Шаблон для ответа на сообщение текстом.
|
||||||
|
:param message: Объект сообщения или callback-запроса.
|
||||||
|
:param text: Текст отправного сообщения от бота.
|
||||||
|
:param markup: Кнопки сообщения (инлайн или реплай).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Преобразуем клавиатуру
|
||||||
|
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
|
||||||
|
if markup:
|
||||||
|
if isinstance(markup, InlineKeyboardBuilder):
|
||||||
|
reply_markup: InlineKeyboardMarkup = markup.as_markup()
|
||||||
|
elif isinstance(markup, ReplyKeyboardBuilder):
|
||||||
|
reply_markup: ReplyKeyboardMarkup = markup.as_markup(resize_keyboard=True)
|
||||||
|
|
||||||
|
# Обработчик ответа на сообщение
|
||||||
|
if isinstance(message, Message):
|
||||||
|
await message.reply(
|
||||||
|
text=text,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
# Обработчик ответа на callback
|
||||||
|
else:
|
||||||
|
await message.message.reply(
|
||||||
|
text=text,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def msg_photo(
|
||||||
|
message: Message | CallbackQuery,
|
||||||
|
text: str = "Сообщение отправлено!",
|
||||||
|
file: str = "assets/default.jpg",
|
||||||
|
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
|
||||||
|
"""
|
||||||
|
Шаблон для ответа на сообщение фотографией.
|
||||||
|
:param message: Объект сообщения или callback-запроса.
|
||||||
|
:param file: Путь к фотографии для ответа.
|
||||||
|
:param text: Подпись к фото.
|
||||||
|
:param markup: Кнопки сообщения (инлайн или реплай).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Преобразуем клавиатуру
|
||||||
|
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
|
||||||
|
if markup:
|
||||||
|
if isinstance(markup, InlineKeyboardBuilder):
|
||||||
|
reply_markup = markup.as_markup()
|
||||||
|
elif isinstance(markup, ReplyKeyboardBuilder):
|
||||||
|
reply_markup = markup.as_markup(resize_keyboard=True)
|
||||||
|
|
||||||
|
# Обработчик ответа на сообщение
|
||||||
|
if isinstance(message, Message):
|
||||||
|
await message.reply_photo(
|
||||||
|
photo=FSInputFile(file),
|
||||||
|
caption=text,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработчик ответа на callback
|
||||||
|
else:
|
||||||
|
await message.message.reply_photo(
|
||||||
|
photo=FSInputFile(file),
|
||||||
|
caption=text,
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
5
bot/utils/__init__.py
Normal file
5
bot/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .interesting_facts import *
|
||||||
|
from .usernames import *
|
||||||
|
from .pagination import *
|
||||||
|
from .type_message import *
|
||||||
|
from .argument import *
|
||||||
59
bot/utils/argument.py
Normal file
59
bot/utils/argument.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from configs import BotSettings
|
||||||
|
|
||||||
|
__all__ = ("is_command", "find_argument")
|
||||||
|
|
||||||
|
|
||||||
|
def is_command(message: Optional[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли сообщение командой.
|
||||||
|
|
||||||
|
Сообщение считается командой, если:
|
||||||
|
1. Оно не пустое;
|
||||||
|
2. Начинается с префикса команды, указанного в настройках.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Optional[str]): Входное сообщение.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, если сообщение является командой, иначе False.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
>>> is_command("/start")
|
||||||
|
True
|
||||||
|
>>> is_command("hello")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
if not message:
|
||||||
|
return False
|
||||||
|
return message.strip().startswith(BotSettings.PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def find_argument(message: Optional[str]) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Извлекает аргумент команды из сообщения.
|
||||||
|
|
||||||
|
Аргументом считается текст после первой команды и пробела.
|
||||||
|
Если аргумента нет — возвращает None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Optional[str]): Входное сообщение.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Аргумент команды или None, если его нет.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
>>> find_argument("/start referrer")
|
||||||
|
'referrer'
|
||||||
|
>>> find_argument("/start")
|
||||||
|
None
|
||||||
|
>>> find_argument("hello")
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if not is_command(message):
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts = message.strip().split(maxsplit=1)
|
||||||
|
return parts[1] if len(parts) > 1 else None
|
||||||
54
bot/utils/interesting_facts.py
Normal file
54
bot/utils/interesting_facts.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from random import choice
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from configs.config import Lists
|
||||||
|
|
||||||
|
__all__ = ("interesting_fact", "get_best_response",)
|
||||||
|
|
||||||
|
def interesting_fact(mode: str = "факт", lists: Optional[list[str]] = None) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
|
||||||
|
|
||||||
|
:param mode: строка, определяющая тип контента ("факт", "анекдот", "цитата").
|
||||||
|
:param lists: необязательный список строк, из которого можно выбирать вручную.
|
||||||
|
:return: случайный элемент из соответствующего списка.
|
||||||
|
"""
|
||||||
|
if lists is not None:
|
||||||
|
return choice(lists)
|
||||||
|
|
||||||
|
mode = mode.lower()
|
||||||
|
|
||||||
|
if mode == "анекдот":
|
||||||
|
source: list[str] = Lists.jokes
|
||||||
|
elif mode == "цитата":
|
||||||
|
source: list[str] = Lists.quotes
|
||||||
|
else:
|
||||||
|
source: list[str] = Lists.facts
|
||||||
|
|
||||||
|
return choice(source)
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_response(
|
||||||
|
user_text: str,
|
||||||
|
responses: Dict[str, Dict[str, List[str]]],
|
||||||
|
random_phrases: List[str],
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Подбирает наиболее подходящий ответ на сообщение пользователя.
|
||||||
|
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
|
||||||
|
|
||||||
|
:param user_text: текст сообщения пользователя
|
||||||
|
:param responses: словарь с ключевыми словами и ответами
|
||||||
|
:param random_phrases: список случайных фраз, если совпадений нет
|
||||||
|
:return: строка с ответом
|
||||||
|
"""
|
||||||
|
normalized_text: str = user_text.lower()
|
||||||
|
|
||||||
|
# Перебор ключевых слов в словаре
|
||||||
|
for _, data in responses.items():
|
||||||
|
for keyword in data["keywords"]:
|
||||||
|
if keyword in normalized_text:
|
||||||
|
return choice(data["answers"])
|
||||||
|
|
||||||
|
# Если совпадений нет — выдаём случайную фразу
|
||||||
|
return choice(random_phrases)
|
||||||
28
bot/utils/pagination.py
Normal file
28
bot/utils/pagination.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from aiogram.types import InlineKeyboardButton
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ('pagination_btn',)
|
||||||
|
|
||||||
|
def pagination_btn(action: str,
|
||||||
|
page: int = 0,
|
||||||
|
total_posts: int = 0,
|
||||||
|
bt_page: int = 5) -> list[InlineKeyboardButton]:
|
||||||
|
"""
|
||||||
|
Создает кнопки для пагинации.
|
||||||
|
|
||||||
|
:param action: Действие в котором нужна пангинация.
|
||||||
|
:param page: Номер начальной страницы, по умолчанию 0.
|
||||||
|
:param total_posts: Количество постов.
|
||||||
|
:param bt_page: Количество кнопок на одной странице.
|
||||||
|
:return: Готовый лист списка инлайн-кнопок.
|
||||||
|
"""
|
||||||
|
navigation_buttons: list[InlineKeyboardButton] = []
|
||||||
|
if page > 0:
|
||||||
|
navigation_buttons.append(InlineKeyboardButton(
|
||||||
|
text="←", callback_data=f"{action}_page_{page - 1}"
|
||||||
|
))
|
||||||
|
if (page + 1) * bt_page < total_posts:
|
||||||
|
navigation_buttons.append(InlineKeyboardButton(
|
||||||
|
text="→", callback_data=f"{action}_page_{page + 1}"
|
||||||
|
))
|
||||||
|
return navigation_buttons
|
||||||
18
bot/utils/random_lists.py
Normal file
18
bot/utils/random_lists.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from random import choice
|
||||||
|
|
||||||
|
def get_best_response(user_text: str) -> str:
|
||||||
|
"""
|
||||||
|
Подбирает наиболее подходящий ответ на сообщение пользователя.
|
||||||
|
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
|
||||||
|
|
||||||
|
:param user_text: текст сообщения пользователя
|
||||||
|
:return: строка с ответом
|
||||||
|
"""
|
||||||
|
normalized_text: str = user_text.lower()
|
||||||
|
|
||||||
|
for _, data in RESPONSES.items():
|
||||||
|
for keyword in data["keywords"]:
|
||||||
|
if keyword in normalized_text:
|
||||||
|
return choice(data["answers"])
|
||||||
|
|
||||||
|
return choice(RANDOM_PHRASES)
|
||||||
85
bot/utils/type_message.py
Normal file
85
bot/utils/type_message.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("CHAT_TYPES", "CONTENT_TYPE_RU", "type_chat", "type_msg")
|
||||||
|
|
||||||
|
|
||||||
|
# Словарь сопоставлений "chat_type -> русское название"
|
||||||
|
CHAT_TYPES: Final[dict[str, str]] = {
|
||||||
|
"private": "Личный",
|
||||||
|
"group": "Группа",
|
||||||
|
"supergroup": "Группа",
|
||||||
|
"channel": "Канал",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Словарь сопоставлений "content_type -> русское название"
|
||||||
|
CONTENT_TYPE_RU: Final[dict[str, str]] = {
|
||||||
|
"text": "Текст",
|
||||||
|
"animation": "Гиф",
|
||||||
|
"audio": "Аудио",
|
||||||
|
"document": "Файл",
|
||||||
|
"photo": "Фото",
|
||||||
|
"sticker": "Стикер",
|
||||||
|
"video": "Видео",
|
||||||
|
"video_note": "Видеосообщение",
|
||||||
|
"voice": "Голосовое сообщение",
|
||||||
|
"contact": "Контакт",
|
||||||
|
"dice": "Кубик",
|
||||||
|
"game": "Игра",
|
||||||
|
"poll": "Опрос",
|
||||||
|
"venue": "Место",
|
||||||
|
"location": "Локация",
|
||||||
|
"new_chat_members": "Новые участники чата",
|
||||||
|
"left_chat_member": "Участник вышел",
|
||||||
|
"new_chat_title": "Новое название чата",
|
||||||
|
"new_chat_photo": "Новая картинка чата",
|
||||||
|
"delete_chat_photo": "Удалена картинка чата",
|
||||||
|
"group_chat_created": "Создана группа",
|
||||||
|
"supergroup_chat_created": "Создана супергруппа",
|
||||||
|
"channel_chat_created": "Создан канал",
|
||||||
|
"message_auto_delete_timer_changed": "Изменён автоудалитель",
|
||||||
|
"migrate_to_chat_id": "Группа → супергруппа",
|
||||||
|
"migrate_from_chat_id": "Супергруппа → группа",
|
||||||
|
"pinned_message": "Закреплённое сообщение",
|
||||||
|
"invoice": "Счёт",
|
||||||
|
"successful_payment": "Успешный платёж",
|
||||||
|
"connected_website": "Подключённый сайт",
|
||||||
|
"passport_data": "Данные Telegram Passport",
|
||||||
|
"proximity_alert_triggered": "Алерт о приближении",
|
||||||
|
"video_chat_scheduled": "Запланированный видеочат",
|
||||||
|
"video_chat_started": "Видеочат начался",
|
||||||
|
"video_chat_ended": "Видеочат завершён",
|
||||||
|
"video_chat_participants_invited": "Приглашены участники видеочата",
|
||||||
|
"web_app_data": "Данные из веб-приложения",
|
||||||
|
"forum_topic_created": "Создана тема форума",
|
||||||
|
"forum_topic_edited": "Изменена тема форума",
|
||||||
|
"forum_topic_closed": "Тема форума закрыта",
|
||||||
|
"forum_topic_reopened": "Тема форума открыта",
|
||||||
|
"general_forum_topic_hidden": "Общая тема скрыта",
|
||||||
|
"general_forum_topic_unhidden": "Общая тема снова отображается",
|
||||||
|
"giveaway_created": "Создан розыгрыш",
|
||||||
|
"giveaway": "Розыгрыш",
|
||||||
|
"giveaway_completed": "Розыгрыш завершён",
|
||||||
|
"message_reaction": "Реакция на сообщение",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def type_msg(message: Message) -> str:
|
||||||
|
"""
|
||||||
|
Определяет и возвращает тип сообщения на русском языке.
|
||||||
|
|
||||||
|
:param message: объект Message от aiogram
|
||||||
|
:return: строка с типом сообщения
|
||||||
|
"""
|
||||||
|
return CONTENT_TYPE_RU.get(message.content_type, f"Неизвестный тип ({message.content_type})")
|
||||||
|
|
||||||
|
def type_chat(message: Message) -> str:
|
||||||
|
"""
|
||||||
|
Преобразует информацию о чате в его тип на русском языке.
|
||||||
|
|
||||||
|
:param message: Объект сообщения из aiogram, содержащий информацию о чате.
|
||||||
|
:return: Тип чата строкой.
|
||||||
|
"""
|
||||||
|
return CHAT_TYPES.get(message.chat.type, f"Неизвестный тип чата {message.chat.type}")
|
||||||
21
bot/utils/usernames.py
Normal file
21
bot/utils/usernames.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ('username', )
|
||||||
|
|
||||||
|
# Функция получения юзера или ID пользователя
|
||||||
|
def username(message: Message) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан.
|
||||||
|
|
||||||
|
:param message: Объект сообщения из aiogram.
|
||||||
|
:return: Строка с юзернеймом пользователя или его ID.
|
||||||
|
:raises ValueError: Если в сообщении отсутствует информация о пользователе.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if message.from_user:
|
||||||
|
return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}"
|
||||||
|
raise ValueError("Информация о пользователе отсутствует в сообщении.")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise e # Перебрасываем ошибку выше для дальнейшей обработки
|
||||||
3
configs/__init__.py
Normal file
3
configs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .config import *
|
||||||
|
from .cmd_list import *
|
||||||
|
from .roles import *
|
||||||
86
configs/cmd_list.py
Normal file
86
configs/cmd_list.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from typing import Final
|
||||||
|
|
||||||
|
# Список команд по ключу
|
||||||
|
COMMANDS: Final[dict[str, list[str]]] = {
|
||||||
|
"start": [
|
||||||
|
"start", "старт", "почати",
|
||||||
|
"ыефке", "cnfhn", "on", "вкл", "щт", "drk",
|
||||||
|
],
|
||||||
|
"help": [
|
||||||
|
"help", "помощь", "допомога",
|
||||||
|
"рудзщь", "dopomoga", "?",
|
||||||
|
],
|
||||||
|
"menu": [
|
||||||
|
"menu", "меню", "менюшка",
|
||||||
|
"ьщкф", "menyu",
|
||||||
|
],
|
||||||
|
"create": [
|
||||||
|
"create", "создать", "створити",
|
||||||
|
"сщзду", "sozdat", "stvoriti",
|
||||||
|
],
|
||||||
|
"report": [
|
||||||
|
"report", "репорт", "скарга",
|
||||||
|
"кщзщтв", "repert",
|
||||||
|
],
|
||||||
|
"mute": [
|
||||||
|
"mute", "заглушить", "заглушити",
|
||||||
|
"угуыщцук", "zaglushit",
|
||||||
|
],
|
||||||
|
"kick": [
|
||||||
|
"kick", "кик", "викинути",
|
||||||
|
"куиф", "vikynuty",
|
||||||
|
],
|
||||||
|
"ban": [
|
||||||
|
"ban", "бан", "забанити",
|
||||||
|
"ьфд", "zabanyty",
|
||||||
|
],
|
||||||
|
"stats": [
|
||||||
|
"stats", "статистика", "статистика",
|
||||||
|
"ыпщз", "statystyka",
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
"settings", "настройки", "налаштування",
|
||||||
|
"гшеукефьз", "nastroyky",
|
||||||
|
],
|
||||||
|
"info": [
|
||||||
|
"info", "инфо", "інфо",
|
||||||
|
"шкещ", "info",
|
||||||
|
],
|
||||||
|
"feedback": [
|
||||||
|
"feedback", "обратная связь", "зворотній зв’язок",
|
||||||
|
"гуеекфьз", "obratnaia_svyaz",
|
||||||
|
],
|
||||||
|
"subscribe": [
|
||||||
|
"subscribe", "подписаться", "підписатися",
|
||||||
|
"подписатсь", "pidpysatysia",
|
||||||
|
],
|
||||||
|
"unsubscribe": [
|
||||||
|
"unsubscribe", "отписаться", "відписатися",
|
||||||
|
"отписаться", "vidpysatysia",
|
||||||
|
],
|
||||||
|
"language": [
|
||||||
|
"language", "язык", "мова",
|
||||||
|
"йцукефь", "mova",
|
||||||
|
],
|
||||||
|
"cancel": [
|
||||||
|
"cancel", "отмена", "скасувати",
|
||||||
|
"утпщге", "skasuvaty",
|
||||||
|
],
|
||||||
|
"list": [
|
||||||
|
"list", "список", "список",
|
||||||
|
"дшззщк", "spysok",
|
||||||
|
],
|
||||||
|
"forward": [
|
||||||
|
"forward", "переслать", "переслати",
|
||||||
|
"дшпекщву", "pereslaty",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
"new": [
|
||||||
|
"new", "туц", "вступление",
|
||||||
|
"cnegktybt", "ym.", "нью",
|
||||||
|
],
|
||||||
|
"active": [
|
||||||
|
"active",
|
||||||
|
]
|
||||||
|
}
|
||||||
394
configs/config.py
Normal file
394
configs/config.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse, ParseResult
|
||||||
|
from typing import ClassVar, Final, Optional, Any
|
||||||
|
|
||||||
|
from pydantic import field_validator, model_validator
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from aiogram.types import ChatAdministratorRights
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Улучшенный класс настроек с комплексной валидацией"""
|
||||||
|
|
||||||
|
# Конфигурация загрузки переменных окружения
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
case_sensitive=False,
|
||||||
|
validate_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Режимы и базовые параметры
|
||||||
|
PYTHONUNBUFFERED: str = "1"
|
||||||
|
LOCALE_PATH: str = "locales"
|
||||||
|
|
||||||
|
DEBUG: bool = False
|
||||||
|
OWNER: str = "@verdise"
|
||||||
|
|
||||||
|
# Токены бота
|
||||||
|
BOT_TOKEN: Optional[str] = None
|
||||||
|
BOT_DEBUG_TOKEN: Optional[str] = None
|
||||||
|
|
||||||
|
# Параметры сообщений
|
||||||
|
PARSE_MODE: str = "HTML"
|
||||||
|
ENCOD: str = "utf-8"
|
||||||
|
TIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
|
||||||
|
PREFIX: str = "/!.&?"
|
||||||
|
BOT_LANGUAGE: str = "Aiogram3"
|
||||||
|
|
||||||
|
# Настройки сообщений
|
||||||
|
DISABLE_NOTIFICATION: bool = False
|
||||||
|
PROTECT_CONTENT: bool = False
|
||||||
|
ALLOW_SENDING_WITHOUT_REPLY: bool = True
|
||||||
|
LINK_PREVIEW_IS_DISABLED: bool = False
|
||||||
|
LINK_PREVIEW_PREFER_SMALL_MEDIA: bool = False
|
||||||
|
LINK_PREVIEW_PREFER_LARGE_MEDIA: bool = True
|
||||||
|
LINK_PREVIEW_SHOW_ABOVE_TEXT: bool = True
|
||||||
|
SHOW_CAPTION_ABOVE_MEDIA: bool = False
|
||||||
|
|
||||||
|
# Разрешения и логирование
|
||||||
|
BOT_EDIT: bool = True
|
||||||
|
START_INFO_CONSOLE: bool = True
|
||||||
|
START_INFO_TO_FILE: bool = True
|
||||||
|
LOG_CONSOLE: bool = True
|
||||||
|
LOG_FILE: bool = True
|
||||||
|
LOG_DIR: Path = Path('Logs')
|
||||||
|
LOG_FILE_INFO: Path = Path('bot_info.log')
|
||||||
|
|
||||||
|
# Вебхук
|
||||||
|
WEBHOOK: bool = False
|
||||||
|
WEBHOOK_URL: str = "https://bot.primo.dpdns.org/webhook" # публичный HTTPS url
|
||||||
|
WEBAPP_HOST: str = "0.0.0.0" # адрес, на котором слушает uvicorn внутри контейнера
|
||||||
|
WEBAPP_PORT: int = 3131
|
||||||
|
LOG_LEVEL: str = "warning"
|
||||||
|
ACCES_LOG: bool = False
|
||||||
|
|
||||||
|
# API ключи
|
||||||
|
API_KEY: Optional[str] = None
|
||||||
|
WEB_API_KEY: Optional[str] = None
|
||||||
|
WEATHER_API_KEY: Optional[str] = None
|
||||||
|
|
||||||
|
# Пользовательские данные
|
||||||
|
TG_API_UID: int = 0
|
||||||
|
TG_API_HASH: Optional[str] = None
|
||||||
|
|
||||||
|
# Идентификаторы
|
||||||
|
ADMIN_ID: list[int] = []
|
||||||
|
MODERATOR_ID: int = 0
|
||||||
|
IMPORTANT_ID: int = 0
|
||||||
|
IMPORTANT_GROUP_ID: int = 0
|
||||||
|
IMPORTANT_CHANNEL_ID: int = 0
|
||||||
|
SUPPORT_CHAT_ID: int = 0
|
||||||
|
|
||||||
|
# Настройки бота
|
||||||
|
PROJECT_NAME: str = "PRIMO"
|
||||||
|
BOT_NAME: str = "Первозданная Жемчужина"
|
||||||
|
BOT_DESCRIPTION: Optional[str] = None
|
||||||
|
BOT_SHORT_DESCRIPTION: Optional[str] = None
|
||||||
|
|
||||||
|
# Ролевой проект
|
||||||
|
RP_NAME: Optional[str] = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
|
||||||
|
INFO_URL: Optional[str] = "https://t.me/PrimoWorldRP"
|
||||||
|
FLUD_URL: Optional[str] = "https://t.me/PrimoWorldRP"
|
||||||
|
RP_URL: Optional[str] = "https://t.me/PrimoWorldRP"
|
||||||
|
LIFE_URL: Optional[str] = "https://t.me/PrimoWorldRP"
|
||||||
|
RP_OWNER: Optional[str] = None
|
||||||
|
ROLES: list[str] = ["Альбедо", "Чжун Ли", "Кэйа"]
|
||||||
|
|
||||||
|
# Права администратора
|
||||||
|
ANONYMOUS: bool = False
|
||||||
|
MANAGE_CHAT: bool = True
|
||||||
|
CHANGE_INFO: bool = True
|
||||||
|
PROMOTE_MEMBERS: bool = True
|
||||||
|
RESTRICT_MEMBERS: bool = True
|
||||||
|
POST_MESSAGE: bool = True
|
||||||
|
MANAGE_TOPICS: bool = True
|
||||||
|
INVITE_USER: bool = True
|
||||||
|
DELETE_MESSAGES: bool = True
|
||||||
|
MANAGE_VIDEO_CHATS: bool = True
|
||||||
|
EDIT_MESSAGES: bool = True
|
||||||
|
PIN_MESSAGE: bool = True
|
||||||
|
POST_STORIES: bool = True
|
||||||
|
EDIT_STORIES: bool = True
|
||||||
|
DELETE_STORIES: bool = True
|
||||||
|
|
||||||
|
# ================= ВАЛИДАТОРЫ =================
|
||||||
|
|
||||||
|
@field_validator('PYTHONUNBUFFERED')
|
||||||
|
def validate_unbuffered(cls, v: str) -> str:
|
||||||
|
"""Проверка корректности значения буферизации"""
|
||||||
|
if v not in ('0', '1'):
|
||||||
|
raise ValueError("PYTHONUNBUFFERED должен быть '0' или '1'")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('PARSE_MODE')
|
||||||
|
def validate_parse_mode(cls, v: str) -> str:
|
||||||
|
"""Проверка допустимого режима разметки"""
|
||||||
|
allowed_modes: set[str] = {"HTML", "Markdown", "MarkdownV2"}
|
||||||
|
if v not in allowed_modes:
|
||||||
|
raise ValueError(f"Недопустимый PARSE_MODE. Допустимые значения: {', '.join(allowed_modes)}")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('PREFIX')
|
||||||
|
def validate_prefix(cls, v: str) -> str:
|
||||||
|
"""Очистка и проверка префиксов команд"""
|
||||||
|
cleaned: str = ''.join(sorted(set(v), key=v.index)) # Удаление дубликатов с сохранением порядка
|
||||||
|
if len(cleaned) < 1:
|
||||||
|
raise ValueError("PREFIX должен содержать хотя бы один символ")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
@field_validator('LOG_DIR', 'LOG_FILE_INFO', mode='before')
|
||||||
|
def validate_paths(cls, v: Any) -> Path:
|
||||||
|
"""Преобразование путей в объекты Path"""
|
||||||
|
return Path(v) if isinstance(v, str) else v
|
||||||
|
|
||||||
|
@field_validator('TG_API_UID', 'MODERATOR_ID')
|
||||||
|
def validate_ids(cls, v: int) -> int:
|
||||||
|
"""Проверка корректности идентификаторов"""
|
||||||
|
if v < 0:
|
||||||
|
raise ValueError("ID не может быть отрицательным")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('WEBHOOK_URL')
|
||||||
|
def validate_webhook_url(cls, v: str) -> str:
|
||||||
|
"""Базовая проверка URL вебхука"""
|
||||||
|
parsed: ParseResult = urlparse(v)
|
||||||
|
if not all([parsed.scheme, parsed.netloc]):
|
||||||
|
raise ValueError("Некорректный URL вебхука")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('BOT_NAME', 'PROJECT_NAME', 'OWNER')
|
||||||
|
def validate_non_empty(cls, v: str) -> str:
|
||||||
|
"""Проверка непустых строк"""
|
||||||
|
if not v.strip():
|
||||||
|
raise ValueError("Поле не может быть пустым")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@model_validator(mode='after')
|
||||||
|
def validate_bot_token(cls, setting: "Settings") -> "Settings":
|
||||||
|
"""Проверка наличия необходимых токенов"""
|
||||||
|
if setting.DEBUG and not setting.BOT_DEBUG_TOKEN:
|
||||||
|
raise ValueError("Требуется BOT_DEBUG_TOKEN в режиме DEBUG")
|
||||||
|
if not setting.DEBUG and not setting.BOT_TOKEN:
|
||||||
|
raise ValueError("Требуется BOT_TOKEN для рабочего режима")
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@model_validator(mode='after')
|
||||||
|
def validate_webhook_config(cls, setting: "Settings") -> "Settings":
|
||||||
|
"""Проверка конфигурации вебхука"""
|
||||||
|
if setting.WEBHOOK and not setting.WEBHOOK_URL:
|
||||||
|
raise ValueError("WEBHOOK_URL обязателен при включенном WEBHOOK")
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@model_validator(mode='after')
|
||||||
|
def validate_logging_paths(cls, setting: "Settings") -> "Settings":
|
||||||
|
"""Создание директорий для логов при необходимости"""
|
||||||
|
if setting.LOG_FILE and not setting.LOG_DIR.exists():
|
||||||
|
setting.LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@model_validator(mode='after')
|
||||||
|
def set_dynamic_descriptions(cls, setting: "Settings") -> "Settings":
|
||||||
|
"""Динамическая установка описаний бота"""
|
||||||
|
if setting.BOT_DESCRIPTION is None:
|
||||||
|
setting.BOT_DESCRIPTION = f"Ваш помощник в удивительные миры! Prod. by:『{setting.OWNER}』"
|
||||||
|
if setting.BOT_SHORT_DESCRIPTION is None:
|
||||||
|
setting.BOT_SHORT_DESCRIPTION = f"Тех.поддержка: {setting.OWNER}"
|
||||||
|
return setting
|
||||||
|
|
||||||
|
# ================= СВОЙСТВА =================
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rights(self) -> ChatAdministratorRights:
|
||||||
|
"""Права администратора бота"""
|
||||||
|
return ChatAdministratorRights(
|
||||||
|
is_anonymous=self.ANONYMOUS,
|
||||||
|
can_manage_chat=self.MANAGE_CHAT,
|
||||||
|
can_delete_messages=self.DELETE_MESSAGES,
|
||||||
|
can_manage_video_chats=self.MANAGE_VIDEO_CHATS,
|
||||||
|
can_restrict_members=self.RESTRICT_MEMBERS,
|
||||||
|
can_promote_members=self.PROMOTE_MEMBERS,
|
||||||
|
can_change_info=self.CHANGE_INFO,
|
||||||
|
can_invite_users=self.INVITE_USER,
|
||||||
|
can_post_stories=self.POST_STORIES,
|
||||||
|
can_edit_stories=self.EDIT_STORIES,
|
||||||
|
can_delete_stories=self.DELETE_STORIES,
|
||||||
|
can_post_messages=self.POST_MESSAGE,
|
||||||
|
can_edit_messages=self.EDIT_MESSAGES,
|
||||||
|
can_pin_messages=self.PIN_MESSAGE,
|
||||||
|
can_manage_topics=self.MANAGE_TOPICS,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_bot_token(self) -> str:
|
||||||
|
"""Активный токен бота в зависимости от режима"""
|
||||||
|
token = self.BOT_DEBUG_TOKEN if self.DEBUG else self.BOT_TOKEN
|
||||||
|
if not token:
|
||||||
|
raise ValueError("Активный токен бота отсутствует")
|
||||||
|
return token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_dir_absolute(self) -> Path:
|
||||||
|
"""Абсолютный путь к директории логов"""
|
||||||
|
return self.LOG_DIR.absolute()
|
||||||
|
|
||||||
|
|
||||||
|
# Инициализация настроек
|
||||||
|
settings: Settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
|
# Классы для обратной совместимости и удобства использования
|
||||||
|
|
||||||
|
class BotSettings:
|
||||||
|
"""Алиасы для настроек бота."""
|
||||||
|
DEBUG: Final[bool] = settings.DEBUG
|
||||||
|
OWNER: Final[str] = settings.OWNER
|
||||||
|
BOT_TOKEN: Final[str] = settings.active_bot_token
|
||||||
|
PARSE_MODE: Final[str] = settings.PARSE_MODE
|
||||||
|
ENCOD: Final[str] = settings.ENCOD
|
||||||
|
TIME_FORMAT: Final[str] = settings.TIME_FORMAT
|
||||||
|
PREFIX: Final[str] = settings.PREFIX
|
||||||
|
BOT_LANGUAGE: Final[str] = settings.BOT_LANGUAGE
|
||||||
|
DISABLE_NOTIFICATION: Final[bool] = settings.DISABLE_NOTIFICATION
|
||||||
|
PROTECT_CONTENT: Final[bool] = settings.PROTECT_CONTENT
|
||||||
|
ALLOW_SENDING_WITHOUT_REPLY: Final[bool] = settings.ALLOW_SENDING_WITHOUT_REPLY
|
||||||
|
LINK_PREVIEW_IS_DISABLED: Final[bool] = settings.LINK_PREVIEW_IS_DISABLED
|
||||||
|
LINK_PREVIEW_PREFER_SMALL_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_SMALL_MEDIA
|
||||||
|
LINK_PREVIEW_PREFER_LARGE_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_LARGE_MEDIA
|
||||||
|
LINK_PREVIEW_SHOW_ABOVE_TEXT: Final[bool] = settings.LINK_PREVIEW_SHOW_ABOVE_TEXT
|
||||||
|
SHOW_CAPTION_ABOVE_MEDIA: Final[bool] = settings.SHOW_CAPTION_ABOVE_MEDIA
|
||||||
|
|
||||||
|
|
||||||
|
class Permission:
|
||||||
|
"""Алиасы для разрешений."""
|
||||||
|
BOT_EDIT: Final[bool] = settings.BOT_EDIT
|
||||||
|
START_INFO_CONSOLE: Final[bool] = settings.START_INFO_CONSOLE
|
||||||
|
START_INFO_TO_FILE: Final[bool] = settings.START_INFO_TO_FILE
|
||||||
|
|
||||||
|
|
||||||
|
class LogConfig:
|
||||||
|
"""Алиасы для конфигурации логов."""
|
||||||
|
CONSOLE: Final[bool] = settings.LOG_CONSOLE
|
||||||
|
FILE: Final[bool] = settings.LOG_FILE
|
||||||
|
DIR: Final[Path] = settings.LOG_DIR
|
||||||
|
FILE_INFO: Final[Path] = settings.LOG_FILE_INFO
|
||||||
|
ROTATION: ClassVar[str] = '100 MB'
|
||||||
|
RETENTION: ClassVar[str] = '7 days'
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook:
|
||||||
|
"""Алиасы для вебхука."""
|
||||||
|
WEBHOOK: Final[bool] = settings.WEBHOOK
|
||||||
|
WEBHOOK_URL: Final[str] = settings.WEBHOOK_URL
|
||||||
|
WEBHOOK_HOST: Final[str] = settings.WEBAPP_HOST
|
||||||
|
WEBHOOK_PORT: Final[int] = settings.WEBAPP_PORT
|
||||||
|
LOG_LEVEL: Final[str] = settings.LOG_LEVEL
|
||||||
|
ACCES_LOG: Final[bool] = settings.ACCES_LOG
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class APISettings:
|
||||||
|
"""Алиасы для API."""
|
||||||
|
API_KEY: Final[Optional[str]] = settings.API_KEY
|
||||||
|
WEB_API_KEY: Final[Optional[str]] = settings.WEB_API_KEY
|
||||||
|
WEATHER_API_KEY: Final[Optional[str]] = settings.WEATHER_API_KEY
|
||||||
|
|
||||||
|
|
||||||
|
class UserIn:
|
||||||
|
"""Алиасы для пользовательских данных."""
|
||||||
|
TG_API_UID: Final[int] = settings.TG_API_UID
|
||||||
|
TG_API_HASH: Final[Optional[str]] = settings.TG_API_HASH
|
||||||
|
|
||||||
|
|
||||||
|
class ImportantID:
|
||||||
|
"""Алиасы для важных ID."""
|
||||||
|
ADMIN_ID: Final[list[int]] = settings.ADMIN_ID
|
||||||
|
MODERATOR_ID: Final[int] = settings.MODERATOR_ID
|
||||||
|
IMPORTANT_ID: Final[int] = settings.IMPORTANT_ID
|
||||||
|
IMPORTANT_GROUP_ID: Final[int] = settings.IMPORTANT_GROUP_ID
|
||||||
|
IMPORTANT_CHANNEL_ID: Final[int] = settings.IMPORTANT_CHANNEL_ID
|
||||||
|
SUPPORT_CHAT_ID: Final[int] = settings.SUPPORT_CHAT_ID
|
||||||
|
|
||||||
|
|
||||||
|
class BotEdit:
|
||||||
|
"""Алиасы для настроек редактирования бота."""
|
||||||
|
ALLOW_PERMISSION: Final[bool] = settings.BOT_EDIT
|
||||||
|
PROJECT_NAME: Final[str] = settings.PROJECT_NAME
|
||||||
|
NAME: Final[str] = settings.BOT_NAME
|
||||||
|
DESCRIPTION: Final[str] = settings.BOT_DESCRIPTION
|
||||||
|
SHORT_DESCRIPTION: Final[str] = settings.BOT_SHORT_DESCRIPTION
|
||||||
|
ANONYMOUS: Final[bool] = settings.ANONYMOUS
|
||||||
|
MANAGE_CHAT: Final[bool] = settings.MANAGE_CHAT
|
||||||
|
CHANGE_INFO: Final[bool] = settings.CHANGE_INFO
|
||||||
|
PROMOTE_MEMBERS: Final[bool] = settings.PROMOTE_MEMBERS
|
||||||
|
RESTRICT_MEMBERS: Final[bool] = settings.RESTRICT_MEMBERS
|
||||||
|
POST_MESSAGE: Final[bool] = settings.POST_MESSAGE
|
||||||
|
MANAGE_TOPICS: Final[bool] = settings.MANAGE_TOPICS
|
||||||
|
INVITE_USER: Final[bool] = settings.INVITE_USER
|
||||||
|
DELETE_MESSAGES: Final[bool] = settings.DELETE_MESSAGES
|
||||||
|
MANAGE_VIDEO_CHATS: Final[bool] = settings.MANAGE_VIDEO_CHATS
|
||||||
|
EDIT_MESSAGES: Final[bool] = settings.EDIT_MESSAGES
|
||||||
|
PIN_MESSAGE: Final[bool] = settings.PIN_MESSAGE
|
||||||
|
POST_STORIES: Final[bool] = settings.POST_STORIES
|
||||||
|
EDIT_STORIES: Final[bool] = settings.EDIT_STORIES
|
||||||
|
DELETE_STORIES: Final[bool] = settings.DELETE_STORIES
|
||||||
|
RIGHTS: Final[ChatAdministratorRights] = settings.rights
|
||||||
|
|
||||||
|
|
||||||
|
class RpValue:
|
||||||
|
"""Переменные связанные с ролевым проектом."""
|
||||||
|
RP_NAME: Final[str] = settings.RP_NAME
|
||||||
|
INFO_URL: str = settings.INFO_URL
|
||||||
|
FLUD_URL: str = settings.FLUD_URL
|
||||||
|
RP_URL: str = settings.RP_URL
|
||||||
|
LIFE_URL: str = settings.LIFE_URL
|
||||||
|
RP_OWNER: str = settings.RP_OWNER
|
||||||
|
ROLES: list[str] = settings.ROLES
|
||||||
|
|
||||||
|
|
||||||
|
class Project:
|
||||||
|
POSTS_DIR: ClassVar[Path] = Path('posts')
|
||||||
|
|
||||||
|
|
||||||
|
class Lists:
|
||||||
|
"""Интересные списки фактов, цитат и анекдотов."""
|
||||||
|
facts: list[str] = [
|
||||||
|
"Python был создан Гвидо ван Россумом в 1991 году.",
|
||||||
|
"Имена Python и Monty Python связаны — язык назван в честь шоу.",
|
||||||
|
"Python — язык с динамической типизацией.",
|
||||||
|
"В Python всё является объектом, даже функции и типы данных.",
|
||||||
|
"Списки в Python — это изменяемые коллекции, в отличие от кортежей.",
|
||||||
|
"Python поддерживает парадигмы ООП, функционального и императивного программирования.",
|
||||||
|
"Zen of Python можно увидеть, набрав `import this` в интерпретаторе.",
|
||||||
|
]
|
||||||
|
jokes: list[str] = [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
]
|
||||||
|
quotes: list[str] = [
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Экспорт совместимых компонентов
|
||||||
|
__all__ = (
|
||||||
|
"BotSettings",
|
||||||
|
"LogConfig",
|
||||||
|
"Webhook",
|
||||||
|
"APISettings",
|
||||||
|
"UserIn",
|
||||||
|
"ImportantID",
|
||||||
|
"Permission",
|
||||||
|
"BotEdit",
|
||||||
|
"Project",
|
||||||
|
"RpValue",
|
||||||
|
'settings',
|
||||||
|
'Lists',
|
||||||
|
)
|
||||||
294
configs/roles.py
Normal file
294
configs/roles.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
from database import RoleRegion
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("genshin_roles", "hsr_roles", "all_roles",)
|
||||||
|
|
||||||
|
genshin_roles: list = [
|
||||||
|
# Мондштадт
|
||||||
|
("Альбедо", RoleRegion.MONDSTADT),
|
||||||
|
("Барбара", RoleRegion.MONDSTADT),
|
||||||
|
("Беннет", RoleRegion.MONDSTADT),
|
||||||
|
("Венти", RoleRegion.MONDSTADT),
|
||||||
|
("Далия", RoleRegion.MONDSTADT),
|
||||||
|
("Джинн", RoleRegion.MONDSTADT),
|
||||||
|
("Дилюк", RoleRegion.MONDSTADT),
|
||||||
|
("Диона", RoleRegion.MONDSTADT),
|
||||||
|
("Кли", RoleRegion.MONDSTADT),
|
||||||
|
("Кэйа", RoleRegion.MONDSTADT),
|
||||||
|
("Лиза", RoleRegion.MONDSTADT),
|
||||||
|
("Мика", RoleRegion.MONDSTADT),
|
||||||
|
("Мона", RoleRegion.MONDSTADT),
|
||||||
|
("Ноэлль", RoleRegion.MONDSTADT),
|
||||||
|
("Розария", RoleRegion.MONDSTADT),
|
||||||
|
("Рэйзор", RoleRegion.MONDSTADT),
|
||||||
|
("Сахароза", RoleRegion.MONDSTADT),
|
||||||
|
("Фишль", RoleRegion.MONDSTADT),
|
||||||
|
("Эмбер", RoleRegion.MONDSTADT),
|
||||||
|
("Эола", RoleRegion.MONDSTADT),
|
||||||
|
|
||||||
|
# Ли Юэ
|
||||||
|
("Бай Чжу", RoleRegion.LIYUE),
|
||||||
|
("Бэй Доу", RoleRegion.LIYUE),
|
||||||
|
("Гань Юй", RoleRegion.LIYUE),
|
||||||
|
("Е Лань", RoleRegion.LIYUE),
|
||||||
|
("Ка Мин", RoleRegion.LIYUE),
|
||||||
|
("Кэ Цин", RoleRegion.LIYUE),
|
||||||
|
("Лань Янь", RoleRegion.LIYUE),
|
||||||
|
("Нин Гуан", RoleRegion.LIYUE),
|
||||||
|
("Син Цю", RoleRegion.LIYUE),
|
||||||
|
("Синь Янь", RoleRegion.LIYUE),
|
||||||
|
("Сян Лин", RoleRegion.LIYUE),
|
||||||
|
("Сянь Юнь", RoleRegion.LIYUE),
|
||||||
|
("Сяо", RoleRegion.LIYUE),
|
||||||
|
("Ху Тао", RoleRegion.LIYUE),
|
||||||
|
("Ци Ци", RoleRegion.LIYUE),
|
||||||
|
("Чжун Ли", RoleRegion.LIYUE),
|
||||||
|
("Чун Юнь", RoleRegion.LIYUE),
|
||||||
|
("Шэнь Хэ", RoleRegion.LIYUE),
|
||||||
|
("Юнь Цзинь", RoleRegion.LIYUE),
|
||||||
|
("Янь Фэй", RoleRegion.LIYUE),
|
||||||
|
("Яо Яо", RoleRegion.LIYUE),
|
||||||
|
|
||||||
|
# Инадзума
|
||||||
|
("Аяка", RoleRegion.INAZUMA),
|
||||||
|
("Аято", RoleRegion.INAZUMA),
|
||||||
|
("Горо", RoleRegion.INAZUMA),
|
||||||
|
("Ёимия", RoleRegion.INAZUMA),
|
||||||
|
("Итто", RoleRegion.INAZUMA),
|
||||||
|
("Кадзуха", RoleRegion.INAZUMA),
|
||||||
|
("Кирара", RoleRegion.INAZUMA),
|
||||||
|
("Кокоми", RoleRegion.INAZUMA),
|
||||||
|
("Мидзуки", RoleRegion.INAZUMA),
|
||||||
|
("Райдэн Макото", RoleRegion.INAZUMA),
|
||||||
|
("Райдэн Эи", RoleRegion.INAZUMA),
|
||||||
|
("Сара", RoleRegion.INAZUMA),
|
||||||
|
("Саю", RoleRegion.INAZUMA),
|
||||||
|
("Синобу", RoleRegion.INAZUMA),
|
||||||
|
("Тиори", RoleRegion.INAZUMA),
|
||||||
|
("Тома", RoleRegion.INAZUMA),
|
||||||
|
("Хэйдзо", RoleRegion.INAZUMA),
|
||||||
|
("Яэ Мико", RoleRegion.INAZUMA),
|
||||||
|
|
||||||
|
# Сумеру
|
||||||
|
("Аль-Хайтам", RoleRegion.SUMERU),
|
||||||
|
("Дори", RoleRegion.SUMERU),
|
||||||
|
("Дэхья", RoleRegion.SUMERU),
|
||||||
|
("Кавех", RoleRegion.SUMERU),
|
||||||
|
("Кандакия", RoleRegion.SUMERU),
|
||||||
|
("Коллеи", RoleRegion.SUMERU),
|
||||||
|
("Лайла", RoleRegion.SUMERU),
|
||||||
|
("Нахида", RoleRegion.SUMERU),
|
||||||
|
("Нилу", RoleRegion.SUMERU),
|
||||||
|
("Руккхадевата", RoleRegion.SUMERU),
|
||||||
|
("Сайно", RoleRegion.SUMERU),
|
||||||
|
("Сетос", RoleRegion.SUMERU),
|
||||||
|
("Странник", RoleRegion.SUMERU),
|
||||||
|
("Тигнари", RoleRegion.SUMERU),
|
||||||
|
("Фарузан", RoleRegion.SUMERU),
|
||||||
|
|
||||||
|
# Фонтейн
|
||||||
|
("Клоринда", RoleRegion.FONTAINE),
|
||||||
|
("Линетт", RoleRegion.FONTAINE),
|
||||||
|
("Лини", RoleRegion.FONTAINE),
|
||||||
|
("Навия", RoleRegion.FONTAINE),
|
||||||
|
("Нёвиллет", RoleRegion.FONTAINE),
|
||||||
|
("Ризли", RoleRegion.FONTAINE),
|
||||||
|
("Сиджвин", RoleRegion.FONTAINE),
|
||||||
|
("Фокалорс", RoleRegion.FONTAINE),
|
||||||
|
("Фремине", RoleRegion.FONTAINE),
|
||||||
|
("Фурина", RoleRegion.FONTAINE),
|
||||||
|
("Шарлотта", RoleRegion.FONTAINE),
|
||||||
|
("Шеврёз", RoleRegion.FONTAINE),
|
||||||
|
("Эмилия", RoleRegion.FONTAINE),
|
||||||
|
("Эскофье", RoleRegion.FONTAINE),
|
||||||
|
|
||||||
|
# Натлан
|
||||||
|
("Ахав", RoleRegion.NATLAN),
|
||||||
|
("Вареса", RoleRegion.NATLAN),
|
||||||
|
("Иансан", RoleRegion.NATLAN),
|
||||||
|
("Ифа", RoleRegion.NATLAN),
|
||||||
|
("Качина", RoleRegion.NATLAN),
|
||||||
|
("Кинич", RoleRegion.NATLAN),
|
||||||
|
("Мавуика", RoleRegion.NATLAN),
|
||||||
|
("Муалани", RoleRegion.NATLAN),
|
||||||
|
("Оророн", RoleRegion.NATLAN),
|
||||||
|
("Ситлали", RoleRegion.NATLAN),
|
||||||
|
("Часка", RoleRegion.NATLAN),
|
||||||
|
("Шилонен", RoleRegion.NATLAN),
|
||||||
|
|
||||||
|
# Снежная
|
||||||
|
("Арлекино", RoleRegion.SNEZHNAYA),
|
||||||
|
("Дотторе", RoleRegion.SNEZHNAYA),
|
||||||
|
("Капитано", RoleRegion.SNEZHNAYA),
|
||||||
|
("Коломбина", RoleRegion.SNEZHNAYA),
|
||||||
|
("Панталоне", RoleRegion.SNEZHNAYA),
|
||||||
|
("Пульчинелла", RoleRegion.SNEZHNAYA),
|
||||||
|
("Пьеро", RoleRegion.SNEZHNAYA),
|
||||||
|
("Сандроне", RoleRegion.SNEZHNAYA),
|
||||||
|
("Синьора", RoleRegion.SNEZHNAYA),
|
||||||
|
("Царица", RoleRegion.SNEZHNAYA),
|
||||||
|
("Тарталья", RoleRegion.SNEZHNAYA),
|
||||||
|
|
||||||
|
# Каэнри'ах
|
||||||
|
("Айно", RoleRegion.KHAENRIAH),
|
||||||
|
("Алиса", RoleRegion.KHAENRIAH),
|
||||||
|
("Варка", RoleRegion.KHAENRIAH),
|
||||||
|
("Дурин", RoleRegion.KHAENRIAH),
|
||||||
|
("Инеффа", RoleRegion.KHAENRIAH),
|
||||||
|
("Лаума", RoleRegion.KHAENRIAH),
|
||||||
|
("Нефер", RoleRegion.KHAENRIAH),
|
||||||
|
("Николь", RoleRegion.KHAENRIAH),
|
||||||
|
("Флинс", RoleRegion.KHAENRIAH),
|
||||||
|
("Ягода", RoleRegion.KHAENRIAH),
|
||||||
|
|
||||||
|
# Другие (Genshin Impact)
|
||||||
|
("Дайнслейф", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Итэр", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Люмин", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Паймон", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Рэйндоттир", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Скирк", RoleRegion.GENSHIN_OTHER),
|
||||||
|
("Элой", RoleRegion.GENSHIN_OTHER),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Роли для Honkai: Star Rail
|
||||||
|
hsr_roles: list = [
|
||||||
|
# Звездный экспресс
|
||||||
|
("Вельт", RoleRegion.HSR_STAR),
|
||||||
|
("Дань Хэн", RoleRegion.HSR_STAR),
|
||||||
|
("Келус", RoleRegion.HSR_STAR),
|
||||||
|
("Март 7", RoleRegion.HSR_STAR),
|
||||||
|
("Стелла", RoleRegion.HSR_STAR),
|
||||||
|
("Химеко", RoleRegion.HSR_STAR),
|
||||||
|
|
||||||
|
# Космическая станция Герта
|
||||||
|
("Арлан", RoleRegion.HSR_GERTA),
|
||||||
|
("Аста", RoleRegion.HSR_GERTA),
|
||||||
|
("Великая Герта", RoleRegion.HSR_GERTA),
|
||||||
|
("Жуань Мэй", RoleRegion.HSR_GERTA),
|
||||||
|
("Полька Какамонд", RoleRegion.HSR_GERTA),
|
||||||
|
("Скрюллум", RoleRegion.HSR_GERTA),
|
||||||
|
("Стивен Ллойд", RoleRegion.HSR_GERTA),
|
||||||
|
|
||||||
|
# Ярило-VI
|
||||||
|
("Броня", RoleRegion.HSR_YARILO),
|
||||||
|
("Гепард", RoleRegion.HSR_YARILO),
|
||||||
|
("Зеле", RoleRegion.HSR_YARILO),
|
||||||
|
("Клара", RoleRegion.HSR_YARILO),
|
||||||
|
("Коколия", RoleRegion.HSR_YARILO),
|
||||||
|
("Лука", RoleRegion.HSR_YARILO),
|
||||||
|
("Наташа", RoleRegion.HSR_YARILO),
|
||||||
|
("Пела", RoleRegion.HSR_YARILO),
|
||||||
|
("Рысь", RoleRegion.HSR_YARILO),
|
||||||
|
("Сампо", RoleRegion.HSR_YARILO),
|
||||||
|
("Сервал", RoleRegion.HSR_YARILO),
|
||||||
|
("Хук", RoleRegion.HSR_YARILO),
|
||||||
|
|
||||||
|
# Лофу Сяньчжоу
|
||||||
|
("Байлу", RoleRegion.HSR_LOFU),
|
||||||
|
("Байхэн", RoleRegion.HSR_LOFU),
|
||||||
|
("Гуйнайфэнь", RoleRegion.HSR_LOFU),
|
||||||
|
("Линша", RoleRegion.HSR_LOFU),
|
||||||
|
("Лоча", RoleRegion.HSR_LOFU),
|
||||||
|
("Моцзэ", RoleRegion.HSR_LOFU),
|
||||||
|
("Сушан", RoleRegion.HSR_LOFU),
|
||||||
|
("Сюэи", RoleRegion.HSR_LOFU),
|
||||||
|
("Фу Сюань", RoleRegion.HSR_LOFU),
|
||||||
|
("Фуга", RoleRegion.HSR_LOFU),
|
||||||
|
("Фэйсяо", RoleRegion.HSR_LOFU),
|
||||||
|
("Ханья", RoleRegion.HSR_LOFU),
|
||||||
|
("Хохо", RoleRegion.HSR_LOFU),
|
||||||
|
("Цзинлю", RoleRegion.HSR_LOFU),
|
||||||
|
("Цзин Юань", RoleRegion.HSR_LOFU),
|
||||||
|
("Цзяоцю", RoleRegion.HSR_LOFU),
|
||||||
|
("Цинцюэ", RoleRegion.HSR_LOFU),
|
||||||
|
("Юйкун", RoleRegion.HSR_LOFU),
|
||||||
|
("Юньли", RoleRegion.HSR_LOFU),
|
||||||
|
("Яньцин", RoleRegion.HSR_LOFU),
|
||||||
|
|
||||||
|
# Пенакония
|
||||||
|
("Ахерон", RoleRegion.HSR_PENACONY),
|
||||||
|
("Воскресенье", RoleRegion.HSR_PENACONY),
|
||||||
|
("Галлахер", RoleRegion.HSR_PENACONY),
|
||||||
|
("Мистер Река", RoleRegion.HSR_PENACONY),
|
||||||
|
("Зарянка", RoleRegion.HSR_PENACONY),
|
||||||
|
("Искорка", RoleRegion.HSR_PENACONY),
|
||||||
|
("Миша", RoleRegion.HSR_PENACONY),
|
||||||
|
("Рацио", RoleRegion.HSR_PENACONY),
|
||||||
|
("Чёрный Лебедь", RoleRegion.HSR_PENACONY),
|
||||||
|
|
||||||
|
# Амфореус
|
||||||
|
("Аглая", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Анаксагор", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Гиацина", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Гисиленса", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Кастория", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Керидра", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Кирена", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Ликург", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Мидей", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Трибби", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Фаенон", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
("Цифер", RoleRegion.HSR_AMPHOREUS),
|
||||||
|
|
||||||
|
# Охотники за Стеллар
|
||||||
|
("Блэйд", RoleRegion.HSR_HUNTER),
|
||||||
|
("Кафка", RoleRegion.HSR_HUNTER),
|
||||||
|
("Светлячок", RoleRegion.HSR_HUNTER),
|
||||||
|
("Серебряный Волк", RoleRegion.HSR_HUNTER),
|
||||||
|
("Элио", RoleRegion.HSR_HUNTER),
|
||||||
|
|
||||||
|
# КММ
|
||||||
|
("Авантюрин", RoleRegion.HSR_KMM),
|
||||||
|
("Агат", RoleRegion.HSR_KMM),
|
||||||
|
("Алмаз", RoleRegion.HSR_KMM),
|
||||||
|
("Обсидиан", RoleRegion.HSR_KMM),
|
||||||
|
("Опал", RoleRegion.HSR_KMM),
|
||||||
|
("Перламутр", RoleRegion.HSR_KMM),
|
||||||
|
("Сапфир", RoleRegion.HSR_KMM),
|
||||||
|
("Сугилит", RoleRegion.HSR_KMM),
|
||||||
|
("Топаз", RoleRegion.HSR_KMM),
|
||||||
|
("Янтарь", RoleRegion.HSR_KMM),
|
||||||
|
("Яшма", RoleRegion.HSR_KMM),
|
||||||
|
|
||||||
|
# Эоны
|
||||||
|
("Акивили", RoleRegion.HSR_EONS),
|
||||||
|
("Аха", RoleRegion.HSR_EONS),
|
||||||
|
("Клипот", RoleRegion.HSR_EONS),
|
||||||
|
("Лань", RoleRegion.HSR_EONS),
|
||||||
|
("Нанук", RoleRegion.HSR_EONS),
|
||||||
|
("Нус", RoleRegion.HSR_EONS),
|
||||||
|
("Ороборос", RoleRegion.HSR_EONS),
|
||||||
|
("Тайззиронт", RoleRegion.HSR_EONS),
|
||||||
|
("Фили", RoleRegion.HSR_EONS),
|
||||||
|
("Шипе", RoleRegion.HSR_EONS),
|
||||||
|
("Эна", RoleRegion.HSR_EONS),
|
||||||
|
("Яоши", RoleRegion.HSR_EONS),
|
||||||
|
("IX", RoleRegion.HSR_EONS),
|
||||||
|
|
||||||
|
# Вечногорящий особняк
|
||||||
|
("Акаш", RoleRegion.HSR_FIRE_MANSION),
|
||||||
|
("Герцог Инферно", RoleRegion.HSR_FIRE_MANSION),
|
||||||
|
("Дубра", RoleRegion.HSR_FIRE_MANSION),
|
||||||
|
("Катерина", RoleRegion.HSR_FIRE_MANSION),
|
||||||
|
("Констанция", RoleRegion.HSR_FIRE_MANSION),
|
||||||
|
|
||||||
|
# Лорды Опустошители
|
||||||
|
("Асат Прамад", RoleRegion.HSR_LORDS),
|
||||||
|
("Зефиро", RoleRegion.HSR_LORDS),
|
||||||
|
("Оростелла", RoleRegion.HSR_LORDS),
|
||||||
|
("Фантилия", RoleRegion.HSR_LORDS),
|
||||||
|
|
||||||
|
# Прочие (Honkai: Star Rail)
|
||||||
|
("Аргенти", RoleRegion.HSR_OTHER),
|
||||||
|
("Бутхилл", RoleRegion.HSR_OTHER),
|
||||||
|
("Раппа", RoleRegion.HSR_OTHER),
|
||||||
|
("Архив Пустоты", RoleRegion.HSR_OTHER),
|
||||||
|
|
||||||
|
# Фейт
|
||||||
|
("Арчер", RoleRegion.HSR_FATE),
|
||||||
|
("Сейбер", RoleRegion.HSR_FATE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Общий список ролей
|
||||||
|
all_roles: list = genshin_roles + hsr_roles
|
||||||
1
database/__init__.py
Normal file
1
database/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .database import *
|
||||||
1171
database/database.py
Normal file
1171
database/database.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
locales/en/LC_MESSAGES/bot.mo
Normal file
BIN
locales/en/LC_MESSAGES/bot.mo
Normal file
Binary file not shown.
58
locales/en/LC_MESSAGES/bot.po
Normal file
58
locales/en/LC_MESSAGES/bot.po
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# English translations for Bot Super Project.
|
||||||
|
# Copyright (C) 2025 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the Bot Super Project
|
||||||
|
# project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Bot Super Project 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: john@doe-email.com\n"
|
||||||
|
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
|
||||||
|
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Language-Team: en <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:43
|
||||||
|
msgid "Выберите язык:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:50
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Язык {lang} не поддерживается!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:29
|
||||||
|
msgid "Создать пост📔"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:30
|
||||||
|
msgid "Посмотреть список📋"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:31
|
||||||
|
msgid "Изменить язык🌐"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:35
|
||||||
|
#, python-brace-format
|
||||||
|
msgid ""
|
||||||
|
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
|
||||||
|
"\n"
|
||||||
|
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
|
||||||
|
"историй! \n"
|
||||||
|
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
|
||||||
|
"интереснее! \n"
|
||||||
|
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
|
||||||
|
"клавиатуре!\n"
|
||||||
|
"\n"
|
||||||
|
"Интересный факт:\n"
|
||||||
|
"<blockquote>{fact}</blockquote>\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
56
locales/messages.pot
Normal file
56
locales/messages.pot
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Translations template for PROJECT.
|
||||||
|
# Copyright (C) 2025 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Bot Super Project 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: john@doe-email.com\n"
|
||||||
|
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.13.1\n"
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:43
|
||||||
|
msgid "Выберите язык:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:50
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Язык {lang} не поддерживается!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:29
|
||||||
|
msgid "Создать пост📔"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:30
|
||||||
|
msgid "Посмотреть список📋"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:31
|
||||||
|
msgid "Изменить язык🌐"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:35
|
||||||
|
#, python-brace-format
|
||||||
|
msgid ""
|
||||||
|
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
|
||||||
|
"\n"
|
||||||
|
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
|
||||||
|
"историй! \n"
|
||||||
|
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
|
||||||
|
"интереснее! \n"
|
||||||
|
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
|
||||||
|
"клавиатуре!\n"
|
||||||
|
"\n"
|
||||||
|
"Интересный факт:\n"
|
||||||
|
"<blockquote>{fact}</blockquote>\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
BIN
locales/ru/LC_MESSAGES/bot.mo
Normal file
BIN
locales/ru/LC_MESSAGES/bot.mo
Normal file
Binary file not shown.
59
locales/ru/LC_MESSAGES/bot.po
Normal file
59
locales/ru/LC_MESSAGES/bot.po
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Russian translations for Bot Super Project.
|
||||||
|
# Copyright (C) 2025 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the Bot Super Project
|
||||||
|
# project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Bot Super Project 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: john@doe-email.com\n"
|
||||||
|
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
|
||||||
|
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"Language-Team: ru <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||||
|
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:43
|
||||||
|
msgid "Выберите язык:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:50
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Язык {lang} не поддерживается!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:29
|
||||||
|
msgid "Создать пост📔"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:30
|
||||||
|
msgid "Посмотреть список📋"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:31
|
||||||
|
msgid "Изменить язык🌐"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:35
|
||||||
|
#, python-brace-format
|
||||||
|
msgid ""
|
||||||
|
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
|
||||||
|
"\n"
|
||||||
|
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
|
||||||
|
"историй! \n"
|
||||||
|
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
|
||||||
|
"интереснее! \n"
|
||||||
|
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
|
||||||
|
"клавиатуре!\n"
|
||||||
|
"\n"
|
||||||
|
"Интересный факт:\n"
|
||||||
|
"<blockquote>{fact}</blockquote>\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
BIN
locales/uk/LC_MESSAGES/bot.mo
Normal file
BIN
locales/uk/LC_MESSAGES/bot.mo
Normal file
Binary file not shown.
59
locales/uk/LC_MESSAGES/bot.po
Normal file
59
locales/uk/LC_MESSAGES/bot.po
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Ukrainian translations for Bot Super Project.
|
||||||
|
# Copyright (C) 2025 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the Bot Super Project
|
||||||
|
# project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Bot Super Project 0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: john@doe-email.com\n"
|
||||||
|
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
|
||||||
|
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: uk\n"
|
||||||
|
"Language-Team: uk <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||||
|
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:43
|
||||||
|
msgid "Выберите язык:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/lang.py:50
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Язык {lang} не поддерживается!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:29
|
||||||
|
msgid "Создать пост📔"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:30
|
||||||
|
msgid "Посмотреть список📋"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:31
|
||||||
|
msgid "Изменить язык🌐"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot/handlers/commands/start.py:35
|
||||||
|
#, python-brace-format
|
||||||
|
msgid ""
|
||||||
|
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
|
||||||
|
"\n"
|
||||||
|
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
|
||||||
|
"историй! \n"
|
||||||
|
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
|
||||||
|
"интереснее! \n"
|
||||||
|
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
|
||||||
|
"клавиатуре!\n"
|
||||||
|
"\n"
|
||||||
|
"Интересный факт:\n"
|
||||||
|
"<blockquote>{fact}</blockquote>\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
75
main.py
Normal file
75
main.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bot import dp, bot, BotInfo, WebhookApp, setup_middlewares, router
|
||||||
|
from configs.config import Webhook
|
||||||
|
from database import db
|
||||||
|
from middleware import setup_logging
|
||||||
|
|
||||||
|
|
||||||
|
async def on_startup() -> None:
|
||||||
|
"""Действия при запуске бота."""
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
# Создание базы данных
|
||||||
|
await db.init_db()
|
||||||
|
if not await db.check_connection():
|
||||||
|
print("Не удалось подключиться к БД!")
|
||||||
|
return
|
||||||
|
await db.init_default_roles()
|
||||||
|
|
||||||
|
# Настройка информации о боте
|
||||||
|
await BotInfo.setup(bots=bot)
|
||||||
|
|
||||||
|
# Настройка middleware
|
||||||
|
setup_middlewares(
|
||||||
|
dp=dp,
|
||||||
|
bot=bot,
|
||||||
|
channel_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
dp.include_router(router)
|
||||||
|
BotInfo.start_info_out()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_polling() -> None:
|
||||||
|
"""Запуск в режиме polling."""
|
||||||
|
try:
|
||||||
|
await on_startup()
|
||||||
|
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
|
||||||
|
finally:
|
||||||
|
await bot.session.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_webhook() -> None:
|
||||||
|
"""Запуск в режиме webhook."""
|
||||||
|
app: WebhookApp = WebhookApp(host=Webhook.WEBHOOK_HOST, port=Webhook.WEBHOOK_PORT)
|
||||||
|
try:
|
||||||
|
await on_startup()
|
||||||
|
await app.start()
|
||||||
|
# держим процесс живым
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
finally:
|
||||||
|
await app.stop()
|
||||||
|
await bot.session.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
# Запуск в нужном режиме
|
||||||
|
if Webhook.WEBHOOK:
|
||||||
|
await run_webhook()
|
||||||
|
else:
|
||||||
|
await run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Защита для Windows
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
print("❌ Бот остановлен!")
|
||||||
|
sys.exit(0)
|
||||||
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 *
|
||||||
234
middleware/loggers/logs.py
Normal file
234
middleware/loggers/logs.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
from sys import stderr
|
||||||
|
from pathlib import Path
|
||||||
|
from functools import wraps
|
||||||
|
from inspect import iscoroutinefunction
|
||||||
|
from typing import Any, Callable, Optional, TypeVar, cast, Final
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from aiogram.types import Message, User
|
||||||
|
|
||||||
|
from configs.config import BotEdit, LogConfig
|
||||||
|
|
||||||
|
# Экспортируемые объекты
|
||||||
|
__all__ = ('Logger', 'setup_logging', 'loggers', 'log',)
|
||||||
|
|
||||||
|
# Универсальный тип для функций
|
||||||
|
F: TypeVar = TypeVar('F', bound=Callable[..., Any])
|
||||||
|
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
"""
|
||||||
|
Кастомный логгер с поддержкой декораторов и прямого вызова.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
system_name: Имя системы для логирования
|
||||||
|
_log_format: Формат логов
|
||||||
|
"""
|
||||||
|
_log_format: Final[str] = (
|
||||||
|
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <red>|</red> '
|
||||||
|
'<blue>{extra[system]}-{extra[log_type]}</blue> <red>| '
|
||||||
|
'{extra[user]} |</red> <level>{message}</level>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, system_name: str = BotEdit.PROJECT_NAME) -> None:
|
||||||
|
"""
|
||||||
|
Инициализация логгера.
|
||||||
|
|
||||||
|
:param system_name: Имя системы для логирования
|
||||||
|
"""
|
||||||
|
self.system_name = system_name
|
||||||
|
self._setup_done = False
|
||||||
|
|
||||||
|
def setup(self, start: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Настройка обработчиков Loguru: консоль и файлы.
|
||||||
|
|
||||||
|
:param start: Если True, сразу логирует запуск проекта
|
||||||
|
"""
|
||||||
|
if self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Полная очистка настроек
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
# Создание директории для файловых логов
|
||||||
|
log_dir: Path = Path(getattr(LogConfig, 'DIR', 'logs'))
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Консольный лог
|
||||||
|
if getattr(LogConfig, 'CONSOLE', False):
|
||||||
|
logger.add(
|
||||||
|
sink=stderr,
|
||||||
|
format=self._log_format,
|
||||||
|
colorize=True,
|
||||||
|
level='DEBUG',
|
||||||
|
filter=lambda rec: rec['extra'].get('log_type') != 'DEBUG'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Файловые логи
|
||||||
|
if getattr(LogConfig, 'FILE', False):
|
||||||
|
# Общий лог
|
||||||
|
logger.add(
|
||||||
|
sink=log_dir / 'bot.log',
|
||||||
|
rotation=getattr(LogConfig, 'ROTATION', '100 MB'),
|
||||||
|
retention=getattr(LogConfig, 'RETENTION', '7 days'),
|
||||||
|
format=self._log_format,
|
||||||
|
level='DEBUG',
|
||||||
|
enqueue=True,
|
||||||
|
backtrace=True,
|
||||||
|
diagnose=True
|
||||||
|
)
|
||||||
|
# Раздельные логи по уровням
|
||||||
|
for level_name in ['INFO', 'WARNING', 'ERROR', 'DEBUG', 'CRITICAL']:
|
||||||
|
logger.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='Запуск проекта...',
|
||||||
|
log_type='START'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_user(message: Optional[Message] = None) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует имя пользователя из объекта Message.
|
||||||
|
|
||||||
|
:param message: Объект aiogram.types.Message
|
||||||
|
:return: Строка '@username' или 'id<user_id>'
|
||||||
|
"""
|
||||||
|
if message is None or message.from_user is None:
|
||||||
|
return '@System'
|
||||||
|
user: User = message.from_user
|
||||||
|
return f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
def log_entry(
|
||||||
|
self,
|
||||||
|
level: str,
|
||||||
|
text: str,
|
||||||
|
log_type: str,
|
||||||
|
user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Основной метод для записи логов.
|
||||||
|
|
||||||
|
:param level: Уровень логирования (например, 'INFO')
|
||||||
|
:param text: Сообщение для логирования
|
||||||
|
:param log_type: Кастомный тип лога (например, 'HANDLER')
|
||||||
|
:param user: Явно указанный пользователь
|
||||||
|
:param message: Объект Message для извлечения юзера
|
||||||
|
"""
|
||||||
|
actual_user: str = user or self.format_user(message)
|
||||||
|
logger.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: Категория лога
|
||||||
|
:param text: Кастомный текст сообщения
|
||||||
|
:return: Декорированную функцию
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: F) -> F:
|
||||||
|
is_coroutine = iscoroutinefunction(func)
|
||||||
|
action_text = text or f'Вызов {func.__name__}'
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
message = self._find_message(args)
|
||||||
|
self.log_entry(level, f"[START] {action_text}", log_type, message=message)
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
self.log_entry(level, f"[SUCCESS] {action_text}", log_type, message=message)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self.log_entry(
|
||||||
|
'ERROR',
|
||||||
|
f"[ERROR] {action_text} | Exception: {e!r}",
|
||||||
|
log_type,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
message = self._find_message(args)
|
||||||
|
self.log_entry(level, f"[START] {action_text}", log_type, message=message)
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
self.log_entry(level, f"[SUCCESS] {action_text}", log_type, message=message)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self.log_entry(
|
||||||
|
'ERROR',
|
||||||
|
f"[ERROR] {action_text} | Exception: {e!r}",
|
||||||
|
log_type,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return cast(F, async_wrapper if is_coroutine else sync_wrapper)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_message(args: tuple[Any, ...]) -> Optional[Message]:
|
||||||
|
"""
|
||||||
|
Ищет объект Message в аргументах функции.
|
||||||
|
|
||||||
|
:param args: Аргументы функции
|
||||||
|
:return: Найденный Message или None
|
||||||
|
"""
|
||||||
|
return next((arg for arg in args if isinstance(arg, Message)), None)
|
||||||
|
|
||||||
|
# Методы для прямого вызова
|
||||||
|
def debug(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
self.log_entry('DEBUG', text, log_type, user, message)
|
||||||
|
|
||||||
|
def info(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
self.log_entry('INFO', text, log_type, user, message)
|
||||||
|
|
||||||
|
def warning(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
self.log_entry('WARNING', text, log_type, user, message)
|
||||||
|
|
||||||
|
def error(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
self.log_entry('ERROR', text, log_type, user, message)
|
||||||
|
|
||||||
|
def critical(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
self.log_entry('CRITICAL', text, log_type, user, message)
|
||||||
|
|
||||||
|
|
||||||
|
# Создаем глобальный экземпляр логгера
|
||||||
|
loggers: Logger = Logger()
|
||||||
|
|
||||||
|
# Экспортируемые функции для обратной совместимости
|
||||||
|
setup_logging = loggers.setup
|
||||||
|
log = loggers.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
|
||||||
42
middleware/validators/url_vld.py
Normal file
42
middleware/validators/url_vld.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from re import Pattern, compile
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("valid_url", "url_to_text",)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_url(url: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли строка валидной ссылкой (URL).
|
||||||
|
|
||||||
|
:param url: Строка для проверки.
|
||||||
|
:return: True, если строка является валидным URL, иначе False.
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
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 e:
|
||||||
|
raise e # Перебрасываем ошибку выше для дальнейшей обработки или уведомления
|
||||||
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[project]
|
||||||
|
name = "primoexamplebot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "none"
|
||||||
|
authors = [
|
||||||
|
{name = "admin",email = "inkscaper0349@outlook.com"}
|
||||||
|
]
|
||||||
|
license = {text = "MIT License"}
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10,<4.0"
|
||||||
|
dependencies = [
|
||||||
|
"aiogram (>=3.22.0,<4.0.0)",
|
||||||
|
"loguru (>=0.7.3,<0.8.0)",
|
||||||
|
"pydantic-settings (>=2.10.1,<3.0.0)",
|
||||||
|
"sqlalchemy (>=2.0.43,<3.0.0)",
|
||||||
|
"babel (>=2.17.0,<3.0.0)",
|
||||||
|
"aiosqlite (>=0.21.0,<0.22.0)",
|
||||||
|
"email-validator (>=2.3.0,<3.0.0)",
|
||||||
|
"apscheduler (>=3.11.0,<4.0.0)",
|
||||||
|
"fastapi (>=0.116.1,<0.117.0)",
|
||||||
|
"uvicorn (>=0.35.0,<0.36.0)",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pytest = "^8.4.1"
|
||||||
|
pytest-asyncio = "^1.1.0"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = "test_*.py"
|
||||||
|
python_classes = "Test*"
|
||||||
|
python_functions = "test_*"
|
||||||
Reference in New Issue
Block a user