Files
remnawave-bedolaga-telegram…/app/services/ban_notification_service.py
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00

381 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Сервис для отправки уведомлений от ban системы пользователям
"""
import logging
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 = logging.getLogger(__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(f'Не удалось получить telegram_id через remnawave: {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(f'Ошибка поиска пользователя по email в подписках: {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(f'Пользователь {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(f'Email уведомление о бане отправлено пользователю {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(f'Уведомление о бане отправлено пользователю {username} (telegram_id: {user.telegram_id})')
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(f'Ошибка отправки уведомления пользователю {username} (telegram_id: {user.telegram_id}): {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(f'Пользователь {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(f'Email уведомление о разбане отправлено пользователю {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(f'Уведомление о разбане отправлено пользователю {username} (telegram_id: {user.telegram_id})')
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(f'Ошибка отправки уведомления пользователю {username} (telegram_id: {user.telegram_id}): {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(f'Пользователь {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(f'Email предупреждение отправлено пользователю {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(f'Предупреждение отправлено пользователю {username} (telegram_id: {user.telegram_id})')
return True, 'Предупреждение отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
f'Ошибка отправки предупреждения пользователю {username} (telegram_id: {user.telegram_id}): {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(f'Пользователь {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(f'WiFi notification: node_name={node_name!r}, node_info={node_info!r}')
# Безопасное форматирование
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(f'Email WiFi уведомление отправлено пользователю {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(f'Уведомление о WiFi бане отправлено пользователю {username} (telegram_id: {user.telegram_id})')
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
f'Ошибка отправки WiFi уведомления пользователю {username} (telegram_id: {user.telegram_id}): {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(f'Пользователь {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(f'Email Mobile уведомление отправлено пользователю {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(
f'Уведомление о Mobile бане отправлено пользователю {username} (telegram_id: {user.telegram_id})'
)
return True, 'Уведомление отправлено', user.telegram_id
except TelegramAPIError as e:
logger.error(
f'Ошибка отправки Mobile уведомления пользователю {username} (telegram_id: {user.telegram_id}): {e}'
)
return False, f'Ошибка Telegram API: {e!s}', user.telegram_id
# Глобальный экземпляр сервиса
ban_notification_service = BanNotificationService()