diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py
index 2888698c..51c4b722 100644
--- a/handlers/admin/users/keyboard.py
+++ b/handlers/admin/users/keyboard.py
@@ -4,7 +4,7 @@ from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import RENEWAL_PRICES, TOTAL_GB
+from config import RENEWAL_PRICES, TOTAL_GB, HWID_RESET_BUTTON
from database import get_clusters
from handlers.buttons import BACK
@@ -189,11 +189,36 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
action="users_reset_traffic", data=email, tg_id=key_details["tg_id"]
).pack(),
)
+ if HWID_RESET_BUTTON:
+ builder.button(
+ text="💻 HWID",
+ callback_data=AdminUserEditorCallback(
+ action="users_hwid_menu", data=email, tg_id=key_details["tg_id"]
+ ).pack(),
+ )
builder.row(build_editor_back_btn(key_details["tg_id"], True))
builder.adjust(1)
return builder.as_markup()
+def build_hwid_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup:
+ builder = InlineKeyboardBuilder()
+ builder.button(
+ text="♻️ Сбросить HWID",
+ callback_data=AdminUserEditorCallback(
+ action="users_hwid_reset", data=email, tg_id=tg_id
+ ).pack(),
+ )
+ builder.button(
+ text="🔙 Назад",
+ callback_data=AdminUserEditorCallback(
+ action="users_key_edit", data=email, tg_id=tg_id
+ ).pack(),
+ )
+ builder.adjust(1)
+ return builder.as_markup()
+
+
def build_key_delete_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py
index 83fabe76..95f8a2e5 100644
--- a/handlers/admin/users/users_handler.py
+++ b/handlers/admin/users/users_handler.py
@@ -14,7 +14,7 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import RENEWAL_PRICES, TOTAL_GB, USE_COUNTRY_SELECTION
+from config import RENEWAL_PRICES, TOTAL_GB, USE_COUNTRY_SELECTION, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
from database import (
delete_key,
delete_user_data,
@@ -39,6 +39,7 @@ from handlers.keys.key_utils import (
from handlers.utils import generate_random_email, sanitize_key_name
from logger import logger
from utils.csv_export import export_referrals_csv
+from panels.remnawave import RemnawaveAPI
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn, build_admin_back_kb
from .keyboard import (
@@ -54,6 +55,7 @@ from .keyboard import (
build_users_balance_kb,
build_users_key_expiry_kb,
build_users_key_show_kb,
+ build_hwid_menu_kb
)
@@ -73,6 +75,87 @@ class UserEditorState(StatesGroup):
selecting_country = State()
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_hwid_menu"), IsAdminFilter())
+async def handle_hwid_menu(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
+ email = callback_data.data
+ tg_id = callback_data.tg_id
+
+ client_id = await get_client_id_by_email(email)
+ if not client_id:
+ await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.")
+ return
+
+ servers = await get_servers()
+ remna_server = None
+ for cluster_servers in servers.values():
+ for server in cluster_servers:
+ if server.get("panel_type", "") == "remnawave":
+ remna_server = server
+ break
+ if remna_server:
+ break
+
+ if not remna_server:
+ await callback_query.message.edit_text("🚫 Нет доступного сервера Remnawave.")
+ return
+
+ api = RemnawaveAPI(remna_server["api_url"])
+ if not await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
+ await callback_query.message.edit_text("❌ Ошибка авторизации в Remnawave.")
+ return
+
+ devices = await api.get_user_hwid_devices(client_id)
+ count = len(devices) if devices else 0
+
+ text = f"💻 HWID устройства\n\nПривязано: {count}"
+ await callback_query.message.edit_text(text, reply_markup=build_hwid_menu_kb(email, tg_id))
+
+
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_hwid_reset"), IsAdminFilter())
+async def handle_hwid_reset(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
+ email = callback_data.data
+ tg_id = callback_data.tg_id
+
+ client_id = await get_client_id_by_email(email)
+ if not client_id:
+ await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.")
+ return
+
+ servers = await get_servers()
+ remna_server = None
+ for cluster_servers in servers.values():
+ for server in cluster_servers:
+ if server.get("panel_type", "") == "remnawave":
+ remna_server = server
+ break
+ if remna_server:
+ break
+
+ if not remna_server:
+ await callback_query.message.edit_text("🚫 Нет доступного сервера Remnawave.")
+ return
+
+ api = RemnawaveAPI(remna_server["api_url"])
+ if not await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
+ await callback_query.message.edit_text("❌ Ошибка авторизации в Remnawave.")
+ return
+
+ devices = await api.get_user_hwid_devices(client_id)
+ if not devices:
+ await callback_query.message.edit_text("ℹ️ У пользователя нет привязанных устройств.", reply_markup=build_editor_kb(tg_id, True))
+ return
+
+ deleted = 0
+ for d in devices:
+ if await api.delete_user_hwid_device(client_id, d["hwid"]):
+ deleted += 1
+
+ await callback_query.message.edit_text(
+ f"✅ Удалено HWID-устройств: {deleted} из {len(devices)}.",
+ reply_markup=build_editor_kb(tg_id, True),
+ )
+
+
@router.callback_query(
AdminPanelCallback.filter(F.action == "search_user"),
IsAdminFilter(),
diff --git a/handlers/buttons.py b/handlers/buttons.py
index a305ffd7..d18c3f8e 100644
--- a/handlers/buttons.py
+++ b/handlers/buttons.py
@@ -73,6 +73,7 @@ MANUAL_INSTRUCTIONS = "📖 Ручная установка"
RENEW_KEY = "🔄 Продлить подписку"
TV_CONTINUE = "▶ Продолжить"
TV_INSTRUCTIONS = "📖 Полная инструкция"
+HWID_BUTTON = "♻️ Сбросить привязку"
# Кнопки касс
diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py
index 896a8561..7875c336 100644
--- a/handlers/keys/key_view.py
+++ b/handlers/keys/key_view.py
@@ -20,10 +20,12 @@ from config import (
QRCODE,
TOGGLE_CLIENT,
USE_COUNTRY_SELECTION,
+ HWID_RESET_BUTTON
)
from database import (
get_key_details,
get_keys,
+ get_servers
)
from handlers.buttons import (
ADD_SUB,
@@ -41,6 +43,7 @@ from handlers.buttons import (
RENEW_FULL,
TV_BUTTON,
UNFREEZE,
+ HWID_BUTTON
)
from handlers.texts import (
FROZEN_SUBSCRIPTION_MSG,
@@ -178,145 +181,172 @@ async def handle_new_alias_input(message: Message, state: FSMContext, session: A
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: CallbackQuery, session: Any):
- tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
- try:
- record = await get_key_details(key_name, session)
- if not record:
- await callback_query.message.answer(text="Информация о подписке не найдена.")
- return
+ image_path = os.path.join("img", "pic_view.jpg")
+ await render_key_info(callback_query.message, session, key_name, image_path)
- is_frozen = record["is_frozen"]
- builder = InlineKeyboardBuilder()
- image_path = os.path.join("img", "pic_view.jpg")
- if not os.path.isfile(image_path):
- await callback_query.message.answer("Файл изображения не найден.")
- return
+async def render_key_info(message: Message, session: Any, key_name: str, image_path: str):
+ from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
+ from panels.remnawave import RemnawaveAPI
- if is_frozen:
- builder.row(
- InlineKeyboardButton(
- text=UNFREEZE,
- callback_data=f"unfreeze_subscription|{key_name}",
- )
- )
- builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
- builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
+ record = await get_key_details(key_name, session)
+ if not record:
+ await message.answer("Информация о подписке не найдена.")
+ return
- await edit_or_send_message(
- target_message=callback_query.message,
- text=FROZEN_SUBSCRIPTION_MSG,
- reply_markup=builder.as_markup(),
- media_path=image_path,
- )
- return
+ is_frozen = record["is_frozen"]
+ email = record["email"]
+ client_id = record.get("client_id")
+ remnawave_link = record.get("remnawave_link")
+ key = record.get("key")
+ final_link = key or remnawave_link
- key = record.get("key")
- remnawave_link = record.get("remnawave_link")
- final_link = key or remnawave_link
-
- expiry_time = record["expiry_time"]
- server_name = record["server_id"]
- expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
- time_left = expiry_date - datetime.utcnow()
-
- if time_left.total_seconds() <= 0:
- days_left_message = "🕒 Статус подписки:\n🔴 Истекла"
- else:
- total_seconds = int(time_left.total_seconds())
- days = total_seconds // 86400
- hours = (total_seconds % 86400) // 3600
- minutes = (total_seconds % 3600) // 60
- days_left_message = (
- f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}"
- )
-
- formatted_expiry_date = (
- f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года"
- )
- response_message = key_message(
- final_link,
- formatted_expiry_date,
- days_left_message,
- server_name,
- server_name if USE_COUNTRY_SELECTION else None,
- )
-
- if ENABLE_UPDATE_SUBSCRIPTION_BUTTON:
- builder.row(
- InlineKeyboardButton(
- text="🔄 Обновить подписку",
- callback_data=f"update_subscription|{key_name}",
- )
- )
-
- is_full_remnawave = await is_full_remnawave_cluster(server_name, session)
- if is_full_remnawave and final_link:
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_DEVICE,
- web_app=WebAppInfo(url=final_link),
- )
- )
- else:
- if CONNECT_PHONE_BUTTON:
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_PHONE,
- callback_data=f"connect_phone|{key_name}",
- )
- )
- builder.row(
- InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"),
- InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"),
- )
- else:
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_DEVICE,
- callback_data=f"connect_device|{key_name}",
- )
- )
-
- if QRCODE:
- builder.row(
- InlineKeyboardButton(
- text=QR,
- callback_data=f"show_qr|{key_name}",
- )
- )
- if ENABLE_DELETE_KEY_BUTTON:
- builder.row(
- InlineKeyboardButton(text=RENEW, callback_data=f"renew_key|{key_name}"),
- InlineKeyboardButton(text=DELETE, callback_data=f"delete_key|{key_name}"),
- )
- else:
- builder.row(InlineKeyboardButton(text=RENEW_FULL, callback_data=f"renew_key|{key_name}"))
-
- if USE_COUNTRY_SELECTION:
- builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}"))
-
- if TOGGLE_CLIENT:
- builder.row(
- InlineKeyboardButton(
- text=FREEZE,
- callback_data=f"freeze_subscription|{key_name}",
- )
- )
+ builder = InlineKeyboardBuilder()
+ if is_frozen:
+ builder.row(InlineKeyboardButton(text=UNFREEZE, callback_data=f"unfreeze_subscription|{key_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
-
await edit_or_send_message(
- target_message=callback_query.message,
- text=response_message,
+ target_message=message,
+ text=FROZEN_SUBSCRIPTION_MSG,
reply_markup=builder.as_markup(),
media_path=image_path,
)
- except Exception as e:
- await handle_error(
- tg_id,
- callback_query,
- f"Ошибка при получении информации о ключе: {e}",
+ return
+
+ expiry_time = record["expiry_time"]
+ server_name = record["server_id"]
+ expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
+ time_left = expiry_date - datetime.utcnow()
+
+ if time_left.total_seconds() <= 0:
+ days_left_message = "🕒 Статус подписки:\n🔴 Истекла"
+ else:
+ total_seconds = int(time_left.total_seconds())
+ days = total_seconds // 86400
+ hours = (total_seconds % 86400) // 3600
+ minutes = (total_seconds % 3600) // 60
+ days_left_message = (
+ f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}"
)
+
+ formatted_expiry_date = (
+ f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года"
+ )
+
+ hwid_count = 0
+ is_full_remnawave = await is_full_remnawave_cluster(server_name, session)
+ if is_full_remnawave and client_id:
+ servers = await get_servers()
+ remna_server = next(
+ (srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None
+ )
+ if remna_server:
+ api = RemnawaveAPI(remna_server["api_url"])
+ if await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
+ devices = await api.get_user_hwid_devices(client_id)
+ hwid_count = len(devices or [])
+
+ response_message = key_message(
+ final_link,
+ formatted_expiry_date,
+ days_left_message,
+ server_name,
+ server_name if USE_COUNTRY_SELECTION else None,
+ hwid_count=hwid_count,
+ )
+
+ if ENABLE_UPDATE_SUBSCRIPTION_BUTTON:
+ builder.row(InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}"))
+
+ if is_full_remnawave and final_link:
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
+ else:
+ if CONNECT_PHONE_BUTTON:
+ builder.row(InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"),
+ InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"),
+ )
+ else:
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+
+ if HWID_RESET_BUTTON and hwid_count > 0:
+ builder.row(
+ InlineKeyboardButton(
+ text=HWID_BUTTON,
+ callback_data=f"reset_hwid|{key_name}",
+ )
+ )
+
+ if QRCODE:
+ builder.row(InlineKeyboardButton(text=QR, callback_data=f"show_qr|{key_name}"))
+
+ if ENABLE_DELETE_KEY_BUTTON:
+ builder.row(
+ InlineKeyboardButton(text=RENEW, callback_data=f"renew_key|{key_name}"),
+ InlineKeyboardButton(text=DELETE, callback_data=f"delete_key|{key_name}"),
+ )
+ else:
+ builder.row(InlineKeyboardButton(text=RENEW_FULL, callback_data=f"renew_key|{key_name}"))
+
+ if USE_COUNTRY_SELECTION:
+ builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}"))
+
+ if TOGGLE_CLIENT:
+ builder.row(InlineKeyboardButton(text=FREEZE, callback_data=f"freeze_subscription|{key_name}"))
+
+ builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
+ builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
+
+ await edit_or_send_message(
+ target_message=message,
+ text=response_message,
+ reply_markup=builder.as_markup(),
+ media_path=image_path,
+ )
+
+
+@router.callback_query(F.data.startswith("reset_hwid|"))
+async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
+ from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
+ from panels.remnawave import RemnawaveAPI
+
+ key_name = callback_query.data.split("|")[1]
+ record = await get_key_details(key_name, session)
+ if not record:
+ await callback_query.answer("❌ Ключ не найден.", show_alert=True)
+ return
+
+ client_id = record.get("client_id")
+ if not client_id:
+ await callback_query.answer("❌ У ключа отсутствует client_id.", show_alert=True)
+ return
+
+ servers = await get_servers()
+ remna_server = next(
+ (srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None
+ )
+ if not remna_server:
+ await callback_query.answer("❌ Remnawave-сервер не найден.", show_alert=True)
+ return
+
+ api = RemnawaveAPI(remna_server["api_url"])
+ if not await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
+ await callback_query.answer("❌ Авторизация в Remnawave не удалась.", show_alert=True)
+ return
+
+ devices = await api.get_user_hwid_devices(client_id)
+ if not devices:
+ await callback_query.answer("✅ Устройства не были привязаны.", show_alert=True)
+ else:
+ deleted = 0
+ for device in devices:
+ if await api.delete_user_hwid_device(client_id, device["hwid"]):
+ deleted += 1
+ await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True)
+
+ image_path = os.path.join("img", "pic_view.jpg")
+ await render_key_info(callback_query.message, session, key_name, image_path)
diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so
index cff2c06c..d466062c 100644
Binary files a/panels/remnawave.cpython-312-x86_64-linux-gnu.so and b/panels/remnawave.cpython-312-x86_64-linux-gnu.so differ