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