mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user