diff --git a/web/__init__.py b/web/__init__.py index 9ebeadaa..d4711282 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,9 +1,9 @@ from aiohttp.web_urldispatcher import UrlDispatcher - import bot +from config import TBLOCKER_WEBHOOK_PATH +from .tblocker import tblocker_webhook async def register_web_routes(router: UrlDispatcher) -> None: dp = bot.dp - - # todo: add your api routes here + router.add_post(TBLOCKER_WEBHOOK_PATH, tblocker_webhook) diff --git a/web/tblocker.py b/web/tblocker.py new file mode 100644 index 00000000..470d06f5 --- /dev/null +++ b/web/tblocker.py @@ -0,0 +1,166 @@ +import asyncio +import datetime +from datetime import datetime +import json +import os +import re + +import aiohttp +from aiohttp import web +import asyncpg +from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from bot import bot +from config import BLOCK_DURATION, DATABASE_URL, SERVER_COUNTRIES, TIMESTAMP_TTL +from database import get_key_details +from logger import logger +from handlers.buttons import MAIN_MENU + + +last_unblock_data = {} + + + +def get_country_from_server(server: str) -> str: + """ + Определяет страну сервера по его имени или домену. + Ищет совпадение части домена в полных доменах. + """ + server_part = server.split('.')[0] + + for full_domain, country in SERVER_COUNTRIES.items(): + if server_part in full_domain: + return country + return server + + +def handle_telegram_errors(func): + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except TelegramForbiddenError: + tg_id = kwargs.get("tg_id") or args[1] + logger.warning(f"🚫 Бот заблокирован пользователем {tg_id}.") + return False + except TelegramBadRequest: + tg_id = kwargs.get("tg_id") or args[1] + logger.warning(f"🚫 Чат не найден для пользователя {tg_id}.") + return False + except Exception as e: + tg_id = kwargs.get("tg_id") or args[1] + logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}") + return False + return wrapper + + +@handle_telegram_errors +async def send_notification(tg_id: int, username: str, ip: str, server: str, action: str, timestamp: str): + country = get_country_from_server(server) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) + + if action == 'block': + message = ( + f"⚠️ Замечено использование торрентов ⚠️\n\n" + f"Уважаемый пользователь, мы обнаружили использование торрент-трафика в вашей подписке.\n\n" + f"📋 Детали:" + f"
" + f"• Подписка: {username}\n" + f"• Сервер: {country}\n" + f"• Время блокировки страны: {BLOCK_DURATION} минут" + f"
\n\n" + f"❗️ Важно:\n" + f"• Возможно, вы забыли отключить торрент-клиент и сейчас качаете или раздаете торренты через VPN\n" + f"• Загрузка и раздача через торренты запрещена согласно правилам использования сервиса\n" + f"• Пожалуйста, полностью выключите торрент-клиент\n\n" + f"⏳ После истечения времени блокировки доступ будет автоматически восстановлен.\n\n" + f"⚠️ Внимание: При повторном использовании торрентов, блокировка будет применена снова." + ) + else: + message = ( + f"✅ Доступ восстановлен\n\n" + f"Уважаемый пользователь, временные ограничения для вашей подписки сняты.\n\n" + f"📋 Детали:" + f"
" + f"• Подписка: {username}\n" + f"• Сервер: {country}" + f"
\n\n" + f"💬 Напоминание:\n" + f"• Пожалуйста, воздержитесь от использования торрентов\n" + f"• Убедитесь, что торрент-клиент полностью выключен" + ) + + await bot.send_message( + chat_id=tg_id, + text=message, + parse_mode='HTML', + reply_markup=builder.as_markup() + ) + logger.info(f"Отправлено уведомление пользователю {tg_id} о {action} для подписки {username}") + return True + + +async def tblocker_webhook(request): + try: + data = await request.json() + logger.info(f"Получен запрос от tblocker: {data}") + + username = data.get('username') + ip = data.get('ip') + server = data.get('server') + action = data.get('action') + timestamp = data.get('timestamp') + + if not all([username, ip, server, action, timestamp]): + logger.error("Неполные данные в вебхуке") + return web.json_response({"error": "Missing required fields"}, status=400) + + global last_unblock_data + current_time = datetime.now().timestamp() + + last_unblock_data = { + k: v for k, v in last_unblock_data.items() + if current_time - v['received_at'] <= TIMESTAMP_TTL + } + + cache_key = f"{username}:{server}" + if action == 'unblock' and cache_key in last_unblock_data: + if timestamp == last_unblock_data[cache_key]['timestamp']: + return web.json_response({"status": "ok", "message": "duplicate unblock skipped"}) + + if action == 'unblock': + last_unblock_data[cache_key] = { + 'timestamp': timestamp, + 'received_at': current_time + } + + conn = await asyncpg.connect(DATABASE_URL) + try: + key_info = await get_key_details(username, conn) + + if not key_info: + logger.error(f"Ключ не найден для email {username}") + return web.json_response({"error": "Key not found"}, status=404) + + success = await send_notification( + key_info['tg_id'], + username, + ip, + server, + action, + timestamp + ) + + if not success: + logger.warning(f"Не удалось отправить уведомление пользователю {key_info['tg_id']}") + + return web.json_response({"status": "ok"}) + finally: + await conn.close() + + except Exception as e: + logger.error(f"Ошибка при обработке вебхука: {str(e)}") + return web.json_response({"error": str(e)}, status=500)