feat: send torrent blocker notification to user (not just admin)
- torrent_blocker.report is now a dual event: admin notification + user message - New _handle_torrent_detected user handler sends WEBHOOK_TORRENT_DETECTED - process_event handles events registered in both admin and user handlers - Webhook router passes DB session for dual events (needs_db_session check) - Add WEBHOOK_NOTIFY_TORRENT_DETECTED setting (default: true) - Add WEBHOOK_TORRENT_DETECTED locale texts (ru/en/ua/zh/fa)
This commit is contained in:
@@ -133,6 +133,7 @@ class Settings(BaseSettings):
|
||||
WEBHOOK_NOTIFY_NOT_CONNECTED: bool = True
|
||||
WEBHOOK_NOTIFY_BANDWIDTH_THRESHOLD: bool = True
|
||||
WEBHOOK_NOTIFY_DEVICES: bool = True
|
||||
WEBHOOK_NOTIFY_TORRENT_DETECTED: bool = True
|
||||
|
||||
TRIAL_DURATION_DAYS: int = 3
|
||||
TRIAL_TRAFFIC_LIMIT_GB: int = 10
|
||||
|
||||
@@ -1755,5 +1755,6 @@
|
||||
"WEBHOOK_USER_NOT_CONNECTED": "📡 <b>Not connected yet</b>\n\nYour subscription{tariff_label} is active but no VPN connection has been made. Connect to start using the service.",
|
||||
"WEBHOOK_DEVICE_ADDED": "📱 <b>New device</b>\n\nA new device has been added to your subscription{tariff_label}: <code>{device}</code>",
|
||||
"WEBHOOK_DEVICE_DELETED": "📱 <b>Device removed</b>\n\nA device has been removed from your subscription{tariff_label}: <code>{device}</code>",
|
||||
"WEBHOOK_TORRENT_DETECTED": "🚫 <b>Torrent detected</b>\n\nTorrent traffic was detected on your connection{tariff_label}. Using torrents may result in subscription restrictions.",
|
||||
"WEBHOOK_CLOSE_BUTTON": "✖️ Close"
|
||||
}
|
||||
@@ -1776,5 +1776,6 @@
|
||||
"WEBHOOK_USER_NOT_CONNECTED": "📡 <b>هنوز متصل نشدهاید</b>\n\nاشتراک{tariff_label} شما فعال است اما هنوز اتصال VPN برقرار نشده. برای شروع استفاده متصل شوید.",
|
||||
"WEBHOOK_DEVICE_ADDED": "📱 <b>دستگاه جدید</b>\n\nدستگاه جدیدی به اشتراک{tariff_label} شما اضافه شد: <code>{device}</code>",
|
||||
"WEBHOOK_DEVICE_DELETED": "📱 <b>دستگاه حذف شد</b>\n\nدستگاهی از اشتراک{tariff_label} شما حذف شد: <code>{device}</code>",
|
||||
"WEBHOOK_TORRENT_DETECTED": "🚫 <b>تورنت شناسایی شد</b>\n\nترافیک تورنت در اتصال{tariff_label} شما شناسایی شد. استفاده از تورنت ممکن است منجر به محدودیت اشتراک شود.",
|
||||
"WEBHOOK_CLOSE_BUTTON": "✖️ بستن"
|
||||
}
|
||||
@@ -1779,5 +1779,6 @@
|
||||
"WEBHOOK_USER_NOT_CONNECTED": "📡 <b>Вы ещё не подключились</b>\n\nВаша подписка{tariff_label} активна, но VPN-соединение не было установлено. Подключитесь, чтобы начать пользоваться.",
|
||||
"WEBHOOK_DEVICE_ADDED": "📱 <b>Новое устройство</b>\n\nК подписке{tariff_label} подключено новое устройство: <code>{device}</code>",
|
||||
"WEBHOOK_DEVICE_DELETED": "📱 <b>Устройство удалено</b>\n\nУстройство отключено от подписки{tariff_label}: <code>{device}</code>",
|
||||
"WEBHOOK_TORRENT_DETECTED": "🚫 <b>Обнаружен торрент</b>\n\nВ вашем подключении{tariff_label} обнаружен торрент-трафик. Использование торрентов может привести к ограничению подписки.",
|
||||
"WEBHOOK_CLOSE_BUTTON": "✖️ Закрыть"
|
||||
}
|
||||
|
||||
@@ -1647,5 +1647,6 @@
|
||||
"WEBHOOK_USER_NOT_CONNECTED": "📡 <b>Ви ще не підключились</b>\n\nВаша підписка{tariff_label} активна, але VPN-з'єднання не було встановлено. Підключіться, щоб почати користуватися.",
|
||||
"WEBHOOK_DEVICE_ADDED": "📱 <b>Новий пристрій</b>\n\nДо підписки{tariff_label} підключено новий пристрій: <code>{device}</code>",
|
||||
"WEBHOOK_DEVICE_DELETED": "📱 <b>Пристрій видалено</b>\n\nПристрій відключено від підписки{tariff_label}: <code>{device}</code>",
|
||||
"WEBHOOK_TORRENT_DETECTED": "🚫 <b>Виявлено торент</b>\n\nУ вашому підключенні{tariff_label} виявлено торент-трафік. Використання торентів може призвести до обмеження підписки.",
|
||||
"WEBHOOK_CLOSE_BUTTON": "✖️ Закрити"
|
||||
}
|
||||
|
||||
@@ -1643,6 +1643,7 @@
|
||||
"WEBHOOK_USER_NOT_CONNECTED": "📡 <b>尚未连接</b>\n\n您的订阅{tariff_label}已激活,但尚未建立VPN连接。请连接以开始使用服务。",
|
||||
"WEBHOOK_DEVICE_ADDED": "📱 <b>新设备</b>\n\n订阅{tariff_label}已添加新设备:<code>{device}</code>",
|
||||
"WEBHOOK_DEVICE_DELETED": "📱 <b>设备已移除</b>\n\n设备已从订阅{tariff_label}中移除:<code>{device}</code>",
|
||||
"WEBHOOK_TORRENT_DETECTED": "🚫 <b>检测到种子下载</b>\n\n在您的连接{tariff_label}中检测到种子流量。使用种子可能导致订阅受限。",
|
||||
"WEBHOOK_CLOSE_BUTTON": "✖️ 关闭",
|
||||
"BALANCE_TOPPED_UP_CART_SUFFICIENT": "✅ 余额已充值 {amount}!\n\n💰 当前余额:{balance}\n\n🛒 您有一个已保存的购物车,金额为 {cart_total}\n余额足够完成订购。",
|
||||
"BALANCE_TOPPED_UP_CART_INSUFFICIENT": "✅ 余额已充值 {amount}!\n\n💰 当前余额:{balance}\n\n🛒 您有一个已保存的购物车,金额为 {cart_total}\n还差:{missing}"
|
||||
|
||||
@@ -80,6 +80,7 @@ _TEXT_KEY_TO_SETTING: dict[str, str] = {
|
||||
'WEBHOOK_USER_NOT_CONNECTED': 'WEBHOOK_NOTIFY_NOT_CONNECTED',
|
||||
'WEBHOOK_DEVICE_ADDED': 'WEBHOOK_NOTIFY_DEVICES',
|
||||
'WEBHOOK_DEVICE_DELETED': 'WEBHOOK_NOTIFY_DEVICES',
|
||||
'WEBHOOK_TORRENT_DETECTED': 'WEBHOOK_NOTIFY_TORRENT_DETECTED',
|
||||
}
|
||||
|
||||
# Admin event display names for notification messages
|
||||
@@ -158,6 +159,7 @@ class RemnaWaveWebhookService:
|
||||
'user.not_connected': self._handle_user_not_connected,
|
||||
'user_hwid_devices.added': self._handle_device_added,
|
||||
'user_hwid_devices.deleted': self._handle_device_deleted,
|
||||
'torrent_blocker.report': self._handle_torrent_detected,
|
||||
}
|
||||
|
||||
# Admin-scoped handlers: no user resolution, notify admin chat
|
||||
@@ -173,6 +175,10 @@ class RemnaWaveWebhookService:
|
||||
"""Check if the event is admin-scoped (no DB session needed)."""
|
||||
return event_name in self._admin_handlers
|
||||
|
||||
def needs_db_session(self, event_name: str) -> bool:
|
||||
"""Check if the event requires a DB session (user handler or dual event)."""
|
||||
return event_name in self._user_handlers
|
||||
|
||||
@classmethod
|
||||
def _prune_intentional_panel_deletions(cls) -> None:
|
||||
if not cls._intentional_panel_deletions_by_uuid and not cls._intentional_panel_deletions_by_telegram_id:
|
||||
@@ -260,12 +266,20 @@ class RemnaWaveWebhookService:
|
||||
Returns True if the event was processed, False if skipped/unknown.
|
||||
db may be None for admin events that don't require database access.
|
||||
"""
|
||||
# Check if event has both admin and user handlers (e.g. torrent_blocker.report)
|
||||
user_handler = self._user_handlers.get(event_name)
|
||||
if event_name in self._admin_handlers and user_handler:
|
||||
# Dual event: send admin notification AND process user handler
|
||||
await self._process_admin_event(event_name, data)
|
||||
if db is not None:
|
||||
await self._process_user_event(db, event_name, data, user_handler)
|
||||
return True
|
||||
|
||||
# Check admin-scoped handlers (no DB needed)
|
||||
if event_name in self._admin_handlers:
|
||||
return await self._process_admin_event(event_name, data)
|
||||
|
||||
# Check user-scoped handlers (require DB session)
|
||||
user_handler = self._user_handlers.get(event_name)
|
||||
if user_handler:
|
||||
if db is None:
|
||||
logger.error('RemnaWave webhook: DB session required for user event', event_name=event_name)
|
||||
@@ -1409,3 +1423,14 @@ class RemnaWaveWebhookService:
|
||||
format_kwargs={'device': device_name or '—'},
|
||||
subscription=subscription,
|
||||
)
|
||||
|
||||
async def _handle_torrent_detected(
|
||||
self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict
|
||||
) -> None:
|
||||
logger.info('Webhook: torrent detected for user', user_id=user.id)
|
||||
await self._notify_user(
|
||||
user,
|
||||
'WEBHOOK_TORRENT_DETECTED',
|
||||
reply_markup=self._get_subscription_keyboard(user),
|
||||
subscription=subscription,
|
||||
)
|
||||
|
||||
@@ -944,6 +944,11 @@ class BotConfigurationService:
|
||||
'format': 'Булево значение.',
|
||||
'example': 'true',
|
||||
},
|
||||
'WEBHOOK_NOTIFY_TORRENT_DETECTED': {
|
||||
'description': 'Уведомление пользователю при обнаружении торрент-трафика.',
|
||||
'format': 'Булево значение.',
|
||||
'example': 'true',
|
||||
},
|
||||
'RESET_TRAFFIC_ON_TARIFF_SWITCH': {
|
||||
'description': (
|
||||
'Автоматически сбрасывает счётчик использованного трафика '
|
||||
|
||||
@@ -129,8 +129,9 @@ def create_remnawave_webhook_router(bot: Bot) -> APIRouter:
|
||||
|
||||
# Process event — return 200 to prevent retries for application-level errors.
|
||||
# Only return non-200 for infrastructure failures (DB unavailable).
|
||||
# Admin events (node/service/crm) don't need a DB session.
|
||||
if webhook_service.is_admin_event(event_name):
|
||||
# Admin-only events (node/service/crm) don't need a DB session.
|
||||
# Dual events (admin + user, e.g. torrent_blocker.report) need DB for user handler.
|
||||
if webhook_service.is_admin_event(event_name) and not webhook_service.needs_db_session(event_name):
|
||||
try:
|
||||
processed = await webhook_service.process_event(None, event_name, data)
|
||||
return JSONResponse({'status': 'ok', 'processed': processed})
|
||||
@@ -138,7 +139,7 @@ def create_remnawave_webhook_router(bot: Bot) -> APIRouter:
|
||||
logger.exception('RemnaWave webhook processing error for event', event_name=event_name)
|
||||
return JSONResponse({'status': 'ok', 'processed': False})
|
||||
|
||||
# User events require a DB session
|
||||
# User events and dual events require a DB session
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user