Files

382 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)