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

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
+13
View File
@@ -0,0 +1,13 @@
"""
Обработчики команд и сообщений телеграм-бота.
Организуют взаимодействие между Telegram API и бизнес-логикой.
"""
from .base_handler import BaseHandler
from .user_handlers import UserHandlers
from .game_handlers import GameHandlers
from .admin_handlers import AdminHandlers
from .moderation_handlers import ModerationHandlers
from .payment_handler import PaymentHandler
__all__ = ['BaseHandler', 'UserHandlers', 'GameHandlers', 'AdminHandlers', 'ModerationHandlers', 'PaymentHandler']
File diff suppressed because it is too large Load Diff
+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
File diff suppressed because it is too large Load Diff
+486
View File
@@ -0,0 +1,486 @@
"""
Обработчики команд модерации.
Отвечают за предупреждения, заглушки и управление пользователями.
"""
from typing import Dict, Callable, List
from telegram import Update
from telegram.ext import ContextTypes
from .base_handler import BaseHandler
from services.user_service import UserService
from services.moderation_service import ModerationService
class ModerationHandlers(BaseHandler):
"""
Обработчики команд модерации.
Команды:
- /warn [пользователь] [причина] - выдать предупреждение
- /mute [пользователь] [время] [причина] - заглушить пользователя
- /unmute [пользователь] - снять заглушку
"""
def __init__(self, config, metrics, user_service: UserService, moderation_service: ModerationService):
"""
Инициализация обработчика модерации.
Args:
config: Конфигурация приложения
metrics: Сборщик метрик
user_service: Сервис управления пользователями
moderation_service: Сервис модерации
"""
super().__init__(config, metrics)
self.user_service = user_service
self.moderation_service = moderation_service
def get_command_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков команд модерации"""
return {
'warn': self.handle_warn,
'mute': self.handle_mute,
'unmute': self.handle_unmute,
'ban': self.handle_ban,
'unban': self.handle_unban,
'kick': self.handle_kick,
}
def get_callback_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков callback запросов"""
return {}
def get_message_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков сообщений"""
return {}
async def handle_warn(self, update: Update, context: ContextTypes):
"""Обработка команды /warn"""
await self.safe_execute(update, context, "warn", self._handle_warn, context.args)
async def _handle_warn(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /warn"""
user = update.effective_user
# Проверяем права модератора (уже проверено в middleware, но оставляем для совместимости)
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if len(args) < 2:
await self.send_response(update,
"Использование: /warn [пользователь] [причина]\n\n"
"Пример: /warn 123456789 Спам в чате"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args([args[0]])
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
reason = ' '.join(args[1:])
# Выдаем предупреждение
result = await self.moderation_service.warn_user(target_user_id, reason, user.id)
if result['success']:
# Получаем информацию о цели для уведомления
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
response_text = (
f"⚠️ Предупреждение выдано!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"📊 Предупреждений: {result['warnings_count']}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}"
)
if result['action_taken']:
response_text += f"\n\n🚨 Автоматическое действие: {result['action_taken']}"
await self.send_response(update, response_text)
# Отправляем уведомление пользователю
try:
private_message = (
f"⚠️ Вам выдано предупреждение!\n\n"
f"📊 Предупреждений: {result['warnings_count']}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}\n\n"
f"Будьте внимательнее к правилам чата!"
)
await context.bot.send_message(chat_id=target_user_id, text=private_message)
except Exception as e:
self.logger.warning(f"Не удалось отправить уведомление пользователю {target_user_id}: {e}")
else:
await self.send_response(update, "❌ Ошибка при выдаче предупреждения")
async def handle_mute(self, update: Update, context: ContextTypes):
"""Обработка команды /mute"""
await self.safe_execute(update, context, "mute", self._handle_mute, context.args)
async def _handle_mute(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /mute"""
user = update.effective_user
# Проверяем права модератора
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if len(args) < 2:
await self.send_response(update,
"Использование: /mute [пользователь] [время_в_секундах] [причина]\n\n"
"Пример: /mute 123456789 300 Спам в чате\n"
"Время: 300 = 5 мин, 3600 = 1 час, 86400 = 24 часа"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args([args[0]])
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
try:
duration = int(args[1])
if duration <= 0 or duration > 86400: # Максимум 24 часа
raise ValueError()
except ValueError:
await self.send_response(update, "❌ Время должно быть числом от 1 до 86400 секунд")
return
reason = ' '.join(args[2:]) if len(args) > 2 else "Нарушение правил"
# Заглушаем пользователя
result = await self.moderation_service.mute_user(target_user_id, reason, user.id, duration)
if result['success']:
# Получаем информацию о цели
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
# Преобразуем время в читаемый формат
hours = duration // 3600
minutes = (duration % 3600) // 60
seconds = duration % 60
time_parts = []
if hours > 0:
time_parts.append(f"{hours}ч")
if minutes > 0:
time_parts.append(f"{minutes}м")
if seconds > 0 and not time_parts: # Показываем секунды только если нет часов/минут
time_parts.append(f"{seconds}с")
time_str = " ".join(time_parts)
response_text = (
f"🔇 Пользователь заглушен!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"⏱️ Длительность: {time_str}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}\n"
f"🕐 До: {result['expires_at'].strftime('%Y-%m-%d %H:%M:%S')}"
)
await self.send_response(update, response_text)
# Отправляем уведомление пользователю
try:
private_message = (
f"🔇 Вы заглушены в чате!\n\n"
f"⏱️ Длительность: {time_str}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}\n"
f"🕐 До: {result['expires_at'].strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"Во время заглушки вы не можете отправлять сообщения."
)
await context.bot.send_message(chat_id=target_user_id, text=private_message)
except Exception as e:
self.logger.warning(f"Не удалось отправить уведомление пользователю {target_user_id}: {e}")
else:
await self.send_response(update, "❌ Ошибка при заглушке пользователя")
async def handle_unmute(self, update: Update, context: ContextTypes):
"""Обработка команды /unmute"""
await self.safe_execute(update, context, "unmute", self._handle_unmute, context.args)
async def _handle_unmute(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /unmute"""
user = update.effective_user
# Проверяем права модератора
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if not args:
await self.send_response(update,
"Использование: /unmute [пользователь]\n\n"
"Пример: /unmute 123456789"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args(args)
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
# Снимаем заглушку
success = await self.moderation_service.unmute_user(target_user_id, user.id)
if success:
# Получаем информацию о цели
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
response_text = (
f"🔊 Заглушка снята!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"👮 Модератор: {user.first_name}"
)
await self.send_response(update, response_text)
# Отправляем уведомление пользователю
try:
private_message = (
f"🔊 Заглушка снята!\n\n"
f"👮 Модератор: {user.first_name}\n\n"
f"Теперь вы можете отправлять сообщения в чате."
)
await context.bot.send_message(chat_id=target_user_id, text=private_message)
except Exception as e:
self.logger.warning(f"Не удалось отправить уведомление пользователю {target_user_id}: {e}")
else:
await self.send_response(update, "❌ Пользователь не был заглушен или ошибка при снятии заглушки")
async def handle_ban(self, update: Update, context: ContextTypes):
"""Обработка команды /ban"""
await self.safe_execute(update, context, "ban", self._handle_ban, context.args)
async def _handle_ban(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /ban"""
user = update.effective_user
# Проверяем права модератора
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if len(args) < 2:
await self.send_response(update,
"Использование: /ban [пользователь] [причина]\n\n"
"Пример: /ban 123456789 Спам и реклама"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args([args[0]])
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
reason = ' '.join(args[1:])
# Баним пользователя
result = await self.moderation_service.ban_user(target_user_id, reason, user.id)
if result['success']:
# Получаем информацию о цели для уведомления
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
response_text = (
f"🚫 Пользователь забанен!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}"
)
if result['permanent']:
response_text += "\n\nПостоянный бан"
else:
response_text += f"\n\nИстекает: {result['expires_at'].strftime('%Y-%m-%d %H:%M:%S')}"
await self.send_response(update, response_text)
# Отправляем уведомление пользователю
try:
private_message = (
f"🚫 Вы были забанены!\n\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}\n\n"
f"Бан {'постоянный' if result['permanent'] else 'истекает ' + result['expires_at'].strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"Вы не сможете пользоваться ботом до снятия бана."
)
await context.bot.send_message(chat_id=target_user_id, text=private_message)
except Exception as e:
self.logger.warning(f"Не удалось отправить уведомление пользователю {target_user_id}: {e}")
else:
await self.send_response(update, "❌ Ошибка при бане пользователя")
async def handle_unban(self, update: Update, context: ContextTypes):
"""Обработка команды /unban"""
await self.safe_execute(update, context, "unban", self._handle_unban, context.args)
async def _handle_unban(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /unban"""
user = update.effective_user
# Проверяем права модератора
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if not args:
await self.send_response(update,
"Использование: /unban [пользователь]\n\n"
"Пример: /unban 123456789"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args(args)
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
# Разбаниваем пользователя
success = await self.moderation_service.unban_user(target_user_id, user.id)
if success:
# Получаем информацию о цели
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
response_text = (
f"✅ Пользователь разбанен!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"👮 Модератор: {user.first_name}"
)
await self.send_response(update, response_text)
# Отправляем уведомление пользователю
try:
private_message = (
f"✅ Ваш бан был снят!\n\n"
f"👮 Модератор: {user.first_name}\n\n"
f"Теперь вы можете пользоваться ботом в полном объеме."
)
await context.bot.send_message(chat_id=target_user_id, text=private_message)
except Exception as e:
self.logger.warning(f"Не удалось отправить уведомление пользователю {target_user_id}: {e}")
else:
await self.send_response(update, "❌ Пользователь не был забанен или ошибка при разбане")
async def handle_kick(self, update: Update, context: ContextTypes):
"""Обработка команды /kick"""
await self.safe_execute(update, context, "kick", self._handle_kick, context.args)
async def _handle_kick(self, update: Update, context: ContextTypes, args: List[str]):
"""Внутренняя обработка команды /kick"""
user = update.effective_user
# Проверяем права модератора
if not await self.is_moderator(update, user.id):
await self.send_response(update, "❌ У вас нет прав модератора")
return
if len(args) < 2:
await self.send_response(update,
"Использование: /kick [пользователь] [причина]\n\n"
"Пример: /kick 123456789 Нарушение правил чата"
)
return
# Парсим аргументы
target_user_id = self.extract_user_id_from_args([args[0]])
if not target_user_id:
await self.send_response(update, "❌ Неверный ID пользователя")
return
reason = ' '.join(args[1:])
# Пытаемся кикнуть пользователя из чата
try:
await context.bot.ban_chat_member(
chat_id=update.effective_chat.id,
user_id=target_user_id,
until_date=None # Временный бан для кика
)
# Разбаниваем через 30 секунд (эффект кика)
import asyncio
async def unban_after_delay():
await asyncio.sleep(30)
try:
await context.bot.unban_chat_member(
chat_id=update.effective_chat.id,
user_id=target_user_id
)
except Exception as e:
self.logger.warning(f"Не удалось автоматически разбанить после кика {target_user_id}: {e}")
# Запускаем задачу в фоне
asyncio.create_task(unban_after_delay())
# Получаем информацию о цели
target_profile = await self.user_service.get_or_create_user(target_user_id)
target_name = target_profile.first_name if target_profile else f"ID:{target_user_id}"
response_text = (
f"👢 Пользователь кикнут!\n\n"
f"👤 Пользователь: {target_name}\n"
f"🆔 ID: {target_user_id}\n"
f"📝 Причина: {reason}\n"
f"👮 Модератор: {user.first_name}\n\n"
f"Пользователь сможет вернуться через 30 секунд."
)
await self.send_response(update, response_text)
except Exception as e:
await self.send_response(update, f"❌ Ошибка при кике пользователя: {str(e)}")
async def is_moderator(self, update: Update, user_id: int) -> bool:
"""
Проверка прав модератора (расширенная проверка прав).
Args:
update: Обновление от Telegram
user_id: ID пользователя
Returns:
True если пользователь модератор или администратор
"""
# Проверяем по списку администраторов в конфигурации
if user_id in self.config.bot_config.admin_ids:
return True
# Проверяем права в чате (администратор = модератор)
try:
chat = update.effective_chat
if chat:
member = await chat.get_member(user_id)
return member.status in ['administrator', 'creator']
except Exception:
pass
return False
+406
View File
@@ -0,0 +1,406 @@
"""
Обработчики для платежных операций и webhook уведомлений.
"""
import json
import logging
from typing import Dict, Any, Optional
from flask import Request, jsonify, Response
from telegram import Update
from telegram.ext import ContextTypes
from services.donation_service import DonationService
from core.payment_exceptions import PaymentError, PaymentSecurityError, PaymentProviderError
class PaymentHandler:
"""
Обработчик платежных операций для Telegram бота.
Отвечает за:
- Создание платежей через команды бота
- Обработку webhook уведомлений от платежных провайдеров
- Управление статусом платежей
"""
def __init__(self, donation_service: DonationService):
"""
Инициализация обработчика платежей.
Args:
donation_service: Сервис для обработки донатов
"""
self.donation_service = donation_service
self.logger = logging.getLogger(__name__)
async def handle_donate_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработка команды /donate.
Args:
update: Объект обновления Telegram
context: Контекст обработчика
"""
try:
user = update.effective_user
args = context.args or []
if not args:
await self._show_donation_menu(update)
return
# Парсинг суммы
try:
amount = float(args[0])
except (ValueError, IndexError):
await update.message.reply_text(
"❌ Неверный формат суммы. Используйте: /donate 100"
)
return
# Определение провайдера (по умолчанию stripe)
provider = args[1] if len(args) > 1 else 'stripe'
# Создание доната
donation_response = await self.donation_service.create_donation(
user_id=user.id,
amount=amount,
provider_name=provider
)
# Формирование сообщения с платежной ссылкой
message = (
f"💰 <b>Донат {amount:.2f} ₽</b>\n\n"
f"🔗 <a href=\"{donation_response.payment_url}\">Оплатить</a>\n\n"
f"После оплаты средства будут автоматически зачислены на ваш счет."
)
await update.message.reply_html(
message,
disable_web_page_preview=True
)
except PaymentError as e:
self.logger.warning(f"Payment error for user {user.id}: {e}")
await update.message.reply_text(f"❌ Ошибка платежа: {e}")
except Exception as e:
self.logger.error(f"Unexpected error in donate command: {e}", exc_info=True)
await update.message.reply_text("❌ Произошла неожиданная ошибка. Попробуйте позже.")
async def handle_payment_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработка команды /payment_status <payment_id>.
Args:
update: Объект обновления Telegram
context: Контекст обработчика
"""
try:
user = update.effective_user
args = context.args or []
if not args:
await update.message.reply_text(
"❌ Укажите ID платежа: /payment_status 123"
)
return
try:
payment_id = int(args[0])
except ValueError:
await update.message.reply_text("❌ Неверный ID платежа")
return
# Получение статуса платежа
payment = self.donation_service.get_payment_status(payment_id)
if not payment:
await update.message.reply_text("❌ Платеж не найден")
return
# Проверка прав доступа
if payment['user_id'] != user.id:
await update.message.reply_text("❌ Доступ запрещен")
return
# Формирование сообщения о статусе
status_emoji = {
'pending': '',
'processing': '🔄',
'succeeded': '',
'failed': '',
'cancelled': '🚫',
'refunded': '↩️'
}
message = (
f"{status_emoji.get(payment['status'], '')} <b>Статус платежа #{payment_id}</b>\n\n"
f"💰 Сумма: {payment['amount']:.2f} {payment['currency']}\n"
f"🔧 Провайдер: {payment['provider']}\n"
f"📅 Создан: {payment['created_at'][:19]}\n"
f"📊 Статус: <code>{payment['status']}</code>\n"
)
if payment.get('processed_at'):
message += f"✅ Обработан: {payment['processed_at'][:19]}\n"
if payment.get('error_message'):
message += f"❌ Ошибка: {payment['error_message']}\n"
await update.message.reply_html(message)
except Exception as e:
self.logger.error(f"Error in payment status command: {e}", exc_info=True)
await update.message.reply_text("❌ Ошибка при получении статуса платежа")
async def handle_my_payments(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработка команды /my_payments.
Args:
update: Объект обновления Telegram
context: Контекст обработчика
"""
try:
user = update.effective_user
# Получение платежей пользователя
payments = self.donation_service.get_user_payments(user.id, limit=10)
if not payments:
await update.message.reply_text("📭 У вас пока нет платежей")
return
# Формирование списка платежей
message = "<b>💳 Ваши платежи</b>\n\n"
for payment in payments:
status_emoji = {
'pending': '',
'processing': '🔄',
'succeeded': '',
'failed': '',
'cancelled': '🚫',
'refunded': '↩️'
}
message += (
f"{status_emoji.get(payment['status'], '')} #{payment['id']} - "
f"{payment['amount']:.2f} {payment['currency']} "
f"({payment['created_at'][:10]}) - <code>{payment['status']}</code>\n"
)
message += "\nДля детальной информации: /payment_status <ID>"
await update.message.reply_html(message)
except Exception as e:
self.logger.error(f"Error in my payments command: {e}", exc_info=True)
await update.message.reply_text("❌ Ошибка при получении списка платежей")
async def handle_cancel_payment(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработка команды /cancel_payment <payment_id>.
Args:
update: Объект обновления Telegram
context: Контекст обработчика
"""
try:
user = update.effective_user
args = context.args or []
if not args:
await update.message.reply_text(
"❌ Укажите ID платежа: /cancel_payment 123"
)
return
try:
payment_id = int(args[0])
except ValueError:
await update.message.reply_text("❌ Неверный ID платежа")
return
# Попытка отмены платежа
success = self.donation_service.cancel_payment(payment_id, user.id)
if success:
await update.message.reply_text("✅ Платеж успешно отменен")
else:
await update.message.reply_text("❌ Невозможно отменить платеж (уже обработан или не найден)")
except Exception as e:
self.logger.error(f"Error in cancel payment command: {e}", exc_info=True)
await update.message.reply_text("❌ Ошибка при отмене платежа")
async def _show_donation_menu(self, update: Update) -> None:
"""
Показать меню донатов.
Args:
update: Объект обновления Telegram
"""
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = [
[
InlineKeyboardButton("💰 100 ₽", callback_data="donate_100"),
InlineKeyboardButton("💎 500 ₽", callback_data="donate_500"),
],
[
InlineKeyboardButton("🏆 1000 ₽", callback_data="donate_1000"),
InlineKeyboardButton("👑 2500 ₽", callback_data="donate_2500"),
],
[
InlineKeyboardButton("🎯 Другая сумма", callback_data="donate_custom"),
]
]
message = (
"🎁 <b>Поддержать проект</b>\n\n"
"Выберите сумму доната или введите свою:\n"
"<code>/donate 500</code>\n\n"
"После оплаты вы получите бонусные очки и поможете развитию проекта! 🚀"
)
await update.message.reply_html(
message,
reply_markup=InlineKeyboardMarkup(keyboard)
)
class WebhookHandler:
"""
Обработчик webhook уведомлений от платежных провайдеров.
"""
def __init__(self, donation_service: DonationService):
"""
Инициализация обработчика webhook.
Args:
donation_service: Сервис для обработки донатов
"""
self.donation_service = donation_service
self.logger = logging.getLogger(__name__)
async def handle_stripe_webhook(self, request: Request) -> Response:
"""
Обработка webhook от Stripe.
Args:
request: HTTP запрос
Returns:
Response: HTTP ответ
"""
try:
# Получение тела запроса и подписи
payload = await request.get_data()
signature = request.headers.get('Stripe-Signature', '')
# Парсинг JSON
try:
webhook_data = json.loads(payload)
except json.JSONDecodeError:
return jsonify({'error': 'Invalid JSON'}), 400
# Обработка webhook через donation service
await self.donation_service.process_payment_webhook(
provider_name='stripe',
webhook_data=webhook_data,
signature=signature
)
return jsonify({'status': 'ok'}), 200
except PaymentSecurityError:
self.logger.warning("Invalid Stripe webhook signature")
return jsonify({'error': 'Invalid signature'}), 401
except PaymentProviderError as e:
self.logger.error(f"Stripe webhook processing error: {e}")
return jsonify({'error': 'Processing error'}), 500
except Exception as e:
self.logger.error(f"Unexpected error in Stripe webhook: {e}", exc_info=True)
return jsonify({'error': 'Internal error'}), 500
async def handle_yookassa_webhook(self, request: Request) -> Response:
"""
Обработка webhook от YooKassa.
Args:
request: HTTP запрос
Returns:
Response: HTTP ответ
"""
try:
# Получение тела запроса и подписи
payload = await request.get_data()
signature = request.headers.get('Authorization', '')
# Парсинг JSON
try:
webhook_data = json.loads(payload)
except json.JSONDecodeError:
return jsonify({'error': 'Invalid JSON'}), 400
# Обработка webhook через donation service
await self.donation_service.process_payment_webhook(
provider_name='yookassa',
webhook_data=webhook_data,
signature=signature
)
return jsonify({'status': 'ok'}), 200
except PaymentSecurityError:
self.logger.warning("Invalid YooKassa webhook signature")
return jsonify({'error': 'Invalid signature'}), 401
except PaymentProviderError as e:
self.logger.error(f"YooKassa webhook processing error: {e}")
return jsonify({'error': 'Processing error'}), 500
except Exception as e:
self.logger.error(f"Unexpected error in YooKassa webhook: {e}", exc_info=True)
return jsonify({'error': 'Internal error'}), 500
async def handle_sbp_webhook(self, request: Request) -> Response:
"""
Обработка webhook от СБП.
Args:
request: HTTP запрос
Returns:
Response: HTTP ответ
"""
try:
# Получение тела запроса и подписи
payload = await request.get_data()
signature = request.headers.get('X-SBP-Signature', '')
# Парсинг JSON
try:
webhook_data = json.loads(payload)
except json.JSONDecodeError:
return jsonify({'error': 'Invalid JSON'}), 400
# Обработка webhook через donation service
await self.donation_service.process_payment_webhook(
provider_name='sbp',
webhook_data=webhook_data,
signature=signature
)
return jsonify({'status': 'ok'}), 200
except PaymentSecurityError:
self.logger.warning("Invalid SBP webhook signature")
return jsonify({'error': 'Invalid signature'}), 401
except PaymentProviderError as e:
self.logger.error(f"SBP webhook processing error: {e}")
return jsonify({'error': 'Processing error'}), 500
except Exception as e:
self.logger.error(f"Unexpected error in SBP webhook: {e}", exc_info=True)
return jsonify({'error': 'Internal error'}), 500
File diff suppressed because it is too large Load Diff