diff --git a/bot/handlers/client/menus.py b/bot/handlers/client/menus.py index a881ff2..cb7179f 100644 --- a/bot/handlers/client/menus.py +++ b/bot/handlers/client/menus.py @@ -4,21 +4,31 @@ from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession -from bot.keyboards.client import close_kb, construct_devices_list, main_menu, subscription_controls +from bot.keyboards.client import ( + close_kb, + construct_devices_list, + main_menu, + prompt_hwid_deletion_kb, + subscription_controls, +) from bot.keyboards.common import return_to_menu from bot.texts import ( CALLBACK_FALLBACK, FAQ_TEXT, GENERAL_PROCESSING, + HWID_LIST, NO_CONNECTIONS, NO_SUBSCRIPTION, + PROMPT_HWID_DELETION, + STANDARD_FALLBACK, + SUCCESSFUL_HWID_DELETION, ) from db.models.users import User from misc import utils from misc.utils import build_main_menu_text, format_subscription_link from schemas.di import DependenciesDTO from schemas.subscriptions import InternalSquads, SubscriptionPlan -from services.rw import get_hwid_list, get_user_by_telegram_id +from services.rw import delete_hwid, get_hwid_list, get_user_by_telegram_id router = Router() @@ -159,6 +169,7 @@ async def sub_info( ), ) + @router.callback_query(F.data == "hwid_control") async def hwid_control(cb: CallbackQuery, state: FSMContext, deps: DependenciesDTO): await state.clear() @@ -169,14 +180,46 @@ async def hwid_control(cb: CallbackQuery, state: FSMContext, deps: DependenciesD if not rw_user: await cb.message.edit_text(NO_SUBSCRIPTION, reply_markup=return_to_menu) return - + devices = await get_hwid_list(deps.rw_sdk, rw_user.uuid) if not devices: await cb.message.edit_text(NO_CONNECTIONS, reply_markup=return_to_menu) return - - await cb.message.edit_text(text="pepe", reply_markup=construct_devices_list(devices)) + hwid_limit = max(rw_user.hwid_device_limit, 3) + + await cb.message.edit_text( + text=HWID_LIST.format(count=len(devices), limit=hwid_limit), + reply_markup=construct_devices_list(devices), + ) + + +@router.callback_query(F.data.startswith("hwid:")) +async def prompt_delete_hwid(cb: CallbackQuery, state: FSMContext): + await state.clear() + hwid = cb.data.split(":")[1] + + await cb.message.edit_text(PROMPT_HWID_DELETION, reply_markup=prompt_hwid_deletion_kb(hwid)) + + +@router.callback_query(F.data.startswith("delete_hwid:")) +async def delete_hwid_bot(cb: CallbackQuery, state: FSMContext, deps: DependenciesDTO): + await state.clear() + hwid = cb.data.split(":")[1] + + await cb.message.edit_text(GENERAL_PROCESSING, reply_markup=close_kb) + rw_user = await get_user_by_telegram_id(deps.rw_sdk, cb.from_user.id) + + if not rw_user: + await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu) + return + + status = await delete_hwid(deps.rw_sdk, rw_user.uuid, hwid) + + if status: + await cb.message.edit_text(SUCCESSFUL_HWID_DELETION, reply_markup=return_to_menu) + else: + await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu) @router.callback_query(F.data == "toggle:autorenewal") diff --git a/bot/keyboards/client.py b/bot/keyboards/client.py index d15c709..6304bed 100644 --- a/bot/keyboards/client.py +++ b/bot/keyboards/client.py @@ -3,7 +3,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from remnawave.models.hwid import HwidDeviceDto from config import settings -from misc.utils import convert_duration_to_int +from misc.utils import convert_duration_to_int, format_hwid_device from schemas.subscriptions import SubscriptionDuration from .common import _return_to_menu_btn @@ -146,10 +146,24 @@ def pay_url(url: str) -> InlineKeyboardMarkup: ] ).as_markup() + def construct_devices_list(devices: list[HwidDeviceDto]) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - + for device in devices: - builder.row(InlineKeyboardButton(text=f"{device.platform} | {device.device_model} | {device.user_agent.split('/')[0]}", callback_data=f"hwid:{device.hwid}")) + builder.row( + InlineKeyboardButton( + text=format_hwid_device(device), callback_data=f"hwid:{device.hwid}" + ) + ) builder.row(_return_btn(data="sub_info")) - return builder.as_markup() \ No newline at end of file + return builder.as_markup() + + +def prompt_hwid_deletion_kb(hwid: str) -> InlineKeyboardMarkup: + return InlineKeyboardBuilder( + [ + [InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_hwid:{hwid}")], + [_return_btn("hwid_control", text="❌ Отмена")], + ] + ).as_markup() diff --git a/bot/texts.py b/bot/texts.py index a8d3208..c232df0 100644 --- a/bot/texts.py +++ b/bot/texts.py @@ -74,6 +74,13 @@ NO_SQUADS = "отсутствуют" # — HWID лимит UNLIMITED_HWID = "без лимитов" +# — OS эмодзи +WINDOWS = "💻" +LINUX = "🐧" +ANDROID = "💚" +APPLE = "🍎" +UNKNOWN_OS = "👤" + # — Трафик NO_TRAFFIC_LIMIT = "не ограничен" TRAFFIC_WITH_LIMIT = "{used} / {limit}" @@ -155,7 +162,10 @@ SUBSCRIPTION_INFORMATION = ( NO_SUBSCRIPTION = f"{MALENIA_WARN} Подписка не найдена." -NO_CONNECTIONS = f"{MALENIA_SHIELD} Вы ещё не подключались по своей ссылке." +NO_CONNECTIONS = f"{MALENIA_SHIELD} У вас нет подключённых устройств." +HWID_LIST = f"{MALENIA_SHIELD} Ваши устройства ({{count}} / {{limit}}):" +PROMPT_HWID_DELETION = f"{MALENIA_WARN} Вы уверены что хотите удалить это устройство?" +SUCCESSFUL_HWID_DELETION = f"{MALENIA_HEART} Устройство успешно удалено." # ============================================================ # РЕФЕРАЛЬНАЯ СИСТЕМА diff --git a/docker-compose.yml b/docker-compose.yml index 66491af..f9d48dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: interval: 10s timeout: 5s retries: 5 + ports: + - 5432:5432 bot: env_file: .env diff --git a/misc/utils.py b/misc/utils.py index 212198c..3dde5d6 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -3,6 +3,8 @@ import urllib.parse from datetime import UTC, datetime, timedelta from random import choice +from remnawave.models.hwid import HwidDeviceDto + from bot import texts from config import settings from schemas.subscriptions import InternalSquads, SubscriptionDuration, SubscriptionPlan @@ -271,3 +273,25 @@ def is_same_plan(rw_user: RWUserInfo, new_plan: SubscriptionPlan) -> bool: active_squad_uuids = {squad["uuid"] for squad in rw_user.active_squads} current_has_whitelist = InternalSquads.WHITELISTS in active_squad_uuids return current_has_whitelist == new_plan.whitelists + + +def _format_os_emoji(os: str) -> str: + os = os.lower() + if os == "windows": + return texts.WINDOWS + if os == "linux": + return texts.LINUX + if os == "ios": + return texts.APPLE + if os == "android": + return texts.ANDROID + return texts.UNKNOWN_OS + + +def format_hwid_device(device: HwidDeviceDto) -> str: + os = device.platform + model = device.device_model + client = device.user_agent.split("/")[0] + emoji = _format_os_emoji(os) + + return f"{emoji} {os} | {model} | {client}" diff --git a/services/rw.py b/services/rw.py index bf573e8..ca29769 100644 --- a/services/rw.py +++ b/services/rw.py @@ -1,11 +1,15 @@ import logging from dataclasses import dataclass from datetime import UTC, datetime, timedelta -from typing import List from uuid import UUID from remnawave import RemnawaveSDK -from remnawave.models import CreateUserRequestDto, UpdateUserRequestDto +from remnawave.models import ( + CreateUserRequestDto, + DeleteUserHwidDeviceResponseDto, + HWIDDeleteRequest, + UpdateUserRequestDto, +) from remnawave.models.hwid import HwidDeviceDto logger = logging.getLogger(__name__) @@ -352,14 +356,21 @@ async def create_user( logger.warning("Failed to create user username=%s: %s", username, exc) return None -async def get_hwid_list( - sdk: RemnawaveSDK, - user_uuid: str -) -> List[HwidDeviceDto] | None: + +async def get_hwid_list(sdk: RemnawaveSDK, user_uuid: str) -> list[HwidDeviceDto] | None: try: resp = await sdk.hwid.get_hwid_user(user_uuid) return resp.devices - except Exception as exc: + except Exception: logger.exception("failed to fetch hwid list for user=%s", user_uuid) return - \ No newline at end of file + + +async def delete_hwid(sdk: RemnawaveSDK, user_uuid: str, hwid: str) -> bool: + body = HWIDDeleteRequest(user_uuid=user_uuid, hwid=hwid) + try: + resp = await sdk.hwid.delete_hwid_to_user(body) + return isinstance(resp, DeleteUserHwidDeviceResponseDto) + except Exception: + logger.exception("failed to fetch hwid list for user=%s", user_uuid) + return False diff --git a/services/user_service.py b/services/user_service.py index 3afa082..c67b7ea 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -2,9 +2,8 @@ import datetime import logging import math -from aiogram.types import InlineKeyboardMarkup, Message, User as TelegramUser +from aiogram.types import Message, User as TelegramUser from remnawave import RemnawaveSDK -from remnawave.models.hwid import HwidDeviceDto from sqlalchemy.ext.asyncio import AsyncSession from bot.texts import NO_SUBSCRIPTION, NOTHING_PLACEHOLDER, SUBSCRIPTION_INFORMATION