"""
Сервис для отправки уведомлений пользователям о платежах.
"""
import logging
import asyncio
import time
from typing import Optional, Dict, Any
from telegram import Bot
from telegram.error import TelegramError, NetworkError, TimedOut, RetryAfter
class NotificationService:
"""
Сервис для отправки уведомлений пользователям.
Отвечает за:
- Уведомления об успешных платежах
- Уведомления о неудачных платежах
- Уведомления администраторам о проблемах
"""
def __init__(self, bot_token: str, admin_chat_ids: Optional[list] = None):
"""
Инициализация сервиса уведомлений.
Args:
bot_token: Токен Telegram бота
admin_chat_ids: Список ID чатов администраторов
"""
self.bot = Bot(token=bot_token)
self.admin_chat_ids = admin_chat_ids or []
self.logger = logging.getLogger(__name__)
# Статистика отправки уведомлений
self.stats = {
'successful_sends': 0,
'failed_sends': 0,
'network_errors': 0,
'timeout_errors': 0,
'rate_limit_errors': 0,
'other_errors': 0
}
# Настройки повторных попыток
self.max_retries = 3
self.base_retry_delay = 1.0 # секунды
self.max_retry_delay = 30.0 # секунды
# Rate limiting
self.last_request_time = 0
self.min_request_interval = 0.1 # минимум 100ms между запросами
async def notify_payment_success(self, user_id: int, amount: float, currency: str = "RUB"):
"""
Уведомление об успешном платеже.
Args:
user_id: ID пользователя
amount: Сумма платежа
currency: Валюта
"""
try:
message = (
f"✅ Спасибо за донат!\n\n"
f"💰 Сумма: {amount:.2f} {currency}\n"
f"🎯 Средства успешно зачислены на ваш счет!\n\n"
f"Вы помогаете развитию проекта и получаете бонусные очки за поддержку. "
f"Посмотреть свою статистику можно командой /rank"
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent success notification to user {user_id} for amount {amount}")
except TelegramError as e:
self.logger.error(f"Failed to send success notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending success notification: {e}")
async def notify_payment_failed(self, user_id: int, reason: str = "Неизвестная ошибка"):
"""
Уведомление о неудачном платеже.
Args:
user_id: ID пользователя
reason: Причина неудачи
"""
try:
message = (
f"❌ Платеж не прошел\n\n"
f"💡 Причина: {reason}\n\n"
f"Попробуйте еще раз или обратитесь в поддержку, "
f"если проблема сохраняется."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent failure notification to user {user_id}: {reason}")
except TelegramError as e:
self.logger.error(f"Failed to send failure notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending failure notification: {e}")
async def notify_payment_cancelled(self, user_id: int):
"""
Уведомление об отмене платежа.
Args:
user_id: ID пользователя
"""
try:
message = (
f"🚫 Платеж отменен\n\n"
f"Если это было сделано по ошибке, попробуйте создать новый платеж."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent cancellation notification to user {user_id}")
except TelegramError as e:
self.logger.error(f"Failed to send cancellation notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending cancellation notification: {e}")
async def notify_payment_pending(self, user_id: int, payment_url: str, amount: float):
"""
Уведомление о том, что платеж находится в обработке.
Args:
user_id: ID пользователя
payment_url: URL для оплаты
amount: Сумма платежа
"""
try:
message = (
f"⏳ Платеж в обработке\n\n"
f"💰 Сумма: {amount:.2f} RUB\n"
f"🔗 Завершить оплату\n\n"
f"Пожалуйста, завершите оплату по ссылке выше."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML',
disable_web_page_preview=True
)
self.logger.info(f"Sent pending notification to user {user_id}")
except TelegramError as e:
self.logger.error(f"Failed to send pending notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending pending notification: {e}")
async def notify_admin_payment_issue(self, payment_id: str, error: str, user_id: Optional[int] = None):
"""
Уведомление администраторов о проблеме с платежом.
Args:
payment_id: ID платежа
error: Описание проблемы
user_id: ID пользователя (опционально)
"""
try:
user_info = f" (Пользователь: {user_id})" if user_id else ""
message = (
f"🚨 Проблема с платежом\n\n"
f"🆔 ID платежа: {payment_id}{user_info}\n"
f"❌ Ошибка: {error}\n\n"
f"Требуется ручная проверка."
)
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send admin notification to {admin_chat_id}: {e}")
self.logger.warning(f"Sent admin notification for payment {payment_id}: {error}")
except Exception as e:
self.logger.error(f"Unexpected error sending admin notification: {e}")
async def notify_admin_system_error(self, error_type: str, error_message: str, details: Optional[str] = None):
"""
Уведомление администраторов о системной ошибке.
Args:
error_type: Тип ошибки
error_message: Сообщение об ошибке
details: Дополнительные детали
"""
try:
message = (
f"🔥 Системная ошибка платежей\n\n"
f"⚠️ Тип: {error_type}\n"
f"📝 Ошибка: {error_message}\n"
)
if details:
message += f"📋 Детали: {details[:500]}\n\n"
message += "Требуется немедленное вмешательство!"
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send system error notification to {admin_chat_id}: {e}")
self.logger.critical(f"Sent system error notification: {error_type} - {error_message}")
except Exception as e:
self.logger.error(f"Unexpected error sending system error notification: {e}")
async def notify_admin_payment_stats(self, stats: dict):
"""
Уведомление администраторов о статистике платежей.
Args:
stats: Статистика платежей
"""
try:
message = (
f"📊 Статистика платежей\n\n"
f"💰 Всего платежей: {stats.get('total_payments', 0)}\n"
f"✅ Успешных: {stats.get('successful_payments', 0)}\n"
f"❌ Неудачных: {stats.get('failed_payments', 0)}\n"
f"⏳ Ожидающих: {stats.get('pending_payments', 0)}\n"
f"📈 Общая сумма: {stats.get('total_amount', 0):.2f} RUB\n"
)
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send stats notification to {admin_chat_id}: {e}")
self.logger.info("Sent payment statistics notification to admins")
except Exception as e:
self.logger.error(f"Unexpected error sending stats notification: {e}")
async def send_custom_notification(self, user_id: int, message: str, parse_mode: str = 'HTML'):
"""
Отправка произвольного уведомления пользователю.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга ('HTML', 'Markdown', None)
"""
await self._send_with_retry(user_id, message, parse_mode)
async def _send_with_retry(self, user_id: int, message: str, parse_mode: str = 'HTML', attempt: int = 1):
"""
Отправка сообщения с механизмом повторных попыток и rate limiting.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга
attempt: Номер текущей попытки
"""
# Rate limiting
await self._apply_rate_limit()
try:
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode=parse_mode if parse_mode != 'HTML' else None,
disable_web_page_preview=True
)
self.stats['successful_sends'] += 1
self.logger.info(f"Sent notification to user {user_id} (attempt {attempt})")
except RetryAfter as e:
# Rate limit от Telegram API
self.stats['rate_limit_errors'] += 1
self.logger.warning(f"Rate limit hit for user {user_id}, retry after {e.retry_after}s")
await asyncio.sleep(e.retry_after)
await self._send_with_retry(user_id, message, parse_mode, attempt)
except TimedOut as e:
# Таймаут сети
self.stats['timeout_errors'] += 1
self.logger.warning(f"Timeout sending notification to user {user_id} (attempt {attempt})")
await self._handle_retry(user_id, message, parse_mode, attempt, "timeout")
except NetworkError as e:
# Сетевая ошибка
self.stats['network_errors'] += 1
self.logger.warning(f"Network error sending notification to user {user_id} (attempt {attempt}): {e}")
await self._handle_retry(user_id, message, parse_mode, attempt, "network")
except TelegramError as e:
# Другие ошибки Telegram
self.stats['other_errors'] += 1
self.logger.error(f"Telegram error sending notification to user {user_id} (attempt {attempt}): {e}")
self.stats['failed_sends'] += 1
except Exception as e:
# Неожиданные ошибки
self.stats['other_errors'] += 1
self.logger.error(f"Unexpected error sending notification to user {user_id} (attempt {attempt}): {e}")
self.stats['failed_sends'] += 1
async def _apply_rate_limit(self):
"""Применение rate limiting между запросами."""
current_time = time.time()
time_since_last_request = current_time - self.last_request_time
if time_since_last_request < self.min_request_interval:
await asyncio.sleep(self.min_request_interval - time_since_last_request)
self.last_request_time = time.time()
async def _handle_retry(self, user_id: int, message: str, parse_mode: str, attempt: int, error_type: str):
"""
Обработка повторных попыток отправки.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга
attempt: Текущий номер попытки
error_type: Тип ошибки
"""
if attempt >= self.max_retries:
self.stats['failed_sends'] += 1
self.logger.error(f"Max retries ({self.max_retries}) exceeded for user {user_id}, giving up")
return
# Экспоненциальная задержка
delay = min(self.base_retry_delay * (2 ** (attempt - 1)), self.max_retry_delay)
self.logger.info(f"Retrying notification to user {user_id} in {delay}s (attempt {attempt + 1}/{self.max_retries})")
await asyncio.sleep(delay)
await self._send_with_retry(user_id, message, parse_mode, attempt + 1)
def get_notification_stats(self) -> Dict[str, Any]:
"""
Получение статистики отправки уведомлений.
Returns:
Dict с статистикой отправки
"""
total_attempts = (
self.stats['successful_sends'] +
self.stats['failed_sends'] +
self.stats['network_errors'] +
self.stats['timeout_errors'] +
self.stats['rate_limit_errors'] +
self.stats['other_errors']
)
return {
**self.stats,
'total_attempts': total_attempts,
'success_rate': (self.stats['successful_sends'] / total_attempts * 100) if total_attempts > 0 else 0.0
}
def reset_stats(self):
"""Сброс статистики отправки уведомлений."""
self.stats = {
'successful_sends': 0,
'failed_sends': 0,
'network_errors': 0,
'timeout_errors': 0,
'rate_limit_errors': 0,
'other_errors': 0
}