Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+368
View File
@@ -0,0 +1,368 @@
"""
Базовый класс для всех обработчиков команд.
Определяет общий интерфейс и предоставляет общие методы.
"""
import logging
import time
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from telegram import Update
from telegram.ext import ContextTypes
from core.exceptions import BotException, ValidationError, PermissionError
from utils.formatters import MessageFormatter
from core.monitoring import MetricsCollector
class BaseHandler(ABC):
"""
Абстрактный базовый класс для обработчиков команд.
Определяет общий интерфейс и предоставляет:
- Общие методы валидации
- Обработку ошибок
- Логирование действий
- Проверку прав доступа
"""
def __init__(self, config, metrics: MetricsCollector, message_formatter: MessageFormatter = None):
"""
Инициализация базового обработчика.
Args:
config: Конфигурация приложения
metrics: Сборщик метрик
message_formatter: Форматтер сообщений
"""
self.config = config
self.metrics = metrics
self.message_formatter = message_formatter or MessageFormatter()
self.logger = logging.getLogger(__name__)
def _ensure_utf8_encoding(self, text: str) -> str:
"""
Обеспечение корректной UTF-8 кодировки текста для Telegram API.
Args:
text: Исходный текст
Returns:
Текст с корректной кодировкой
"""
if isinstance(text, str):
# Проверяем, что текст корректно закодирован в UTF-8
try:
text.encode('utf-8')
return text
except UnicodeEncodeError:
# Если есть проблемы с кодировкой, используем безопасную версию
return text.encode('utf-8', errors='replace').decode('utf-8')
return str(text)
async def safe_execute(self, update: Update, context: ContextTypes, action: str, func, *args, **kwargs):
"""
Безопасное выполнение действия обработчика с обработкой ошибок.
Args:
update: Обновление от Telegram
context: Контекст бота
action: Название действия для логирования
func: Функция для выполнения
*args, **kwargs: Аргументы функции
Returns:
Результат выполнения или None при ошибке
"""
start_time = time.time()
try:
result = await func(update, context, *args, **kwargs)
duration = time.time() - start_time
if self.metrics:
self.metrics.record_command(action, self.__class__.__name__, duration)
return result
except BotException as e:
duration = time.time() - start_time
if self.metrics:
self.metrics.record_error(e.__class__.__name__, self.__class__.__name__, e)
await self._handle_bot_exception(update, e)
except Exception as e:
duration = time.time() - start_time
self.logger.error(f"Unexpected error in {action}: {e}", exc_info=True)
if self.metrics:
self.metrics.record_error(e.__class__.__name__, self.__class__.__name__, e)
await self._handle_unexpected_error(update, context, action, e)
async def _handle_bot_exception(self, update: Update, exception: BotException):
"""Обработка известных ошибок бота"""
error_message = f"{exception.message}"
if isinstance(exception, ValidationError):
error_message = f"⚠️ {exception.message}"
elif isinstance(exception, PermissionError):
error_message = f"🚫 {exception.message}"
await self._send_error_message(update, error_message)
async def _handle_unexpected_error(self, update: Update, context: ContextTypes, action: str, error: Exception):
"""Обработка неожиданных ошибок"""
# Более детальные сообщения об ошибках для отладки
if isinstance(error, TypeError) and "can't be used in 'await' expression" in str(error):
error_message = "Внутренняя ошибка: проблема с асинхронными операциями. Попробуйте позже."
elif isinstance(error, Exception) and "FOREIGN KEY" in str(error):
error_message = "Ошибка базы данных: нарушение целостности данных."
elif isinstance(error, Exception) and "database is locked" in str(error):
error_message = "База данных временно недоступна. Попробуйте позже."
else:
error_message = "Произошла неожиданная ошибка. Попробуйте позже."
# Логируем ошибку для администраторов
await self._log_error_to_admin(context, action, error)
await self._send_error_message(update, error_message)
async def send_response(self, update: Update, text: str, parse_mode: str = None, reply_markup=None):
"""
Унифицированная отправка ответов пользователю.
Args:
update: Обновление от Telegram
text: Текст сообщения
parse_mode: Режим разметки ('HTML', 'Markdown', etc.)
reply_markup: Клавиатура для сообщения
"""
try:
# Обеспечиваем корректную UTF-8 кодировку
safe_text = self._ensure_utf8_encoding(text)
# Определяем способ отправки на основе типа обновления
if update.message:
# Отправка нового сообщения
await update.message.reply_text(
safe_text,
parse_mode=parse_mode,
reply_markup=reply_markup
)
elif update.callback_query:
# Редактирование существующего сообщения
try:
await update.callback_query.edit_message_text(
safe_text,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except Exception as edit_error:
# Если редактирование невозможно, отправляем новое сообщение
self.logger.warning(f"Не удалось отредактировать сообщение: {edit_error}")
if update.callback_query.message:
await update.callback_query.message.reply_text(
safe_text,
parse_mode=parse_mode,
reply_markup=reply_markup
)
else:
# Fallback: отправка в чат
if update.effective_chat:
await update.effective_chat.send_message(
safe_text,
parse_mode=parse_mode,
reply_markup=reply_markup
)
else:
self.logger.error("Не удалось определить способ отправки сообщения")
except Exception as e:
if "TimedOut" in str(type(e)) or "timeout" in str(e).lower():
self.logger.warning(f"Timeout при отправке сообщения, пропускаем: {e}")
return # Не переотправляем при таймаутах
elif "Query is too old" in str(e):
self.logger.warning(f"Query устарел, пропускаем: {e}")
return # Не пытаемся отвечать на устаревшие queries
elif "Message is not modified" in str(e):
self.logger.warning(f"Сообщение не изменилось, пропускаем: {e}")
return # Игнорируем попытки редактирования неизменившихся сообщений
else:
self.logger.error(f"Не удалось отправить сообщение: {e}", exc_info=True)
async def _send_error_message(self, update: Update, message: str):
"""Отправка сообщения об ошибке пользователю"""
await self.send_response(update, message)
async def _log_error_to_admin(self, context: ContextTypes, action: str, error: Exception):
"""Логирование ошибки администраторам"""
try:
if self.config.bot_config.enable_developer_notifications:
developer_id = self.config.bot_config.developer_chat_id
if developer_id:
from datetime import datetime
error_text = (
"🚨 Ошибка в обработчике\n\n"
f"Действие: {action}\n"
f"Ошибка: {str(error)}\n"
f"Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
# Обеспечиваем корректную UTF-8 кодировку
safe_error_text = self._ensure_utf8_encoding(error_text)
await context.bot.send_message(
chat_id=developer_id,
text=safe_error_text
)
except Exception as e:
self.logger.error(f"Не удалось отправить ошибку разработчику: '{e}'", exc_info=True)
def validate_user_id(self, user_id: str) -> int:
"""
Валидация и преобразование ID пользователя.
Args:
user_id: Строка с ID пользователя
Returns:
Целое число ID
Raises:
ValidationError: Если ID некорректен
"""
try:
uid = int(user_id)
if not (1 <= uid <= 2147483647):
raise ValidationError("Неверный диапазон ID пользователя")
return uid
except ValueError:
raise ValidationError("ID пользователя должен быть числом")
def validate_chat_id(self, chat_id: str) -> int:
"""
Валидация и преобразование ID чата.
Args:
chat_id: Строка с ID чата
Returns:
Целое число ID
Raises:
ValidationError: Если ID некорректен
"""
try:
cid = int(chat_id)
if not (-2147483648 <= cid <= 2147483647):
raise ValidationError("Неверный диапазон ID чата")
return cid
except ValueError:
raise ValidationError("ID чата должен быть числом")
async def is_admin(self, update: Update, user_id: int) -> bool:
"""
Проверка прав администратора через PermissionManager.
Args:
update: Обновление от Telegram
user_id: ID пользователя
Returns:
True если пользователь администратор или супер-администратор
"""
from core.permissions import permission_manager, UserRole
# Получаем эффективную роль пользователя
effective_role = await permission_manager.get_effective_role(update, user_id, self.config)
# Проверяем, является ли роль административной
return effective_role in [UserRole.ADMIN, UserRole.SUPER_ADMIN]
async def require_admin(self, update: Update, user_id: int):
"""
Проверка прав администратора с генерацией исключения.
Args:
update: Обновление от Telegram
user_id: ID пользователя
Raises:
PermissionError: Если пользователь не администратор
"""
if not await self.is_admin(update, user_id):
raise PermissionError("Эта команда доступна только администраторам")
def extract_user_id_from_args(self, args: list) -> Optional[int]:
"""
Извлечение ID пользователя из аргументов команды.
Args:
args: Список аргументов команды
Returns:
ID пользователя или None
"""
if not args:
return None
try:
return self.validate_user_id(args[0])
except ValidationError:
return None
def extract_amount_from_args(self, args: list, default: float = None) -> Optional[float]:
"""
Извлечение суммы из аргументов команды.
Args:
args: Список аргументов команды
default: Значение по умолчанию
Returns:
Сумма или None
"""
if not args:
return default
try:
amount = float(args[0])
if amount <= 0:
return None
return amount
except (ValueError, IndexError):
return None
def format_user_mention(self, user_id: int, name: str) -> str:
"""
Форматирование упоминания пользователя.
Args:
user_id: ID пользователя
name: Имя пользователя
Returns:
Отформатированная строка упоминания
"""
return f"[{name}](tg://user?id={user_id})"
@abstractmethod
def get_command_handlers(self) -> Dict[str, callable]:
"""
Получение словаря обработчиков команд.
Returns:
Словарь {команда: функция_обработчик}
"""
pass
@abstractmethod
def get_callback_handlers(self) -> Dict[str, callable]:
"""
Получение словаря обработчиков callback запросов.
Returns:
Словарь {callback_data: функция_обработчик}
"""
pass
@abstractmethod
def get_message_handlers(self) -> Dict[str, callable]:
"""
Получение словаря обработчиков сообщений.
Returns:
Словарь {тип_сообщения: функция_обработчик}
"""
pass