Files
remnawave-bedolaga-telegram…/app/services/ban_notification_service.py
T
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

417 lines
19 KiB
Python

"""
Сервис для отправки уведомлений от ban системы пользователям
"""
import structlog
from aiogram import Bot
from aiogram.exceptions import TelegramAPIError
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.services.notification_delivery_service import (
NotificationType,
notification_delivery_service,
)
from app.services.remnawave_service import remnawave_service
logger = structlog.get_logger(__name__)
def get_delete_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура с кнопкой удаления уведомления"""
return InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text='🗑 Удалить', callback_data='ban_notify:delete')]]
)
class BanNotificationService:
"""Сервис для отправки уведомлений о банах пользователям"""
def __init__(self):
self._bot: Bot | None = None
def set_bot(self, bot: Bot):
"""Установить инстанс бота для отправки сообщений"""
self._bot = bot
async def _find_user_by_identifier(self, db: AsyncSession, user_identifier: str) -> User | None:
"""
Найти пользователя по email или user_id из Remnawave Panel
Args:
db: Сессия БД
user_identifier: Email или user_id пользователя
Returns:
User или None если не найден
"""
# Сначала пытаемся получить telegram_id через remnawave_service
try:
telegram_id = await remnawave_service.get_telegram_id_by_email(user_identifier)
if telegram_id:
# Ищем пользователя по telegram_id
result = await db.execute(select(User).where(User.telegram_id == telegram_id))
user = result.scalar_one_or_none()
if user:
return user
except Exception as e:
logger.warning('Не удалось получить telegram_id через remnawave', error=e)
# Если не нашли через remnawave, пытаемся искать по email в подписках
# (это может быть полезно если у пользователя есть подписка с таким email)
try:
# Импортируем здесь чтобы избежать циклических импортов
from app.database.models import Subscription
result = await db.execute(
select(User).join(Subscription).where(Subscription.email == user_identifier).limit(1)
)
user = result.scalar_one_or_none()
if user:
return user
except Exception as e:
logger.warning('Ошибка поиска пользователя по email в подписках', error=e)
return None
async def send_punishment_notification(
self,
db: AsyncSession,
user_identifier: str,
username: str,
ip_count: int,
limit: int,
ban_minutes: int,
node_name: str | None = None,
) -> tuple[bool, str, int | None]:
"""
Отправить уведомление о блокировке пользователю
Returns:
(success, message, telegram_id)
"""
if not self._bot:
return False, 'Бот не инициализирован', None
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем информацию о ноде (заметно выделяем)
node_info = f'🖥 <b>Нода:</b> <code>{node_name}</code>' if node_name else ''
# Формируем сообщение из настроек
# Используем безопасное форматирование - если {node_info} отсутствует в шаблоне, не будет ошибки
format_vars = {'ip_count': ip_count, 'limit': limit, 'ban_minutes': ban_minutes, 'node_info': node_info}
try:
message_text = settings.BAN_MSG_PUNISHMENT.format(**format_vars)
except KeyError:
# Старый шаблон без {node_info} - форматируем без него
message_text = settings.BAN_MSG_PUNISHMENT.format(ip_count=ip_count, limit=limit, ban_minutes=ban_minutes)
# Добавляем информацию о ноде в конец, если она есть
if node_info:
message_text = message_text.rstrip() + f'\n\n{node_info.rstrip()}'
# Handle email-only users via notification delivery service
if not user.telegram_id:
reason = f'IP лимит превышен: {ip_count}/{limit}. Бан на {ban_minutes} минут.'
if node_name:
reason += f' Нода: {node_name}'
success = await notification_delivery_service.notify_ban(
user=user,
reason=reason,
)
if success:
logger.info('Email уведомление о бане отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
# Отправляем сообщение с кнопкой удаления
try:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
'Уведомление о бане отправлено пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
'Ошибка отправки уведомления пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
async def send_enabled_notification(
self, db: AsyncSession, user_identifier: str, username: str
) -> tuple[bool, str, int | None]:
"""
Отправить уведомление о разблокировке пользователю
Returns:
(success, message, telegram_id)
"""
if not self._bot:
return False, 'Бот не инициализирован', None
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем сообщение из настроек
message_text = settings.BAN_MSG_ENABLED
# Handle email-only users via notification delivery service
if not user.telegram_id:
success = await notification_delivery_service.notify_unban(user=user)
if success:
logger.info('Email уведомление о разбане отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
# Отправляем сообщение с кнопкой удаления
try:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
'Уведомление о разбане отправлено пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
'Ошибка отправки уведомления пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
async def send_warning_notification(
self, db: AsyncSession, user_identifier: str, username: str, warning_message: str
) -> tuple[bool, str, int | None]:
"""
Отправить предупреждение пользователю
Returns:
(success, message, telegram_id)
"""
if not self._bot:
return False, 'Бот не инициализирован', None
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем сообщение из настроек
message_text = settings.BAN_MSG_WARNING.format(warning_message=warning_message)
# Handle email-only users via notification delivery service
if not user.telegram_id:
context = {'message': warning_message}
success = await notification_delivery_service.send_notification(
user=user,
notification_type=NotificationType.WARNING_NOTIFICATION,
context=context,
)
if success:
logger.info('Email предупреждение отправлено пользователю', user_id=user.id)
return True, 'Email предупреждение отправлено', None
return False, 'Не удалось отправить email предупреждение', None
# Отправляем сообщение с кнопкой удаления
try:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
'Предупреждение отправлено пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
)
return True, 'Предупреждение отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
'Ошибка отправки предупреждения пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
async def send_network_wifi_notification(
self,
db: AsyncSession,
user_identifier: str,
username: str,
ban_minutes: int,
network_type: str | None = None,
node_name: str | None = None,
) -> tuple[bool, str, int | None]:
"""
Отправить уведомление о блокировке за использование WiFi сети
Returns:
(success, message, telegram_id)
"""
if not self._bot:
return False, 'Бот не инициализирован', None
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем сообщение из настроек (заметно выделяем)
network_info = f'├ 🌐 Сеть: <b>{network_type}</b>\n' if network_type else ''
node_info = f'🖥 <b>Нода:</b> <code>{node_name}</code>' if node_name else ''
logger.info('WiFi notification: node_name=, node_info', node_name=repr(node_name), node_info=repr(node_info))
# Безопасное форматирование
format_vars = {'ban_minutes': ban_minutes, 'network_info': network_info, 'node_info': node_info}
try:
message_text = settings.BAN_MSG_WIFI.format(**format_vars)
except KeyError:
logger.warning('BAN_MSG_WIFI template missing placeholders, adding node_info to end')
message_text = settings.BAN_MSG_WIFI.format(ban_minutes=ban_minutes)
extra_info = (network_info + node_info).strip()
if extra_info:
message_text = message_text.rstrip() + f'\n\n{extra_info}'
# Handle email-only users via notification delivery service
if not user.telegram_id:
reason = f'Использование WiFi сети запрещено. Бан на {ban_minutes} минут.'
if network_type:
reason += f' Сеть: {network_type}'
if node_name:
reason += f' Нода: {node_name}'
success = await notification_delivery_service.notify_ban(
user=user,
reason=reason,
)
if success:
logger.info('Email WiFi уведомление отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
# Отправляем сообщение с кнопкой удаления
try:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
'Уведомление о WiFi бане отправлено пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
'Ошибка отправки WiFi уведомления пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
async def send_network_mobile_notification(
self,
db: AsyncSession,
user_identifier: str,
username: str,
ban_minutes: int,
network_type: str | None = None,
node_name: str | None = None,
) -> tuple[bool, str, int | None]:
"""
Отправить уведомление о блокировке за использование мобильной сети
Returns:
(success, message, telegram_id)
"""
if not self._bot:
return False, 'Бот не инициализирован', None
# Находим пользователя
user = await self._find_user_by_identifier(db, user_identifier)
if not user:
logger.warning('Пользователь не найден в базе данных', user_identifier=user_identifier)
return False, f'Пользователь не найден: {user_identifier}', None
# Формируем сообщение из настроек (заметно выделяем)
network_info = f'├ 🌐 Сеть: <b>{network_type}</b>\n' if network_type else ''
node_info = f'🖥 <b>Нода:</b> <code>{node_name}</code>' if node_name else ''
# Безопасное форматирование
format_vars = {'ban_minutes': ban_minutes, 'network_info': network_info, 'node_info': node_info}
try:
message_text = settings.BAN_MSG_MOBILE.format(**format_vars)
except KeyError:
message_text = settings.BAN_MSG_MOBILE.format(ban_minutes=ban_minutes)
extra_info = (network_info + node_info).strip()
if extra_info:
message_text = message_text.rstrip() + f'\n\n{extra_info}'
# Handle email-only users via notification delivery service
if not user.telegram_id:
reason = f'Использование мобильной сети запрещено. Бан на {ban_minutes} минут.'
if network_type:
reason += f' Сеть: {network_type}'
if node_name:
reason += f' Нода: {node_name}'
success = await notification_delivery_service.notify_ban(
user=user,
reason=reason,
)
if success:
logger.info('Email Mobile уведомление отправлено пользователю', user_id=user.id)
return True, 'Email уведомление отправлено', None
return False, 'Не удалось отправить email уведомление', None
# Отправляем сообщение с кнопкой удаления
try:
await self._bot.send_message(
chat_id=user.telegram_id, text=message_text, parse_mode='HTML', reply_markup=get_delete_keyboard()
)
logger.info(
'Уведомление о Mobile бане отправлено пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
'Ошибка отправки Mobile уведомления пользователю (telegram_id: )',
username=username,
telegram_id=user.telegram_id,
error=e,
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
# Глобальный экземпляр сервиса
ban_notification_service = BanNotificationService()