Первый коммит

This commit is contained in:
2026-02-17 11:24:55 +07:00
commit a06448ca4b
109 changed files with 21165 additions and 0 deletions

5
bot/core/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""
Модуль управления ботом
"""
from .bots import *
from .webhook import *

398
bot/core/bots.py Normal file
View File

@@ -0,0 +1,398 @@
"""
Ядро PrimoGuard Bot: Инициализация, Управление и Информация
"""
from datetime import datetime
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 pymorphy3 import MorphAnalyzer
from configs import settings
from middleware.loggers import logger
__all__ = ('bot', 'dp', 'storage', 'i18n', 'morph', 'BotInfo')
# ================= STORAGE И DISPATCHER =================
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
dp["is_active"] = True
# ================= ИНТЕРНАЦИОНАЛИЗАЦИЯ =================
i18n = I18n(path="locales", default_locale="ru", domain="bot")
i18n_middleware = SimpleI18nMiddleware(i18n=i18n)
i18n_middleware.setup(dp)
# ================= БОТ =================
bot = Bot(
token=settings.active_bot_token,
default=DefaultBotProperties(
parse_mode=settings.PARSE_MODE,
disable_notification=settings.DISABLE_NOTIFICATION,
protect_content=settings.PROTECT_CONTENT,
allow_sending_without_reply=settings.ALLOW_SENDING_WITHOUT_REPLY,
link_preview_is_disabled=settings.LINK_PREVIEW_IS_DISABLED,
link_preview_prefer_small_media=settings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
link_preview_prefer_large_media=settings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
link_preview_show_above_text=settings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
show_caption_above_media=settings.SHOW_CAPTION_ABOVE_MEDIA
)
)
# ================= МОРФОАНАЛИЗАТОР =================
morph = MorphAnalyzer()
# ================= КЛАСС УПРАВЛЕНИЯ БОТОМ =================
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
is_premium: bool = False
# Возможности бота
can_join_groups: bool = False
can_read_all_group_messages: bool = False
supports_inline_queries: bool = False
can_connect_to_business: bool = False
has_main_web_app: bool = False
added_to_attachment_menu: bool = False
# Данные из конфига
prefix: str = settings.PREFIX
started_at: datetime = None
@classmethod
def mention(cls) -> str:
"""Упоминание бота"""
return f'@{cls.username}' if cls.username else f'id{cls.id}'
@classmethod
async def webhook(cls, bots: Bot = bot) -> None:
"""
Настраивает webhook для бота.
Args:
bots: Объект бота для управления
"""
# Только если включен режим webhook
if not settings.WEBHOOK:
logger.debug("Режим Webhook отключен (WEBHOOK=False)", log_type='WEBHOOK')
return
# Проверяем наличие URL
if not settings.WEBHOOK_URL:
logger.warning(
"⚠️ WEBHOOK_URL не указан в настройках",
log_type='WEBHOOK'
)
return
try:
logger.info("Настройка вебхука бота", log_type='BOT')
# Проверяем текущий webhook
current_info = await bots.get_webhook_info()
# Если уже установлен нужный URL, пропускаем
if current_info.url == settings.WEBHOOK_URL:
logger.info(
f"✓ Вебхук уже установлен: {settings.WEBHOOK_URL}",
log_type='BOT'
)
return
# Устанавливаем webhook
await bots.set_webhook(
url=settings.WEBHOOK_URL,
secret_token=settings.SECRET_TOKEN,
drop_pending_updates=True
)
logger.success(
f"✓ Вебхук установлен: {settings.WEBHOOK_URL}",
log_type='BOT'
)
except Exception as e:
logger.error(
f"❌ Ошибка установки вебхука: {e}",
log_type='BOT'
)
@classmethod
async def info(cls, bots: Bot = bot) -> dict:
"""
Получает и сохраняет информацию о боте.
:param bots: Объект бота для управления
:return: Словарь с данными о боте
"""
logger.info("Получение информации о боте", log_type='BOT')
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.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)
cls.supports_inline_queries = bot_info.supports_inline_queries or False
cls.can_connect_to_business = bot_info.can_connect_to_business or False
cls.has_main_web_app = bot_info.has_main_web_app or False
cls.added_to_attachment_menu = bot_info.added_to_attachment_menu or False
cls.started_at = datetime.now()
logger.success(f"Информация о боте @{cls.username} получена", log_type='BOT')
return {
'id': cls.id,
'url': cls.url,
'first_name': cls.first_name,
'last_name': cls.last_name,
'username': cls.username,
'prefix': cls.prefix,
'is_premium': cls.is_premium,
'can_join_groups': cls.can_join_groups,
'can_read_all_group_messages': cls.can_read_all_group_messages,
'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,
'added_to_attachment_menu': cls.added_to_attachment_menu,
}
@staticmethod
async def set_name(bots: Bot = bot, new_name: str = None) -> bool:
"""Устанавливает имя бота"""
new_name = new_name or settings.BOT_NAME
if not (1 <= len(new_name) <= 64):
logger.error(f"Имя бота должно быть от 1 до 64 символов (текущее: {len(new_name)})", log_type='BOT_SETUP')
return False
try:
current_name = (await bots.get_me()).first_name
if current_name == new_name:
logger.debug(f"Имя бота уже установлено: '{current_name}'", log_type='BOT_SETUP')
return False
await bots.set_my_name(new_name)
logger.success(f"Имя бота изменено: '{current_name}''{new_name}'", log_type='BOT_SETUP')
return True
except Exception as e:
logger.error(f"Ошибка установки имени бота: {e}", log_type='BOT_SETUP')
return False
@staticmethod
async def set_description(bots: Bot = bot, new_description: str = None) -> bool:
"""Устанавливает полное описание бота"""
new_description = new_description or settings.BOT_DESCRIPTION
if not (0 < len(new_description) <= 512):
logger.error(f"Описание должно быть от 1 до 512 символов (текущее: {len(new_description)})", log_type='BOT_SETUP')
return False
try:
current_description: BotDescription = await bots.get_my_description()
current_text = current_description.description if current_description else ""
if current_text == new_description:
logger.debug("Описание бота уже установлено", log_type='BOT_SETUP')
return False
await bots.set_my_description(description=new_description)
logger.success("Описание бота обновлено", log_type='BOT_SETUP')
return True
except Exception as e:
logger.error(f"Ошибка установки описания бота: {e}", log_type='BOT_SETUP')
return False
@staticmethod
async def set_short_description(bots: Bot = bot, new_short: str = None) -> bool:
"""Устанавливает короткое описание бота"""
new_short = new_short or settings.BOT_SHORT_DESCRIPTION
if not (0 < len(new_short) <= 120):
logger.error(f"Короткое описание должно быть от 1 до 120 символов (текущее: {len(new_short)})", log_type='BOT_SETUP')
return False
try:
current_short: BotShortDescription = await bots.get_my_short_description()
current_text = current_short.short_description if current_short else ""
if current_text == new_short:
logger.debug("Короткое описание бота уже установлено", log_type='BOT_SETUP')
return False
await bots.set_my_short_description(short_description=new_short)
logger.success("Короткое описание бота обновлено", log_type='BOT_SETUP')
return True
except Exception as e:
logger.error(f"Ошибка установки короткого описания: {e}", log_type='BOT_SETUP')
return False
@staticmethod
async def set_administrator_rights(bots: Bot = bot, rights: ChatAdministratorRights = None) -> bool:
"""Устанавливает права администратора по умолчанию"""
rights = rights or settings.rights
try:
current_rights = await bots.get_my_default_administrator_rights()
if current_rights == rights:
logger.debug("Права администратора уже установлены", log_type='BOT_SETUP')
return False
await bots.set_my_default_administrator_rights(rights)
logger.success("Права администратора обновлены", log_type='BOT_SETUP')
return True
except Exception as e:
logger.error(f"Ошибка установки прав администратора: {e}", log_type='BOT_SETUP')
return False
@classmethod
def print(cls, to_console: bool = True, to_file: bool = True) -> str:
"""
Красиво форматирует и выводит информацию о боте.
:param to_console: Вывести в консоль
:param to_file: Записать в файлы
:return: Отформатированная строка
"""
# Формирование блоков информации
header = f"╔═══════════════════════════════════════════════════════════╗"
title = f"║ 🤖 PRIMOGUARD BOT - ИНФОРМАЦИЯ О ЗАПУСКЕ ║"
separator = f"╠═══════════════════════════════════════════════════════════╣"
footer = f"╚═══════════════════════════════════════════════════════════╝"
lines = [
header,
title,
separator,
f"║ ⏰ Время запуска: {cls.started_at.strftime('%d.%m.%Y %H:%M:%S')}",
f"",
f"║ 📋 ОСНОВНАЯ ИНФОРМАЦИЯ:",
f"║ • Имя: {cls.first_name} {cls.last_name or ''}".ljust(60) + "",
f"║ • Username: @{cls.username}".ljust(60) + "",
f"║ • ID: {cls.id}".ljust(60) + "",
f"",
f"║ ⚙️ ВОЗМОЖНОСТИ БОТА:",
f"║ • Вступать в группы: {'' if cls.can_join_groups else ''}".ljust(60) + "",
f"║ • Читать все сообщения: {'' if cls.can_read_all_group_messages else ''}".ljust(60) + "",
f"║ • Инлайн-запросы: {'' if cls.supports_inline_queries else ''}".ljust(60) + "",
f"║ • Бизнес-аккаунты: {'' if cls.can_connect_to_business else ''}".ljust(60) + "",
f"║ • Веб-приложение: {'' if cls.has_main_web_app else ''}".ljust(60) + "",
f"║ • Меню вложений: {'' if cls.added_to_attachment_menu else ''}".ljust(60) + "",
f"",
f"║ 🔧 НАСТРОЙКИ:",
f"║ • Префикс команд: {cls.prefix}".ljust(60) + "",
f"║ • Режим: {'Webhook' if settings.WEBHOOK else 'Polling'}".ljust(60) + "",
footer
]
output = '\n'.join(lines)
# Вывод в консоль с цветом
if to_console and settings.START_INFO_CONSOLE:
colored_output = f"\033[96m{output}\033[0m" # Cyan цвет
print(colored_output)
# Запись в файлы
if to_file and settings.START_INFO_TO_FILE:
try:
settings.LOG_DIR.mkdir(parents=True, exist_ok=True)
# Полная информация в bot_info.log
info_file = settings.LOG_DIR / 'bot_info.log'
with open(info_file, 'w', encoding='utf-8') as f:
f.write(output)
# Краткая запись в историю запусков
start_file = settings.LOG_DIR / 'bot_starts.log'
with open(start_file, 'a', encoding='utf-8') as f:
start_entry = f"{cls.started_at.strftime('%d.%m.%Y %H:%M:%S')} | @{cls.username} | Mode: {'Webhook' if settings.WEBHOOK else 'Polling'}\n"
f.write(start_entry)
logger.debug(f"Информация о боте записана в {info_file}", log_type='BOT_INFO')
except Exception as e:
logger.error(f"Ошибка записи информации в файл: {e}", log_type='BOT_INFO')
return output
@classmethod
async def setup(
cls,
bots: Bot = bot,
perm: bool = None,
setup_webhook: bool = True
) -> None:
"""
Выполняет полную настройку бота.
Args:
bots: Объект бота для управления
perm: Разрешение на изменения (если None, берется из настроек)
setup_webhook: Устанавливать ли webhook (по умолчанию True)
"""
perm = perm if perm is not None else settings.BOT_EDIT
logger.info("🚀 Процесс запуска бота!", log_type='START')
# Настройка вебхука (только если разрешено)
if setup_webhook:
await cls.webhook(bots=bots)
# Получение информации
await cls.info(bots=bots)
# Обновление профиля (если разрешено)
if perm:
logger.info("Начало настройки профиля бота...", log_type='BOT_SETUP')
results = {
'name': await cls.set_name(bots=bots),
'description': await cls.set_description(bots=bots),
'short_description': await cls.set_short_description(bots=bots),
'admin_rights': await cls.set_administrator_rights(bots=bots)
}
changed_count = sum(results.values())
logger.info(
f"Настройка завершена. Изменено параметров: {changed_count}/4",
log_type='BOT_SETUP'
)
else:
logger.warning(
"⚠️ Изменение настроек бота отключено (BOT_EDIT=False)",
log_type='BOT_SETUP'
)
# Вывод красивой информации
cls.print()

259
bot/core/webhook.py Normal file
View File

@@ -0,0 +1,259 @@
"""
Управление вебхуком бота через класс-менеджер
"""
import secrets
from typing import Optional
from aiohttp import web
from aiogram import Bot, Dispatcher
from aiogram.types import WebhookInfo
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from configs import settings
from middleware.loggers import logger
__all__ = ('WebhookManager',)
class WebhookManager:
"""
Менеджер для управления webhook режимом.
Инкапсулирует всю логику работы с webhook:
- Создание aiohttp приложения
- Регистрация handlers
- Установка/удаление webhook
- Запуск webhook сервера
Attributes:
bot: Экземпляр бота
dp: Диспетчер
app: aiohttp приложение
secret_token: Секретный токен для webhook
"""
def __init__(self, bot: Bot, dp: Dispatcher):
"""
Args:
bot: Экземпляр бота
dp: Диспетчер
"""
self.bot = bot
self.dp = dp
self.app = web.Application()
self._configured = False
# Генерируем или используем существующий токен
self.secret_token = self._get_or_generate_token()
def _get_or_generate_token(self) -> str:
"""
Получает токен из настроек или генерирует новый.
Returns:
str: Секретный токен
"""
if hasattr(settings, 'SECRET_TOKEN') and settings.SECRET_TOKEN:
logger.debug("Используется SECRET_TOKEN из настроек", log_type='WEBHOOK')
return settings.SECRET_TOKEN
# Генерируем случайный токен (32 символа)
token = secrets.token_urlsafe(32)
logger.info(
f"🔐 Сгенерирован новый SECRET_TOKEN: {token[:8]}...",
log_type='WEBHOOK'
)
return token
async def get_info(self) -> WebhookInfo:
"""
Получает информацию о текущем вебхуке.
Returns:
WebhookInfo: Информация о вебхуке
"""
try:
info = await self.bot.get_webhook_info()
logger.debug(
f"Webhook URL: {info.url or 'не установлен'}",
log_type='WEBHOOK'
)
return info
except Exception as e:
logger.error(
f"Ошибка получения информации о вебхуке: {e}",
log_type='WEBHOOK'
)
raise
async def delete(self, drop_pending_updates: bool = True) -> bool:
"""
Удаляет текущий вебхук.
Args:
drop_pending_updates: Удалить накопленные обновления
Returns:
bool: True если удаление успешно
"""
try:
result = await self.bot.delete_webhook(
drop_pending_updates=drop_pending_updates
)
if result:
logger.success("✓ Вебхук успешно удален", log_type='WEBHOOK')
else:
logger.debug("Вебхук не был установлен", log_type='WEBHOOK')
return result
except Exception as e:
logger.error(f"Ошибка удаления вебхука: {e}", log_type='WEBHOOK')
return False
async def setup(
self,
webhook_url: Optional[str] = None,
secret_token: Optional[str] = None,
drop_pending_updates: bool = True
) -> bool:
"""
Устанавливает вебхук для бота.
Args:
webhook_url: URL вебхука (если None, берется из settings)
secret_token: Секретный токен (если None, используется self.secret_token)
drop_pending_updates: Удалить накопленные обновления
Returns:
bool: True если установка успешна
"""
url = webhook_url or settings.WEBHOOK_URL
token = secret_token or self.secret_token
if not url:
logger.error("WEBHOOK_URL не установлен", log_type='WEBHOOK')
return False
try:
# Проверяем текущий webhook
current_info = await self.bot.get_webhook_info()
# Если уже установлен правильный URL, не трогаем
if current_info.url == url:
logger.info(
f"✓ Webhook уже установлен на {url}",
log_type='WEBHOOK'
)
return True
# Удаляем старый webhook если есть
if current_info.url:
logger.debug(f"Удаление старого webhook: {current_info.url}", log_type='WEBHOOK')
await self.delete(drop_pending_updates=drop_pending_updates)
# Небольшая задержка
import asyncio
await asyncio.sleep(0.5)
# Устанавливаем новый
result = await self.bot.set_webhook(
url=url,
secret_token=token,
drop_pending_updates=drop_pending_updates
)
if result:
logger.success(f"✓ Вебхук установлен: {url}", log_type='WEBHOOK')
else:
logger.error("Не удалось установить вебхук", log_type='WEBHOOK')
return result
except Exception as e:
logger.error(f"❌ Ошибка установки вебхука: {e}", log_type='WEBHOOK')
return False
def configure(
self,
webhook_path: Optional[str] = None,
secret_token: Optional[str] = None
) -> None:
"""
Конфигурирует webhook handler для aiohttp app.
Args:
webhook_path: Путь для webhook (если None, извлекается из WEBHOOK_URL)
secret_token: Секретный токен (если None, используется self.secret_token)
"""
if self._configured:
logger.warning("Webhook уже сконфигурирован", log_type='WEBHOOK')
return
# Определяем путь из WEBHOOK_URL
if webhook_path:
path = webhook_path
elif settings.WEBHOOK_URL:
from urllib.parse import urlparse
parsed = urlparse(settings.WEBHOOK_URL)
path = parsed.path if parsed.path else "/webhook"
else:
path = "/webhook"
# Используем токен
token = secret_token or self.secret_token
# Создаём webhook handler
webhook_handler = SimpleRequestHandler(
dispatcher=self.dp,
bot=self.bot,
secret_token=token
)
# Регистрируем в aiohttp app
webhook_handler.register(self.app, path=path)
setup_application(self.app, self.dp, bot=self.bot)
self._configured = True
logger.success(
f"✓ Webhook handler настроен на путь: {path}",
log_type='WEBHOOK'
)
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
access_log: Optional[bool] = None
) -> None:
"""
Запускает webhook сервер (блокирующий вызов).
Args:
host: Хост сервера (если None, берется из settings)
port: Порт сервера (если None, берется из settings)
access_log: Логировать запросы (если None, берется из settings)
"""
if not self._configured:
logger.error(
"Webhook не сконфигурирован! Вызовите configure() перед run()",
log_type='WEBHOOK'
)
return
host = host or settings.WEBAPP_HOST
port = port or settings.WEBAPP_PORT
access_log_enabled = access_log if access_log is not None else settings.ACCES_LOG
logger.info(
f"🌐 Запуск webhook сервера: {host}:{port}",
log_type='WEBHOOK'
)
web.run_app(
self.app,
host=host,
port=port,
access_log=logger if access_log_enabled else None
)