Первый коммит
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
|
||||
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
# .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/
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
18
.idea/PrimoExampleBot.iml
generated
Normal file
18
.idea/PrimoExampleBot.iml
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/database" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (PrimoExampleBot)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/locales" />
|
||||
</list>
|
||||
</option>
|
||||
</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/BotSystemResetRP.iml" filepath="$PROJECT_DIR$/.idea/BotSystemResetRP.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>
|
||||
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 |
2
bot/__init__.py
Normal file
2
bot/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .core import *
|
||||
from .handlers 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 *
|
||||
260
bot/core/bots.py
Normal file
260
bot/core/bots.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
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",)
|
||||
|
||||
|
||||
# Диспетчер бота, языковых настроек и его хранилища
|
||||
storage: MemoryStorage = MemoryStorage()
|
||||
dp: Dispatcher = Dispatcher(storage=storage)
|
||||
dp["is_active"]: bool = True
|
||||
|
||||
|
||||
# Инициализация i18n
|
||||
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
|
||||
url: str = None
|
||||
first_name: str = None
|
||||
last_name: str = None
|
||||
username: str = None
|
||||
description: str = None
|
||||
short_description: str = 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:
|
||||
"""
|
||||
Удаление или установка вебхука.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:param use_webhook: Статус использования вебхука, поумолчанию (true).
|
||||
:param webhook_url: Ссылка на вебхук.
|
||||
"""
|
||||
# Удаляем текущий вебхук
|
||||
await bots.delete_webhook(drop_pending_updates=True)
|
||||
|
||||
# Если включен вебхук — устанавливаем новый
|
||||
if use_webhook:
|
||||
if webhook_url is None:
|
||||
raise ValueError("Для установки вебхука необходимо указать webhook_url")
|
||||
await bots.set_webhook(webhook_url)
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
@log(level='INFO', log_type='BOT', text='Получение информации о боте')
|
||||
async def info(cls, bots: Bot = bot) -> dict:
|
||||
"""
|
||||
Получает и сохраняет информацию о боте.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:return: Словарь с персональными данными о боте.
|
||||
"""
|
||||
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.description = getattr(bot_info, 'description', '')
|
||||
cls.short_description = getattr(bot_info, 'short_description', '')
|
||||
cls.language_code = bot_info.language_code
|
||||
cls.is_premium = bot_info.is_premium
|
||||
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,
|
||||
'description': cls.description,
|
||||
'short_description': cls.short_description,
|
||||
'language_code': cls.language_code,
|
||||
'prefix': cls.prefix,
|
||||
'bot_owner': cls.bot_owner,
|
||||
'is_premium': cls.is_premium,
|
||||
'added_to_attachment_menu': cls.added_to_attachment_menu,
|
||||
'supports_inline_queries': cls.supports_inline_queries,
|
||||
'can_connect_to_business': cls.can_connect_to_business,
|
||||
'has_main_web_app': cls.has_main_web_app,
|
||||
'can_join_groups': cls.can_join_groups,
|
||||
'can_read_all_group_messages': cls.can_read_all_group_messages,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
@log(level='INFO', log_type='BOT', text='Установка прав администратора')
|
||||
async def set_administrator_rights(bots: Bot = bot, rights: ChatAdministratorRights = BotEdit.RIGHTS) -> None:
|
||||
"""
|
||||
Устанавливает права администратора по умолчанию.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:param rights: Заданные права администратора бота, по умолчанию словарь из конфигов.
|
||||
"""
|
||||
bot_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
|
||||
|
||||
if bot_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:
|
||||
"""
|
||||
Устанавливает имя бота из конфига.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:param new_name: Новое имя бота, по умолчанию из конфигов.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Устанавливает полное описание бота.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:param new_description: Новое описание бота, по умолчанию из конфигов.
|
||||
"""
|
||||
current_description: BotDescription = await bots.get_my_description()
|
||||
|
||||
if not (0 < len(new_description) <= 255):
|
||||
raise ValueError("Описание должно быть от 1 до 255 символов.")
|
||||
|
||||
if current_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:
|
||||
"""
|
||||
Устанавливает короткое описание виджета.
|
||||
|
||||
:param bots: Объект бота для управления.
|
||||
:param new_short: Новое короткое описание бота, по умолчанию из конфигов.
|
||||
"""
|
||||
current_short: BotShortDescription = await bots.get_my_short_description()
|
||||
|
||||
if not (0 < len(new_short) <= 512):
|
||||
raise ValueError("Короткое описание должно быть от 1 до 512 символов.")
|
||||
|
||||
if current_short != new_short:
|
||||
await bots.set_my_short_description(short_description=new_short)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def start_info_out() -> 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}")
|
||||
|
||||
# Печатаем все данные в консоль с задержкой в 1 секунду
|
||||
sleep(1)
|
||||
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:
|
||||
error: str = f"Ошибка при получении ID пользователя: {e}"
|
||||
raise error
|
||||
|
||||
|
||||
@classmethod
|
||||
@log(level='INFO', log_type='START', text='Процесс запуска бота!')
|
||||
async def setup(cls, bots: Bot = bot, perm: bool = Permission.BOT_EDIT):
|
||||
"""
|
||||
Выполняет полную настройку бота.
|
||||
|
||||
:param perm: Разрешение на изменения бота.
|
||||
:param bots: Объект бота для управления.
|
||||
"""
|
||||
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)
|
||||
cls.start_info_out()
|
||||
37
bot/core/webhook.py
Normal file
37
bot/core/webhook.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from uvicorn import Config, Server
|
||||
from aiogram.types import Update
|
||||
|
||||
from configs import Webhook
|
||||
from .bots import dp, bot
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = ("app", "config", "server",)
|
||||
|
||||
|
||||
# Создаём FastAPI приложение
|
||||
app: FastAPI = FastAPI()
|
||||
|
||||
# Создаём конфиг для uvicorn
|
||||
config: Config = Config(
|
||||
app="bot.core.webhook:app",
|
||||
host=Webhook.WEBAPP_HOST,
|
||||
port=Webhook.WEBAPP_PORT,
|
||||
log_level=Webhook.LOG_LEVEL, # выводить только предупреждения и ошибки
|
||||
access_log=Webhook.ACCES_LOG # <-- отключает все HTTP-access логи
|
||||
)
|
||||
|
||||
# Создание вебхук-сервера
|
||||
server: Server = Server(config)
|
||||
|
||||
|
||||
@app.post("/webhook")
|
||||
async def telegram_webhook(request: Request) -> dict[str, Any]:
|
||||
"""
|
||||
Обработчик POST-запроса от Telegram.
|
||||
"""
|
||||
update: Update = Update(**await request.json())
|
||||
await dp.feed_update(bot, update)
|
||||
return {"ok": True}
|
||||
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
|
||||
14
bot/handlers/__init__.py
Normal file
14
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from aiogram import Router
|
||||
from bot.handlers.commands import router as cmd_routers
|
||||
from .messages import router as messages_routers
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
cmd_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)
|
||||
13
bot/handlers/messages/__init__.py
Normal file
13
bot/handlers/messages/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from aiogram import Router
|
||||
from .default import router as default_message_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подготовка роутера команд
|
||||
#router.include_routers(
|
||||
#)
|
||||
|
||||
# Подключение стандартного роутера
|
||||
router.include_router(default_message_router)
|
||||
15
bot/handlers/messages/default.py
Normal file
15
bot/handlers/messages/default.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from bot.utils import type_msg
|
||||
from middleware.loggers import loggers
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "msg"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message()
|
||||
async def default_messages(message: Message | CallbackQuery) -> None:
|
||||
"""Обработчик всех необработанных сообщений."""
|
||||
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
47
bot/middlewares/__init__.py
Normal file
47
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from aiogram import Dispatcher
|
||||
|
||||
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,
|
||||
]
|
||||
|
||||
|
||||
def setup_middlewares(dp: Dispatcher, bot, channel_ids: list[int | str] = None) -> None:
|
||||
"""
|
||||
Регистрирует все middleware в диспетчере.
|
||||
"""
|
||||
channel_ids = 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
|
||||
29
bot/utils/interesting_facts.py
Normal file
29
bot/utils/interesting_facts.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from random import choice
|
||||
|
||||
from configs.config import Lists
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = ("interesting_fact",)
|
||||
|
||||
|
||||
def interesting_fact(mode: str = "факт", lists: list[str] = None) -> str:
|
||||
"""
|
||||
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
|
||||
|
||||
:param mode: Строка, определяющая тип контента ("факт", "анекдот", "цитата").
|
||||
:param lists: Необязательный список строк, из которого можно выбирать вручную.
|
||||
:return: Случайный элемент из соответствующего списка.
|
||||
"""
|
||||
if lists is not None:
|
||||
return choice(lists)
|
||||
|
||||
mode: str = 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)
|
||||
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
|
||||
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
|
||||
WEBAPP_HOST: Final[str] = settings.WEBAPP_HOST
|
||||
WEBAPP_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 ""
|
||||
|
||||
56
main.py
Normal file
56
main.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from asyncio import run
|
||||
|
||||
from bot import BotInfo, bot, dp, router
|
||||
from bot.core import server
|
||||
from bot.middlewares import setup_middlewares
|
||||
from database import db
|
||||
from configs import Webhook
|
||||
from middleware.loggers import setup_logging, loggers
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""
|
||||
Входная точка проекта.
|
||||
Настройка и запуск бота в режиме webhook или polling.
|
||||
"""
|
||||
try:
|
||||
# Логирование
|
||||
setup_logging()
|
||||
|
||||
# Cоздание базы данных
|
||||
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)
|
||||
|
||||
# Выбор режима работы: webhook или polling
|
||||
if Webhook.WEBHOOK:
|
||||
loggers.info(f"Запуск бота @{BotInfo.username} в режиме вебхука...\n")
|
||||
await server.serve()
|
||||
|
||||
else:
|
||||
loggers.info(f"Бот @{BotInfo.username} запущен в режиме polling...\n")
|
||||
await dp.start_polling(bot)
|
||||
|
||||
except Exception as e:
|
||||
loggers.error(f"🔥 Критическая ошибка при запуске: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main())
|
||||
0
middleware/__init__.py
Normal file
0
middleware/__init__.py
Normal file
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 # Перебрасываем ошибку выше для дальнейшей обработки или уведомления
|
||||
1577
poetry.lock
generated
Normal file
1577
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
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)",
|
||||
"uvicorn (>=0.35.0,<0.36.0)",
|
||||
"fastapi (>=0.116.1,<0.117.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)",
|
||||
]
|
||||
|
||||
|
||||
[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_*"
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/database/__init__.py
Normal file
0
tests/database/__init__.py
Normal file
106
tests/database/conftest.py
Normal file
106
tests/database/conftest.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
from asyncio import AbstractEventLoop
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import AsyncGenerator, Any, Generator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from database import BotDatabase, RoleRegion
|
||||
|
||||
# Добавляем путь к корню проекта
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop() -> Generator[AbstractEventLoop, Any, None]:
|
||||
"""
|
||||
Создаёт event loop для асинхронных тестов.
|
||||
Scope: session, чтобы использовать один loop на всю сессию тестов.
|
||||
"""
|
||||
policy = asyncio.get_event_loop_policy()
|
||||
loop: asyncio.AbstractEventLoop = policy.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def test_db() -> AsyncGenerator[BotDatabase, None]:
|
||||
"""
|
||||
Создаёт тестовую базу данных в памяти.
|
||||
Инициализирует тестовые роли.
|
||||
"""
|
||||
db: BotDatabase = BotDatabase("sqlite+aiosqlite:///:memory:", echo=False)
|
||||
await db.init_db()
|
||||
|
||||
# Инициализируем тестовые роли
|
||||
test_roles = [
|
||||
("Альбедо", RoleRegion.MONDSTADT),
|
||||
("Нахида", RoleRegion.SUMERU),
|
||||
("Кафка", RoleRegion.HSR_STAR),
|
||||
("Броння", RoleRegion.HSR_STAR),
|
||||
("Чжун Ли", RoleRegion.LIYUE)
|
||||
]
|
||||
await db.init_roles(test_roles)
|
||||
|
||||
yield db
|
||||
await db.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_session(test_db: BotDatabase) -> AsyncGenerator:
|
||||
"""
|
||||
Создаёт тестовую сессию для работы с БД.
|
||||
Scope: function (по умолчанию).
|
||||
"""
|
||||
async with test_db.session_factory() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_user(test_db: BotDatabase) -> int:
|
||||
"""
|
||||
Создаёт тестового пользователя.
|
||||
Возвращает user_id.
|
||||
"""
|
||||
user_id: int = 123456789
|
||||
await test_db.add_user(
|
||||
user_id=user_id,
|
||||
username="test_user",
|
||||
full_name="Test User"
|
||||
)
|
||||
return user_id
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_user_with_messages(test_db: BotDatabase, test_user: int) -> int:
|
||||
"""
|
||||
Создаёт пользователя с тестовыми сообщениями за разные периоды.
|
||||
Сообщения распределены по месяцам, неделям и дням.
|
||||
"""
|
||||
now: datetime = datetime.now(timezone.utc)
|
||||
|
||||
# Даты сообщений: > месяца назад, в текущем месяце, в текущей неделе, сегодня
|
||||
test_dates: list[datetime] = [
|
||||
now - timedelta(days=40),
|
||||
now - timedelta(days=35),
|
||||
now - timedelta(days=20),
|
||||
now - timedelta(days=15),
|
||||
now - timedelta(days=8),
|
||||
now - timedelta(days=5),
|
||||
now - timedelta(days=2),
|
||||
now - timedelta(hours=12),
|
||||
now - timedelta(hours=1),
|
||||
now
|
||||
]
|
||||
|
||||
for i, date in enumerate(test_dates):
|
||||
await test_db.add_message(
|
||||
user_id=test_user,
|
||||
message_text=f"Тестовое сообщение {i + 1}",
|
||||
created_at=date
|
||||
)
|
||||
|
||||
return test_user
|
||||
125
tests/database/test_messages.py
Normal file
125
tests/database/test_messages.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select, Sequence
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import UserMessage, BotDatabase
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestMessageManagement:
|
||||
"""Тесты для управления сообщениями с полной строгой типизацией"""
|
||||
|
||||
async def test_message_creation(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест создания сообщения.
|
||||
Проверяет, что сообщение успешно сохраняется в базе и содержит правильные данные.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
test_text: str = "Тестовое сообщение для проверки"
|
||||
|
||||
await test_db.add_message(user_id, test_text)
|
||||
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await test_session.execute(stmt)
|
||||
messages: Sequence[UserMessage] = result.scalars().all()
|
||||
|
||||
assert len(messages) == 1
|
||||
assert messages[0].message_text == test_text
|
||||
assert messages[0].user_id == user_id
|
||||
assert messages[0].created_at is not None
|
||||
|
||||
async def test_message_with_custom_date(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""
|
||||
Тест добавления сообщения с кастомной датой.
|
||||
Проверяет, что дата создания сохраняется корректно.
|
||||
"""
|
||||
user_id: int = 999888777
|
||||
custom_date: datetime = datetime(2024, 1, 15, 12, 30, 0, tzinfo=timezone.utc)
|
||||
|
||||
await test_db.add_user(user_id, "test_user", "Test User")
|
||||
await test_db.add_message(
|
||||
user_id=user_id,
|
||||
message_text="Сообщение с кастомной датой",
|
||||
created_at=custom_date
|
||||
)
|
||||
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await test_session.execute(stmt)
|
||||
messages: Sequence[UserMessage] = result.scalars().all()
|
||||
|
||||
assert len(messages) == 1
|
||||
db_date: datetime = messages[0].created_at
|
||||
if db_date.tzinfo is not None:
|
||||
db_date = db_date.replace(tzinfo=None)
|
||||
expected_date: datetime = custom_date.replace(tzinfo=None)
|
||||
assert db_date == expected_date
|
||||
|
||||
async def test_multiple_messages(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест добавления нескольких сообщений.
|
||||
Проверяет, что все сообщения корректно сохраняются в базе.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
# Удаляем старые сообщения
|
||||
async with test_db.session_factory() as session:
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
old_messages: Sequence[UserMessage] = result.scalars().all()
|
||||
for msg in old_messages:
|
||||
await session.delete(msg)
|
||||
await session.commit()
|
||||
|
||||
# Добавляем несколько сообщений
|
||||
for i in range(5):
|
||||
await test_db.add_message(
|
||||
user_id=user_id,
|
||||
message_text=f"Сообщение {i + 1}"
|
||||
)
|
||||
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await test_session.execute(stmt)
|
||||
messages: Sequence[UserMessage] = result.scalars().all()
|
||||
|
||||
assert len(messages) == 5
|
||||
|
||||
async def test_message_ordering(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест проверки порядка сообщений по дате создания.
|
||||
Сообщения должны возвращаться в порядке возрастания даты.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
# Очищаем старые сообщения
|
||||
async with test_db.session_factory() as session:
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
old_messages: Sequence[UserMessage] = result.scalars().all()
|
||||
for msg in old_messages:
|
||||
await session.delete(msg)
|
||||
await session.commit()
|
||||
|
||||
texts: List[str] = ["Сообщение 1", "Сообщение 2", "Сообщение 3"]
|
||||
|
||||
for text in texts:
|
||||
await test_db.add_message(user_id, text)
|
||||
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id).order_by(UserMessage.created_at.asc())
|
||||
result = await test_session.execute(stmt)
|
||||
messages: Sequence[UserMessage] = result.scalars().all()
|
||||
|
||||
assert len(messages) == 3
|
||||
assert messages[0].message_text == "Сообщение 1"
|
||||
assert messages[1].message_text == "Сообщение 2"
|
||||
assert messages[2].message_text == "Сообщение 3"
|
||||
207
tests/database/test_roles.py
Normal file
207
tests/database/test_roles.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import pytest
|
||||
from typing import List, Dict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import Role, RoleRegion, BotDatabase
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRoleSystem:
|
||||
"""Тесты для системы ролей с полной строгой типизацией"""
|
||||
|
||||
async def test_role_creation(self, test_db: BotDatabase, test_session: AsyncSession) -> None:
|
||||
"""
|
||||
Тест создания ролей.
|
||||
Проверяет, что тестовые роли существуют в базе.
|
||||
"""
|
||||
stmt = select(Role)
|
||||
result = await test_session.execute(stmt)
|
||||
roles: List[Role] = result.scalars().all()
|
||||
|
||||
assert len(roles) >= 5
|
||||
role_names: List[str] = [role.name for role in roles]
|
||||
assert "Альбедо" in role_names
|
||||
assert "Нахида" in role_names
|
||||
assert "Кафка" in role_names
|
||||
|
||||
async def test_assign_role(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест назначения роли пользователю.
|
||||
Проверяет успешное назначение свободной роли и правильное сохранение в БД.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
# Освобождаем роль на всякий случай
|
||||
await test_db.release_role("Альбедо")
|
||||
|
||||
# Назначаем роль
|
||||
success: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
assert success, "Не удалось назначить роль"
|
||||
|
||||
# Проверяем, что роль действительно назначена
|
||||
stmt = select(Role).where(Role.name == "Альбедо")
|
||||
result = await test_session.execute(stmt)
|
||||
role: Role = result.scalar_one()
|
||||
|
||||
assert role.occupied_by == user_id
|
||||
|
||||
async def test_assign_occupied_role(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест назначения уже занятой роли.
|
||||
Проверяет, что нельзя назначить роль, если она уже занята другим пользователем.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
other_user_id: int = 999000111
|
||||
|
||||
await test_db.release_role("Альбедо")
|
||||
await test_db.add_user(other_user_id, "other_user", "Other User")
|
||||
|
||||
# Назначаем роль первому пользователю
|
||||
success_first: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
assert success_first, "Не удалось назначить роль первому пользователю"
|
||||
|
||||
# Пытаемся назначить ту же роль другому пользователю
|
||||
success_second: bool = await test_db.assign_role("Альбедо", other_user_id)
|
||||
assert not success_second, "Нельзя назначить занятую роль"
|
||||
|
||||
async def test_release_role(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест освобождения роли.
|
||||
Проверяет, что роль успешно освобождается.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
await test_db.release_role("Нахида")
|
||||
success_assign: bool = await test_db.assign_role("Нахида", user_id)
|
||||
assert success_assign
|
||||
|
||||
success_release: bool = await test_db.release_role("Нахида")
|
||||
assert success_release
|
||||
|
||||
stmt = select(Role).where(Role.name == "Нахида")
|
||||
result = await test_session.execute(stmt)
|
||||
role: Role = result.scalar_one()
|
||||
assert role.occupied_by is None
|
||||
|
||||
async def test_release_unoccupied_role(self, test_db: BotDatabase) -> None:
|
||||
"""
|
||||
Тест освобождения свободной роли.
|
||||
Проверяет, что нельзя освободить уже свободную роль.
|
||||
"""
|
||||
await test_db.release_role("Кафка")
|
||||
success: bool = await test_db.release_role("Кафка")
|
||||
assert not success, "Нельзя освободить свободную роль"
|
||||
|
||||
async def test_get_user_roles(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест получения ролей пользователя.
|
||||
Проверяет, что возвращается корректный список назначенных ролей.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
await test_db.release_role("Альбедо")
|
||||
await test_db.release_role("Нахида")
|
||||
|
||||
success1: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
success2: bool = await test_db.assign_role("Нахида", user_id)
|
||||
assert success1 and success2
|
||||
|
||||
roles: List[str] = await test_db.get_roles_by_user(user_id)
|
||||
assert len(roles) == 2
|
||||
assert "Альбедо" in roles
|
||||
assert "Нахида" in roles
|
||||
|
||||
async def test_get_available_roles(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест получения доступных ролей.
|
||||
Проверяет, что назначенные роли не включены в список свободных.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
# Освобождаем все роли
|
||||
for role_name in ["Альбедо", "Нахида", "Кафка", "Броння", "Чжун Ли"]:
|
||||
await test_db.release_role(role_name)
|
||||
|
||||
# Назначаем одну роль
|
||||
success: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
assert success
|
||||
|
||||
available_roles: List[Role] = await test_db.get_available_roles()
|
||||
role_names: List[str] = [role.name for role in available_roles]
|
||||
|
||||
assert "Альбедо" not in role_names
|
||||
assert len(available_roles) > 0
|
||||
|
||||
for role in available_roles:
|
||||
assert role.occupied_by is None
|
||||
|
||||
async def test_get_occupied_roles(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест получения занятых ролей.
|
||||
Проверяет, что все назначенные роли возвращаются корректно.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
await test_db.release_role("Альбедо")
|
||||
await test_db.release_role("Нахида")
|
||||
|
||||
success1: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
success2: bool = await test_db.assign_role("Нахида", user_id)
|
||||
assert success1 and success2
|
||||
|
||||
occupied_roles: List[Role] = await test_db.get_occupied_roles()
|
||||
role_names: List[str] = [role.name for role in occupied_roles]
|
||||
|
||||
assert "Альбедо" in role_names
|
||||
assert "Нахида" in role_names
|
||||
assert len(occupied_roles) >= 2
|
||||
|
||||
async def test_region_filter(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""
|
||||
Тест фильтрации ролей по регионам.
|
||||
Проверяет, что метод возвращает роли только указанного региона.
|
||||
"""
|
||||
await test_db.release_role("Альбедо")
|
||||
mondstadt_roles: List[Role] = await test_db.get_available_roles(RoleRegion.MONDSTADT)
|
||||
|
||||
assert len(mondstadt_roles) == 1
|
||||
assert mondstadt_roles[0].name == "Альбедо"
|
||||
assert mondstadt_roles[0].region == RoleRegion.MONDSTADT
|
||||
|
||||
async def test_region_stats(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест статистики по регионам.
|
||||
Проверяет, что метод возвращает корректное количество занятых ролей по регионам.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
|
||||
await test_db.release_role("Альбедо")
|
||||
await test_db.release_role("Нахида")
|
||||
|
||||
success1: bool = await test_db.assign_role("Альбедо", user_id)
|
||||
success2: bool = await test_db.assign_role("Нахида", user_id)
|
||||
assert success1 and success2
|
||||
|
||||
stats: Dict[RoleRegion, Dict[str, int]] = await test_db.get_region_stats()
|
||||
|
||||
assert RoleRegion.MONDSTADT in stats
|
||||
assert RoleRegion.SUMERU in stats
|
||||
assert stats[RoleRegion.MONDSTADT]["occupied"] == 1
|
||||
assert stats[RoleRegion.MONDSTADT]["total"] == 1
|
||||
193
tests/database/test_user_stats.py
Normal file
193
tests/database/test_user_stats.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select, Sequence
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import User, UserMessage, BotDatabase
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUserStatistics:
|
||||
"""Тесты для статистики пользователей с полной строгой типизацией"""
|
||||
|
||||
async def test_add_user(self, test_db: BotDatabase, test_session: AsyncSession) -> None:
|
||||
"""
|
||||
Тест добавления пользователя.
|
||||
Проверяет, что пользователь создаётся с правильными данными и статусом 'active'.
|
||||
"""
|
||||
user_id: int = 111222333
|
||||
|
||||
await test_db.add_user(
|
||||
user_id=user_id,
|
||||
username="new_user",
|
||||
full_name="New User"
|
||||
)
|
||||
|
||||
user: User | None = await test_session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.username == "new_user"
|
||||
assert user.status.value == "active"
|
||||
|
||||
async def test_add_message_creates_user(
|
||||
self, test_db: BotDatabase, test_session: AsyncSession
|
||||
) -> None:
|
||||
"""
|
||||
Тест, что добавление сообщения создаёт пользователя, если его нет.
|
||||
Проверяет, что пользователь и сообщение корректно создаются.
|
||||
"""
|
||||
user_id: int = 111222333
|
||||
|
||||
await test_db.add_message(
|
||||
user_id=user_id,
|
||||
message_text="Тестовое сообщение"
|
||||
)
|
||||
|
||||
user: User | None = await test_session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "active"
|
||||
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await test_session.execute(stmt)
|
||||
messages: Sequence[UserMessage] = result.scalars().all()
|
||||
|
||||
assert len(messages) == 1
|
||||
assert messages[0].message_text == "Тестовое сообщение"
|
||||
|
||||
async def test_message_stats_calculation(
|
||||
self, test_db: BotDatabase, test_user_with_messages: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест расчёта статистики сообщений пользователя.
|
||||
Проверяет корректность статистики по дням, неделям, месяцам и общему количеству сообщений.
|
||||
"""
|
||||
user_id: int = test_user_with_messages
|
||||
|
||||
# Получаем статистику
|
||||
day: int
|
||||
week: int
|
||||
month: int
|
||||
total: int
|
||||
day, week, month, total = await test_db.get_message_stats(user_id)
|
||||
|
||||
assert total >= 10, f"Ожидается минимум 10 сообщений, получено {total}"
|
||||
assert day >= 0
|
||||
assert week >= 0
|
||||
assert month >= 0
|
||||
assert total >= 0
|
||||
assert day <= week <= month <= total
|
||||
|
||||
async def test_message_stats_with_dates(
|
||||
self, test_db: BotDatabase, test_user: int
|
||||
) -> None:
|
||||
"""
|
||||
Тест статистики с конкретными известными датами сообщений.
|
||||
Проверяет подсчёт сообщений за день, неделю, месяц и общее количество.
|
||||
"""
|
||||
user_id: int = test_user
|
||||
now: datetime = datetime.now(timezone.utc)
|
||||
|
||||
# Очищаем старые сообщения
|
||||
async with test_db.session_factory() as session:
|
||||
stmt = select(UserMessage).where(UserMessage.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
old_messages: Sequence[UserMessage] = result.scalars().all()
|
||||
for msg in old_messages:
|
||||
await session.delete(msg)
|
||||
await session.commit()
|
||||
|
||||
# Создаём сообщения с фиксированными датами
|
||||
test_messages: list[tuple[datetime, str]] = [
|
||||
(now - timedelta(days=45), "45 дней назад"),
|
||||
(now - timedelta(days=30), "30 дней назад"),
|
||||
(now - timedelta(days=15), "15 дней назад"),
|
||||
(now - timedelta(days=7), "7 дней назад"),
|
||||
(now - timedelta(days=3), "3 дня назад"),
|
||||
(now - timedelta(hours=6), "6 часов назад"),
|
||||
(now, "сейчас")
|
||||
]
|
||||
|
||||
for date, text in test_messages:
|
||||
await test_db.add_message(user_id, text, date)
|
||||
|
||||
day: int
|
||||
week: int
|
||||
month: int
|
||||
total: int
|
||||
day, week, month, total = await test_db.get_message_stats(user_id)
|
||||
|
||||
assert total == 7, f"Ожидалось 7 сообщений, получено {total}"
|
||||
|
||||
day_start: datetime = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_day: int = sum(1 for date, _ in test_messages if date >= day_start)
|
||||
assert day == expected_day, f"За день: ожидалось {expected_day}, получено {day}"
|
||||
|
||||
monday: datetime = (now - timedelta(days=now.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_week: int = sum(1 for date, _ in test_messages if date >= monday)
|
||||
assert week == expected_week, f"За неделю: ожидалось {expected_week}, получено {week}"
|
||||
|
||||
month_start: datetime = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_month: int = sum(1 for date, _ in test_messages if date >= month_start)
|
||||
assert month == expected_month, f"За месяц: ожидалось {expected_month}, получено {month}"
|
||||
|
||||
async def test_empty_user_stats(self, test_db: BotDatabase) -> None:
|
||||
"""
|
||||
Тест статистики для пользователя без сообщений.
|
||||
Все значения должны быть равны нулю.
|
||||
"""
|
||||
user_id: int = 0o00111222
|
||||
await test_db.add_user(user_id, "empty_user", "Empty User")
|
||||
|
||||
day: int
|
||||
week: int
|
||||
month: int
|
||||
total: int
|
||||
day, week, month, total = await test_db.get_message_stats(user_id)
|
||||
|
||||
assert day == 0
|
||||
assert week == 0
|
||||
assert month == 0
|
||||
assert total == 0
|
||||
|
||||
async def test_user_management(self, test_db: BotDatabase) -> None:
|
||||
"""
|
||||
Тест управления пользователями.
|
||||
Проверяет добавление, назначение админа, бан/разбан и возврат статуса пользователя.
|
||||
"""
|
||||
user_id: int = 555666777
|
||||
|
||||
# Добавление пользователя
|
||||
await test_db.add_user(user_id, "managed_user", "Managed User")
|
||||
|
||||
async with test_db.session_factory() as session:
|
||||
user: User | None = await session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "active"
|
||||
|
||||
# Назначение админом
|
||||
await test_db.set_admin(user_id, True)
|
||||
async with test_db.session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "admin"
|
||||
|
||||
# Бан пользователя
|
||||
await test_db.ban_user(user_id)
|
||||
async with test_db.session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "banned"
|
||||
|
||||
# Разбан
|
||||
await test_db.unban_user(user_id)
|
||||
async with test_db.session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "active"
|
||||
|
||||
# Снятие админки
|
||||
await test_db.set_admin(user_id, False)
|
||||
async with test_db.session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
assert user is not None
|
||||
assert user.status.value == "active"
|
||||
Reference in New Issue
Block a user