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,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
@@ -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
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user