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

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
+382
View File
@@ -0,0 +1,382 @@
"""
Middleware для обработки команд и проверки прав доступа.
Реализует цепочку middleware: валидация → логирование → авторизация → обработка.
"""
import time
import logging
from typing import Dict, Any, Optional, Callable, Awaitable
from telegram import Update
from telegram.ext import ContextTypes
from .permissions import permission_manager, UserRole
from .exceptions import PermissionError, ValidationError
from .rate_limiter import rate_limiter, RateLimitExceeded
class MiddlewareContext:
"""
Контекст выполнения middleware.
Содержит информацию о текущем запросе и пользователе.
"""
def __init__(self, update: Update, context: ContextTypes):
self.update = update
self.context = context
self.user_id: Optional[int] = None
self.user_role: Optional[UserRole] = None
self.command: Optional[str] = None
self.args: list = []
self.metadata: Dict[str, Any] = {}
self.registration_time: Optional[float] = None # Время регистрации пользователя
async def initialize(self, config):
"""Инициализация контекста"""
# Получаем информацию о пользователе
if hasattr(self.update, 'effective_user') and self.update.effective_user:
user = self.update.effective_user
self.user_id = user.id
# Используем тот же конфиг, что и в Application
self.user_role = await permission_manager.get_effective_role(
self.update, self.user_id, config
)
print(f"[DEBUG] MiddlewareContext.initialize: user {self.user_id} role = {self.user_role}, config type: {type(config)}")
if hasattr(config, 'bot_config'):
print(f"[DEBUG] Config has super_admin_ids: {config.bot_config.super_admin_ids}")
print(f"[DEBUG] Config has admin_ids: {config.bot_config.admin_ids}")
# Получаем время регистрации пользователя из базы данных
# (предполагаем, что есть метод для получения информации о пользователе)
try:
from ..database import get_user_registration_time
self.registration_time = await get_user_registration_time(self.user_id)
except Exception:
# Если не удается получить время регистрации, оставляем None
self.registration_time = None
# Парсим команду из сообщения или callback
if hasattr(self.update, 'message') and self.update.message and hasattr(self.update.message, 'text') and self.update.message.text:
text = self.update.message.text.strip()
if text.startswith('/'):
parts = text.split()
self.command = parts[0][1:].split('@')[0] # Убираем слеш и username бота
self.args = parts[1:] if len(parts) > 1 else []
elif hasattr(self.update, 'callback_query') and self.update.callback_query and hasattr(self.update.callback_query, 'data') and self.update.callback_query.data:
# Для callback'ов парсим команду из callback_data
callback_data = self.update.callback_query.data
# Если callback_data соответствует команде меню или действия, устанавливаем как команду
if callback_data.startswith(('menu_', 'donate_')) or callback_data in ['help', 'rank', 'leaderboard', 'info', 'start']:
self.command = callback_data
self.args = []
class MiddlewareResult:
"""Результат выполнения middleware"""
def __init__(self, success: bool = True, error_message: str = None, stop_processing: bool = False):
self.success = success
self.error_message = error_message
self.stop_processing = stop_processing
@classmethod
def success(cls):
"""Успешный результат"""
return cls(success=True)
@classmethod
def error(cls, message: str, stop_processing: bool = True):
"""Результат с ошибкой"""
return cls(success=False, error_message=message, stop_processing=stop_processing)
@classmethod
def stop(cls):
"""Остановить обработку без ошибки"""
return cls(success=True, stop_processing=True)
class BaseMiddleware:
"""
Базовый класс для middleware.
"""
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""
Обработка запроса в middleware.
Args:
context: Контекст middleware
config: Конфигурация приложения
Returns:
Результат обработки
"""
raise NotImplementedError("Middleware must implement process method")
class ValidationMiddleware(BaseMiddleware):
"""
Middleware для валидации входящих данных.
"""
def __init__(self, logger=None):
self.logger = logger or logging.getLogger(__name__)
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""Валидация данных"""
try:
# Проверяем наличие пользователя
if not context.user_id:
return MiddlewareResult.error("Не удалось определить пользователя")
# Проверяем корректность команды
if context.command:
# Базовая валидация названия команды
if not context.command.replace('_', '').isalnum():
return MiddlewareResult.error("Неверный формат команды")
# Проверяем длину аргументов
for arg in context.args:
if len(arg) > 1000: # Ограничение на длину аргумента
return MiddlewareResult.error("Слишком длинный аргумент команды")
self.logger.debug(f"Validation passed for user {context.user_id}, command: {context.command}")
return MiddlewareResult.success()
except Exception as e:
self.logger.error(f"Validation error: {e}")
return MiddlewareResult.error("Ошибка валидации данных")
class LoggingMiddleware(BaseMiddleware):
"""
Middleware для логирования действий пользователей.
"""
def __init__(self, logger=None):
self.logger = logger or logging.getLogger(__name__)
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""Логирование действия"""
try:
user_info = f"user_{context.user_id}" if context.user_id else "unknown_user"
command_info = context.command or "unknown_command"
args_info = f" with {len(context.args)} args" if context.args else ""
self.logger.info(f"Command executed: {user_info} -> /{command_info}{args_info}")
# Сохраняем время начала в метаданных
context.metadata['start_time'] = time.time()
return MiddlewareResult.success()
except Exception as e:
self.logger.error(f"Logging error: {e}")
return MiddlewareResult.error("Ошибка логирования")
class AuthorizationMiddleware(BaseMiddleware):
"""
Middleware для проверки прав доступа.
"""
def __init__(self, logger=None):
self.logger = logger or logging.getLogger(__name__)
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""Проверка авторизации"""
try:
# Если команда не указана, пропускаем проверку (например, для callback'ов)
if not context.command:
self.logger.debug(f"Authorization skipped for user {context.user_id} (no command)")
return MiddlewareResult.success()
# Проверяем права доступа
if not permission_manager.can_execute_command(context.user_role, context.command):
role_name = context.user_role.value if context.user_role else "unknown"
self.logger.warning(f"Access denied: user {context.user_id} (role: {role_name}) "
f"tried to execute /{context.command}")
return MiddlewareResult.error("У вас нет прав для выполнения этой команды")
self.logger.debug(f"Authorization passed for user {context.user_id}, command: {context.command}")
return MiddlewareResult.success()
except Exception as e:
self.logger.error(f"Authorization error: {e}")
return MiddlewareResult.error("Ошибка проверки прав доступа")
class RateLimitMiddleware(BaseMiddleware):
"""
Middleware для ограничения частоты запросов (rate limiting).
"""
def __init__(self, logger=None):
self.logger = logger or logging.getLogger(__name__)
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""Проверка rate limit"""
try:
# Пропускаем проверку для не-команд (сообщения, callback'и)
if not context.command:
return MiddlewareResult.success()
# Проверяем лимит запросов
allowed, retry_after = await rate_limiter.check_limit(
context.user_id,
context.user_role,
context.registration_time
)
if not allowed:
self.logger.warning(f"Rate limit exceeded for user {context.user_id}, "
f"command: /{context.command}, retry after: {retry_after}s")
remaining = rate_limiter.get_remaining_requests(
context.user_id,
context.user_role,
context.registration_time
)
if retry_after and retry_after > 0:
message = (f"🚫 Превышен лимит запросов. "
f"Повторите попытку через {retry_after} сек. "
f"(Осталось запросов: {remaining})")
else:
message = (f"🚫 Превышен лимит запросов. "
f"Попробуйте позже. (Осталось запросов: {remaining})")
return MiddlewareResult.error(message)
self.logger.debug(f"Rate limit check passed for user {context.user_id}, command: /{context.command}")
return MiddlewareResult.success()
except Exception as e:
self.logger.error(f"Rate limit check error: {e}")
# В случае ошибки разрешаем выполнение, чтобы не блокировать пользователей
return MiddlewareResult.success()
class MetricsMiddleware(BaseMiddleware):
"""
Middleware для сбора метрик производительности.
"""
def __init__(self, metrics_collector=None, logger=None):
self.metrics = metrics_collector
self.logger = logger or logging.getLogger(__name__)
async def process(self, context: MiddlewareContext, config) -> MiddlewareResult:
"""Сбор метрик"""
try:
# Если есть метрики, собираем их
if self.metrics and context.command:
start_time = context.metadata.get('start_time', time.time())
duration = time.time() - start_time
# Записываем метрики
self.metrics.record_command(
command=context.command,
user_role=context.user_role.value if context.user_role else "unknown",
duration=duration
)
self.logger.debug(f"Metrics recorded for command /{context.command}: {duration:.3f}s")
return MiddlewareResult.success()
except Exception as e:
self.logger.error(f"Metrics collection error: {e}")
# Не останавливаем обработку из-за ошибки метрик
return MiddlewareResult.success()
class MiddlewareChain:
"""
Цепочка middleware для последовательной обработки запросов.
"""
def __init__(self, config, metrics=None, logger=None):
self.config = config
self.logger = logger or logging.getLogger(__name__)
# Создаем цепочку middleware
self.middlewares = [
ValidationMiddleware(self.logger),
LoggingMiddleware(self.logger),
RateLimitMiddleware(self.logger), # Rate limiting после логирования, но перед авторизацией
AuthorizationMiddleware(self.logger),
MetricsMiddleware(metrics, self.logger)
]
async def process_request(self, update: Update, context: ContextTypes,
handler: Callable[[Update, ContextTypes], Awaitable]) -> bool:
"""
Обработка запроса через цепочку middleware.
Args:
update: Обновление от Telegram
context: Контекст бота
handler: Финальный обработчик
Returns:
True если обработка должна продолжиться
"""
# Создаем контекст middleware
middleware_context = MiddlewareContext(update, context)
await middleware_context.initialize(self.config)
# Выполняем цепочку middleware
for middleware in self.middlewares:
try:
result = await middleware.process(middleware_context, self.config)
if not result.success:
# Отправляем сообщение об ошибке пользователю
if result.error_message:
await self._send_error_message(update, result.error_message)
return False # Останавливаем обработку
if result.stop_processing:
return False # Останавливаем обработку без ошибки
except Exception as e:
self.logger.error(f"Middleware {middleware.__class__.__name__} failed: {e}")
await self._send_error_message(update, "Произошла ошибка при обработке запроса")
return False
# Если все middleware прошли успешно, выполняем финальный обработчик
try:
await handler(update, context)
return True
except Exception as e:
self.logger.error(f"Handler execution failed: {e}")
await self._send_error_message(update, "Произошла ошибка при выполнении команды")
return False
async def _send_error_message(self, update: Update, message: str):
"""Отправка сообщения об ошибке"""
try:
if update.message:
await update.message.reply_text(message)
elif update.callback_query:
await update.callback_query.answer(message, show_alert=True)
except Exception as e:
if "Query is too old" in str(e) or "timeout expired" in str(e):
self.logger.warning(f"Не удалось отправить сообщение об ошибке (query устарел): {e}")
else:
self.logger.error(f"Failed to send error message: {e}")
# Глобальная функция для создания цепочки middleware
def create_middleware_chain(config, metrics=None):
"""
Создание цепочки middleware.
Args:
config: Конфигурация приложения
metrics: Сборщик метрик
Returns:
MiddlewareChain: Настроенная цепочка middleware
"""
return MiddlewareChain(config, metrics)