1f0fef114b
- 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
417 lines
19 KiB
Python
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()
|