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)