diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit
index 93f252c1..4194ed90 100644
--- a/.github/hooks/pre-commit
+++ b/.github/hooks/pre-commit
@@ -1,23 +1,17 @@
#!/bin/sh
-# Сохраняем текущие изменения
echo "Сохранение текущих изменений..."
git stash -q --keep-index
-# Запускаем форматирование кода
echo "Запуск Ruff format..."
ruff format . --config pyproject.toml --exclude main.py,handlers/payments
-# Запускаем проверку и исправление кода
echo "Запуск Ruff check с автоисправлением..."
ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
-# Добавляем изменения, внесенные форматированием
git add -u
-# Восстанавливаем сохраненные изменения
echo "Восстановление сохраненных изменений..."
git stash pop -q
-# Выход с кодом 0 (успешно)
exit 0
\ No newline at end of file
diff --git a/api/v2/routes/auth.py b/api/v2/routes/auth.py
index 5d3807de..c9105120 100644
--- a/api/v2/routes/auth.py
+++ b/api/v2/routes/auth.py
@@ -20,7 +20,7 @@ from utils.telegram_login import verify_telegram_login
router = APIRouter(prefix="/auth", tags=["Auth"])
TOKEN_TTL_HINT = "бессрочно" if API_TOKEN_TTL_DAYS is None else f"{API_TOKEN_TTL_DAYS} дн."
-TELEGRAM_LOGIN_MAX_AGE = 86400 # 24 часа
+TELEGRAM_LOGIN_MAX_AGE = 86400
@router.post("/register", response_model=RegisterResponse)
@@ -66,7 +66,7 @@ async def login(
_LOGIN_CODES: dict[str, tuple[str, float]] = {}
-_LOGIN_CODE_TTL = 600.0 # 10 min
+_LOGIN_CODE_TTL = 600.0
def _clean_login_codes() -> None:
diff --git a/api/v2/routes/management.py b/api/v2/routes/management.py
index c84f636f..5572c514 100644
--- a/api/v2/routes/management.py
+++ b/api/v2/routes/management.py
@@ -210,7 +210,6 @@ def _parse_date_range(
return start, end
except ValueError as e:
raise HTTPException(status_code=400, detail=f"Неверный формат дат: {e}")
- # по умолчанию — вчера
end = datetime.now(tz).replace(hour=0, minute=0, second=0, microsecond=0)
start = end - timedelta(days=1)
return start, end
diff --git a/audit/rules.py b/audit/rules.py
index ae7209af..ed8152c0 100644
--- a/audit/rules.py
+++ b/audit/rules.py
@@ -110,10 +110,6 @@ _CALLBACK_PREFIX: list[tuple[str, str]] = [
("cfg_user_traffic|", "tariff_config"),
("rename_key|", "key_manage"),
("reset_hwid|", "key_manage"),
- ("freeze_subscription|", "key_manage"),
- ("freeze_subscription_confirm|", "key_manage"),
- ("unfreeze_subscription|", "key_manage"),
- ("unfreeze_subscription_confirm|", "key_manage"),
("delete_key|", "key_manage"),
("confirm_delete|", "key_manage"),
("update_subscription|", "key_manage"),
@@ -243,8 +239,6 @@ _HANDLER_CONTAINS: list[tuple[str, str] | tuple[str, str, str]] = [
("robokassa_", "pay_start"),
("rename_key", "key_manage"),
("reset_hwid", "key_manage"),
- ("freeze_subscription", "key_manage"),
- ("unfreeze_subscription", "key_manage"),
("delete_key", "key_manage"),
("change_location", "key_manage"),
("select_country", "key_manage"),
diff --git a/core/settings/buttons_config.py b/core/settings/buttons_config.py
index fe6070a6..74bfe136 100644
--- a/core/settings/buttons_config.py
+++ b/core/settings/buttons_config.py
@@ -9,6 +9,7 @@ from .runtime_sync import publish_runtime_config, register_runtime_config
BUTTONS_CONFIG: dict[str, bool] = DEFAULT_BUTTONS_CONFIG.copy()
+BUTTONS_CONFIG.pop("TOGGLE_CLIENT_BUTTON_ENABLE", None)
BUTTONS_CONFIG.setdefault("ANDROID_TV_BUTTON_ENABLE", False)
BUTTONS_CONFIG.setdefault("COUPON_BUTTON_ENABLE", True)
register_runtime_config("BUTTONS_CONFIG", BUTTONS_CONFIG)
@@ -21,6 +22,7 @@ async def load_buttons_config(session: AsyncSession) -> None:
if setting is None:
buttons_config = DEFAULT_BUTTONS_CONFIG.copy()
+ buttons_config.pop("TOGGLE_CLIENT_BUTTON_ENABLE", None)
buttons_config.setdefault("ANDROID_TV_BUTTON_ENABLE", False)
buttons_config.setdefault("COUPON_BUTTON_ENABLE", True)
setting = Setting(
@@ -33,6 +35,7 @@ async def load_buttons_config(session: AsyncSession) -> None:
stored = setting.value or {}
buttons_config = DEFAULT_BUTTONS_CONFIG.copy()
buttons_config.update(stored)
+ buttons_config.pop("TOGGLE_CLIENT_BUTTON_ENABLE", None)
buttons_config.setdefault("ANDROID_TV_BUTTON_ENABLE", False)
buttons_config.setdefault("COUPON_BUTTON_ENABLE", True)
setting.value = buttons_config
@@ -61,6 +64,7 @@ async def update_buttons_config(session: AsyncSession, new_values: dict[str, boo
buttons_config = DEFAULT_BUTTONS_CONFIG.copy()
buttons_config.update(new_values)
+ buttons_config.pop("TOGGLE_CLIENT_BUTTON_ENABLE", None)
buttons_config.setdefault("ANDROID_TV_BUTTON_ENABLE", False)
buttons_config.setdefault("COUPON_BUTTON_ENABLE", True)
diff --git a/database/keys.py b/database/keys.py
index 889870d1..ac32f31a 100644
--- a/database/keys.py
+++ b/database/keys.py
@@ -12,7 +12,7 @@ from core.cache_config import (
KEYS_LIST_CACHE_TTL_SEC,
)
from core.redis_cache import cache_delete, cache_get, cache_key, cache_set
-from database.models import Key, User
+from database.models import Key, Tariff, User
from database.users import invalidate_profile_cache, invalidate_user_snapshot
from logger import logger
@@ -166,6 +166,51 @@ async def get_key_by_server(session: AsyncSession, tg_id: int, client_id: str):
return result.scalar_one_or_none()
+async def get_key_by_email(session: AsyncSession, email: str, tg_id: int | None = None) -> Key | None:
+ stmt = select(Key).where(Key.email == email)
+ if tg_id is not None:
+ stmt = stmt.where(Key.tg_id == tg_id)
+ result = await session.execute(stmt.limit(1))
+ return result.scalar_one_or_none()
+
+
+async def get_key_by_client_id(session: AsyncSession, client_id: str, tg_id: int | None = None) -> Key | None:
+ stmt = select(Key).where(Key.client_id == client_id)
+ if tg_id is not None:
+ stmt = stmt.where(Key.tg_id == tg_id)
+ result = await session.execute(stmt.limit(1))
+ return result.scalar_one_or_none()
+
+
+async def get_key_expiry_presets(session: AsyncSession, email: str) -> tuple[str | None, list[int]]:
+ key_obj = await get_key_by_email(session, email)
+ if not key_obj:
+ return None, []
+
+ if not key_obj.tariff_id:
+ return key_obj.client_id, []
+
+ tariff = await session.execute(select(Tariff.group_code).where(Tariff.id == key_obj.tariff_id))
+ group_code = tariff.scalar_one_or_none()
+ if not group_code:
+ return key_obj.client_id, []
+
+ result = await session.execute(
+ select(Tariff.duration_days)
+ .where(Tariff.group_code == group_code, Tariff.is_active.is_(True))
+ .order_by(Tariff.duration_days)
+ )
+ unique_durations: list[int] = []
+ seen: set[int] = set()
+ for (days,) in result.all():
+ if days is None or days < 1 or days in seen:
+ continue
+ seen.add(int(days))
+ unique_durations.append(int(days))
+
+ return key_obj.client_id, unique_durations
+
+
async def get_key_details(session: AsyncSession, email: str) -> dict | None:
"""Возвращает подробную информацию о ключе по email. Горячие данные кэшируются в Redis."""
ckey = cache_key("key_details", email)
@@ -347,6 +392,24 @@ async def update_key_link(session: AsyncSession, email: str, link: str) -> bool:
return ok
+async def update_key_subscription_links(session: AsyncSession, email: str, link: str) -> bool:
+ stmt = (
+ update(Key)
+ .where(Key.email == email)
+ .values(
+ key=link,
+ remnawave_link=link,
+ )
+ .returning(Key.client_id)
+ )
+ res = await session.execute(stmt)
+ await session.commit()
+ ok = res.scalar_one_or_none() is not None
+ if ok:
+ await invalidate_key_details(email)
+ return ok
+
+
async def save_key_config_with_mode(
session: AsyncSession,
email: str,
@@ -381,6 +444,76 @@ async def save_key_config_with_mode(
await invalidate_key_details(email)
+async def reset_key_tariff_state(session: AsyncSession, tg_id: int, email: str, tariff_id: int) -> None:
+ await session.execute(
+ update(Key)
+ .where(Key.tg_id == tg_id, Key.email == email)
+ .values(
+ tariff_id=tariff_id,
+ selected_device_limit=None,
+ current_device_limit=None,
+ selected_traffic_limit=None,
+ current_traffic_limit=None,
+ selected_price_rub=None,
+ )
+ )
+ await session.commit()
+ await invalidate_keys_list(tg_id)
+ await invalidate_key_details(email)
+
+
+async def save_key_tariff_selection(
+ session: AsyncSession,
+ tg_id: int,
+ email: str,
+ tariff_id: int,
+ selected_devices: int | None,
+ selected_traffic_gb: int | None,
+) -> None:
+ selected_devices_val = int(selected_devices) if selected_devices is not None else None
+ selected_traffic_val = int(selected_traffic_gb) if selected_traffic_gb is not None and int(selected_traffic_gb) > 0 else None
+
+ await session.execute(
+ update(Key)
+ .where(Key.tg_id == tg_id, Key.email == email)
+ .values(
+ tariff_id=tariff_id,
+ selected_device_limit=selected_devices_val,
+ current_device_limit=selected_devices_val,
+ selected_traffic_limit=selected_traffic_val,
+ current_traffic_limit=selected_traffic_val,
+ selected_price_rub=None,
+ )
+ )
+ await session.commit()
+ await invalidate_keys_list(tg_id)
+ await invalidate_key_details(email)
+
+
+async def save_admin_key_config(
+ session: AsyncSession,
+ email: str,
+ base_devices: int,
+ total_devices: int,
+ base_traffic: int | None,
+ total_traffic: int | None,
+ selected_price: int | None,
+) -> None:
+ await session.execute(
+ update(Key)
+ .where(Key.email == email)
+ .values(
+ selected_device_limit=base_devices,
+ current_device_limit=total_devices,
+ selected_traffic_limit=base_traffic,
+ current_traffic_limit=total_traffic,
+ selected_price_rub=selected_price,
+ )
+ )
+ await session.commit()
+ await invalidate_key_details(email)
+
+
async def reset_key_current_limits_to_selected(session: AsyncSession, client_id: str):
"""Сбрасывает текущие лимиты к выбранным для ключа."""
await session.execute(
diff --git a/database/referrals.py b/database/referrals.py
index 730e85ef..10eb5466 100644
--- a/database/referrals.py
+++ b/database/referrals.py
@@ -162,7 +162,7 @@ async def get_total_referral_bonus(session: AsyncSession, referrer_tg_id: int, m
)
result = await session.execute(
- text(bonus_query), # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
+ text(bonus_query),
{"tg_id": referrer_tg_id, "max_levels": max_levels},
)
total_bonus_raw = result.scalar()
@@ -193,7 +193,7 @@ async def get_referrals_by_level(session: AsyncSession, referrer_tg_id: int, max
ORDER BY level
"""
result = await session.execute(
- text(query), # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
+ text(query),
{"referrer_tg_id": referrer_tg_id, "max_levels": max_levels},
)
return {
diff --git a/database/servers.py b/database/servers.py
index b3d4449f..99eddad1 100644
--- a/database/servers.py
+++ b/database/servers.py
@@ -124,6 +124,12 @@ async def get_clusters(session: AsyncSession) -> list[str]:
return [r[0] for r in result.all()]
+async def get_server_names(session: AsyncSession) -> list[str]:
+ stmt = select(Server.server_name).distinct().order_by(Server.server_name)
+ result = await session.execute(stmt)
+ return [r[0] for r in result.all()]
+
+
async def check_unique_server_name(session: AsyncSession, server_name: str, cluster_name: str | None = None) -> bool:
stmt = select(Server).where(Server.server_name == server_name)
if cluster_name:
diff --git a/database/tariffs.py b/database/tariffs.py
index f06f43a4..a17f722a 100644
--- a/database/tariffs.py
+++ b/database/tariffs.py
@@ -147,6 +147,18 @@ async def get_tariff_by_id(session: AsyncSession, tariff_id: int):
return None
+async def get_tariff_group_codes(session: AsyncSession) -> list[str]:
+ result = await session.execute(select(Tariff.group_code).distinct().order_by(Tariff.group_code))
+ return [row[0] for row in result.fetchall() if row[0]]
+
+
+async def get_active_tariffs_by_group_code(session: AsyncSession, group_code: str) -> list[Tariff]:
+ result = await session.execute(
+ select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True)).order_by(Tariff.id)
+ )
+ return result.scalars().all()
+
+
async def get_tariffs_for_cluster(session: AsyncSession, cluster_name: str):
key = cache_key("tariffs_cluster", cluster_name)
cached = await cache_get(key)
diff --git a/handlers/admin/clusters/__init__.py b/handlers/admin/clusters/__init__.py
index 96d6867a..bc48e482 100644
--- a/handlers/admin/clusters/__init__.py
+++ b/handlers/admin/clusters/__init__.py
@@ -1,5 +1,12 @@
-from . import cluster_manage, cluster_sync, cluster_tariffs, cluster_transfers, cluster_wizard # noqa
+from . import cluster_manage, cluster_sync, cluster_tariffs, cluster_transfers, cluster_wizard
from .base import AdminClusterStates, router
-
-__all__ = ["router", "AdminClusterStates"]
+__all__ = [
+ "router",
+ "AdminClusterStates",
+ "cluster_manage",
+ "cluster_sync",
+ "cluster_tariffs",
+ "cluster_transfers",
+ "cluster_wizard",
+]
diff --git a/handlers/admin/management/__init__.py b/handlers/admin/management/__init__.py
index 407200c6..0e9625e0 100644
--- a/handlers/admin/management/__init__.py
+++ b/handlers/admin/management/__init__.py
@@ -1,14 +1,16 @@
from aiogram import Router
+from . import admins, database, domain, file_upload, import_3xui, import_remnawave, maintenance
router = Router()
-from . import (
- admins, # noqa: F401
- database, # noqa: F401
- domain, # noqa: F401
- file_upload, # noqa: F401
- import_3xui, # noqa: F401
- import_remnawave, # noqa: F401
- maintenance, # noqa: F401
+__all__ = (
+ "router",
+ "admins",
+ "database",
+ "domain",
+ "file_upload",
+ "import_3xui",
+ "import_remnawave",
+ "maintenance",
)
diff --git a/handlers/admin/servers/__init__.py b/handlers/admin/servers/__init__.py
index 75d9cd89..0e443e28 100644
--- a/handlers/admin/servers/__init__.py
+++ b/handlers/admin/servers/__init__.py
@@ -1,5 +1,8 @@
-from . import (
- server_manage, # noqa: F401
- server_wizard, # noqa: F401
-)
+from . import server_manage, server_wizard
from .server_states import router
+
+__all__ = (
+ "router",
+ "server_manage",
+ "server_wizard",
+)
diff --git a/handlers/admin/settings/settings_config.py b/handlers/admin/settings/settings_config.py
index 7ad5ca69..b0c1c937 100644
--- a/handlers/admin/settings/settings_config.py
+++ b/handlers/admin/settings/settings_config.py
@@ -8,7 +8,6 @@ BUTTON_TITLES: Final[dict[str, str]] = {
"REFERRAL_QR_BUTTON_ENABLE": "QR реф.меню",
"DELETE_KEY_BUTTON_ENABLE": "Удалить подп-ку",
"INSTRUCTIONS_BUTTON_ENABLE": "Инструкции",
- "TOGGLE_CLIENT_BUTTON_ENABLE": "Заморозка подписки",
"GIFT_BUTTON_ENABLE": "Подарки",
"REFERRAL_BUTTON_ENABLE": "Реф.система",
"TOP_REFERRAL_BUTTON_ENABLE": "Топ-5 рефералов",
diff --git a/handlers/admin/tariffs/__init__.py b/handlers/admin/tariffs/__init__.py
index 45c370d5..605ebe6f 100644
--- a/handlers/admin/tariffs/__init__.py
+++ b/handlers/admin/tariffs/__init__.py
@@ -1,11 +1,13 @@
from aiogram import Router
+from . import tariff_configurator, tariff_manage, tariff_sorting, tariff_subgroups
router = Router()
-from . import ( # noqa: F401
- tariff_configurator,
- tariff_manage,
- tariff_sorting,
- tariff_subgroups,
+__all__ = (
+ "router",
+ "tariff_configurator",
+ "tariff_manage",
+ "tariff_sorting",
+ "tariff_subgroups",
)
diff --git a/handlers/admin/tariffs/tariff_configurator/__init__.py b/handlers/admin/tariffs/tariff_configurator/__init__.py
index 66f7f8e0..ea7a8482 100644
--- a/handlers/admin/tariffs/tariff_configurator/__init__.py
+++ b/handlers/admin/tariffs/tariff_configurator/__init__.py
@@ -1,6 +1,8 @@
-from . import ( # noqa: F401
- common,
- device_pricing,
- options,
- traffic_pricing,
+from . import common, device_pricing, options, traffic_pricing
+
+__all__ = (
+ "common",
+ "device_pricing",
+ "options",
+ "traffic_pricing",
)
diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py
index 3ee5b5c5..143f3098 100644
--- a/handlers/admin/users/keyboard.py
+++ b/handlers/admin/users/keyboard.py
@@ -3,19 +3,18 @@ from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from handlers.buttons import BACK
from config import HWID_RESET_BUTTON
-from database import get_clusters
-from database.models import Key, Tariff
+from database import get_clusters, get_key_expiry_presets
from handlers.buttons import BACK, FREEZE, UNFREEZE
from handlers.utils import format_days
from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from ..panel.keyboard import build_admin_back_btn
+from .utils import build_admin_key_ref
class AdminUserEditorCallback(CallbackData, prefix="admin_users"):
@@ -51,12 +50,13 @@ async def build_user_edit_kb(
for record in key_records:
email = record.email
+ key_ref = build_admin_key_ref(record.client_id, email)
expiry = datetime.fromtimestamp(record.expiry_time / 1000, tz=timezone.utc)
days = (expiry - current_time).days
builder.row(
InlineKeyboardButton(
text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)",
- callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=str(email)).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref).pack(),
)
)
@@ -191,74 +191,63 @@ async def build_users_balance_kb(
return builder.as_markup()
-def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
+def build_users_key_show_kb(tg_id: int, key_ref: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email, edit=True).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref, edit=True).pack(),
)
return builder.as_markup()
-async def build_users_key_expiry_kb(session: AsyncSession, tg_id: int, email: str) -> InlineKeyboardMarkup:
+async def build_users_key_expiry_kb(
+ session: AsyncSession,
+ tg_id: int,
+ email: str,
+ key_ref: str | None = None,
+) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
+ client_id, preset_durations = await get_key_expiry_presets(session, email)
+ resolved_key_ref = key_ref or build_admin_key_ref(client_id, email)
- result = await session.execute(select(Key.server_id, Key.tariff_id).where(Key.email == email))
- row = result.first()
- _server_id, tariff_id = row if row else (None, None)
-
- if tariff_id:
- result = await session.execute(select(Tariff.group_code).where(Tariff.id == tariff_id))
- row = result.first()
- if row and row[0]:
- group_code = row[0]
- result = await session.execute(
- select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True))
- )
- tariffs = result.scalars().all()
- unique_durations = set()
- for tariff in tariffs:
- days = tariff.duration_days
- if days < 1 or days in unique_durations:
- continue
- unique_durations.add(days)
- label = format_days(days)
- builder.row(
- InlineKeyboardButton(
- text=f"+ {label}",
- callback_data=AdminUserKeyEditorCallback(
- action="add", tg_id=tg_id, data=email, month=days
- ).pack(),
- ),
- InlineKeyboardButton(
- text=f"- {label}",
- callback_data=AdminUserKeyEditorCallback(
- action="add", tg_id=tg_id, data=email, month=-days
- ).pack(),
- ),
- )
+ for days in preset_durations:
+ label = format_days(days)
+ builder.row(
+ InlineKeyboardButton(
+ text=f"+ {label}",
+ callback_data=AdminUserKeyEditorCallback(
+ action="add", tg_id=tg_id, data=resolved_key_ref, month=days
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text=f"- {label}",
+ callback_data=AdminUserKeyEditorCallback(
+ action="add", tg_id=tg_id, data=resolved_key_ref, month=-days
+ ).pack(),
+ ),
+ )
builder.row(
InlineKeyboardButton(
text="⏳ Добавить дни",
- callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=resolved_key_ref).pack(),
),
InlineKeyboardButton(
text="⏳ Вычесть дни",
- callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=resolved_key_ref).pack(),
),
)
builder.row(
InlineKeyboardButton(
text="⏳ Установить дату истечения",
- callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=resolved_key_ref).pack(),
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=resolved_key_ref).pack(),
)
)
@@ -276,18 +265,24 @@ def build_user_delete_kb(tg_id: int):
return builder.as_markup()
-def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
+def build_user_key_kb(tg_id: int, key_ref: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref).pack(),
)
builder.adjust(1)
return builder.as_markup()
-def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = False) -> InlineKeyboardMarkup:
+def build_key_edit_kb(
+ key_details: dict,
+ email: str,
+ is_configurable: bool = False,
+ key_ref: str | None = None,
+) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
+ resolved_key_ref = key_ref or build_admin_key_ref(key_details.get("client_id"), email)
is_frozen = (
key_details.get("is_frozen") if isinstance(key_details, dict) else getattr(key_details, "is_frozen", False)
@@ -296,38 +291,44 @@ def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = Fal
builder.button(
text="⏳ Время истечения",
callback_data=AdminUserEditorCallback(
- action="users_expiry_edit", data=email, tg_id=key_details["tg_id"]
+ action="users_expiry_edit", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
builder.button(
text="🔄 Перевыпуск подписки",
callback_data=AdminUserEditorCallback(
- action="users_reissue_menu", data=email, tg_id=key_details["tg_id"]
+ action="users_reissue_menu", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
builder.button(
text="📦 Тариф",
- callback_data=AdminUserEditorCallback(action="users_renew", data=email, tg_id=key_details["tg_id"]).pack(),
+ callback_data=AdminUserEditorCallback(
+ action="users_renew", data=resolved_key_ref, tg_id=key_details["tg_id"]
+ ).pack(),
)
if is_configurable:
builder.button(
text="📱 Конфигурация",
callback_data=AdminUserEditorCallback(
- action="users_edit_config", data=email, tg_id=key_details["tg_id"]
+ action="users_edit_config", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
builder.button(
text="❌ Удалить",
- callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(),
+ callback_data=AdminUserEditorCallback(
+ action="users_delete_key", data=resolved_key_ref, tg_id=key_details["tg_id"]
+ ).pack(),
)
builder.button(
text="📊 Трафик",
- callback_data=AdminUserEditorCallback(action="users_traffic", data=email, tg_id=key_details["tg_id"]).pack(),
+ callback_data=AdminUserEditorCallback(
+ action="users_traffic", data=resolved_key_ref, tg_id=key_details["tg_id"]
+ ).pack(),
)
builder.button(
text="♻️ Сбросить трафик",
callback_data=AdminUserEditorCallback(
- action="users_reset_traffic", data=email, tg_id=key_details["tg_id"]
+ action="users_reset_traffic", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
@@ -335,20 +336,22 @@ def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = Fal
builder.button(
text=UNFREEZE,
callback_data=AdminUserEditorCallback(
- action="users_unfreeze", data=email, tg_id=key_details["tg_id"]
+ action="users_unfreeze", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
else:
builder.button(
text=FREEZE,
- callback_data=AdminUserEditorCallback(action="users_freeze", data=email, tg_id=key_details["tg_id"]).pack(),
+ callback_data=AdminUserEditorCallback(
+ action="users_freeze", data=resolved_key_ref, 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"]
+ action="users_hwid_menu", data=resolved_key_ref, tg_id=key_details["tg_id"]
).pack(),
)
@@ -357,33 +360,33 @@ def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = Fal
return builder.as_markup()
-def build_reissue_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup:
+def build_reissue_menu_kb(key_ref: str, tg_id: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="📦 Полный перевыпуск",
- callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_update_key", data=key_ref, tg_id=tg_id).pack(),
)
builder.button(
text="🔗 Сменить ссылку",
- callback_data=AdminUserEditorCallback(action="users_recreate_key", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_recreate_key", data=key_ref, tg_id=tg_id).pack(),
)
builder.button(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", data=key_ref, tg_id=tg_id).pack(),
)
builder.adjust(1)
return builder.as_markup()
-def build_hwid_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup:
+def build_hwid_menu_kb(key_ref: 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(),
+ callback_data=AdminUserEditorCallback(action="users_hwid_reset", data=key_ref, tg_id=tg_id).pack(),
)
builder.button(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", data=key_ref, tg_id=tg_id).pack(),
)
builder.adjust(1)
return builder.as_markup()
@@ -427,15 +430,15 @@ def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboar
)
-async def build_cluster_selection_kb(session, tg_id: int, email: str, action: str) -> InlineKeyboardMarkup:
+async def build_cluster_selection_kb(session, tg_id: int, key_ref: str, action: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
clusters = await get_clusters(session)
for cluster_id in clusters:
- builder.button(text=cluster_id, callback_data=f"{action}|{tg_id}|{email}|{cluster_id}")
+ builder.button(text=cluster_id, callback_data=f"{action}|{tg_id}|{key_ref}|{cluster_id}")
builder.button(
- text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack()
+ text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref).pack()
)
builder.adjust(1)
return builder.as_markup()
diff --git a/handlers/admin/users/users_audit.py b/handlers/admin/users/users_audit.py
index 78715503..2ab5c796 100644
--- a/handlers/admin/users/users_audit.py
+++ b/handlers/admin/users/users_audit.py
@@ -205,7 +205,6 @@ def _resolve_event_types(category_filter: str) -> list[str] | None:
return sorted(category_events)
-# Порядок и подписи блоков при показе «все» категории
CATEGORY_BLOCK_ORDER = ("auth", "subscriptions", "payments", "marketing", "other")
CATEGORY_BLOCK_LABELS = {
"auth": "Авторизация",
@@ -258,7 +257,6 @@ def _event_label(event_type: str) -> str:
return EVENT_TYPE_LABELS.get(event_type, event_type)
-# Отступ для строки события под статусом (чтобы не слипалось)
_FLOW_INDENT = " "
@@ -266,7 +264,6 @@ def _humanize_path(path: str) -> str:
"""Сокращает типичные callback для админки до читаемого вида."""
if not path or "callback:" not in path:
return path
- # callback:admin_users:users_audit:476217106:telegram | all | 0:0
if "users_audit:" in path:
rest = path.split("users_audit:", 1)[-1].strip()
parts = [p.strip() for p in rest.split("|")[:2] if p.strip()]
@@ -335,7 +332,6 @@ def _format_event_status(event) -> str:
return f"{created_at} {result_text}"
-# Разделитель между событиями — сразу видно границу "что где"
_FLOW_SEP = "—"
def _render_events_as_flow(events: list) -> list[str]:
@@ -421,7 +417,7 @@ async def _render_user_audit(
lines.append("\nСобытий пока нет.")
else:
lines.append("")
- rev = list(reversed(events)) # хронология сверху вниз
+ rev = list(reversed(events))
if full_flow:
by_cat: dict[str, list] = {}
for e in rev:
diff --git a/handlers/admin/users/users_hwid.py b/handlers/admin/users/users_hwid.py
index 5dac73e6..13e20a53 100644
--- a/handlers/admin/users/users_hwid.py
+++ b/handlers/admin/users/users_hwid.py
@@ -7,10 +7,10 @@ from panels.remnawave_runtime import (
resolve_remnawave_api_url,
with_remnawave_api,
)
-from database import get_client_id_by_email
from filters.admin import IsAdminFilter
from .keyboard import AdminUserEditorCallback, build_editor_kb, build_hwid_menu_kb
+from .utils import resolve_admin_key
router = Router()
@@ -25,13 +25,14 @@ async def handle_hwid_menu(
callback_data: AdminUserEditorCallback,
session: AsyncSession,
):
- email = callback_data.data
+ key_ref = str(callback_data.data)
tg_id = callback_data.tg_id
- client_id = await get_client_id_by_email(session, email)
- if not client_id:
- await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.")
+ key_obj = await resolve_admin_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Не удалось найти ключ.", reply_markup=build_editor_kb(tg_id))
return
+ client_id = key_obj.client_id
remna_api_url = await resolve_remnawave_api_url(session, "", fallback_any=True)
if not remna_api_url:
@@ -109,7 +110,7 @@ async def handle_hwid_menu(
f"└ 🔄 Обновлено: {updated}\n\n"
)
- await callback_query.message.edit_text(text, reply_markup=build_hwid_menu_kb(email, tg_id))
+ await callback_query.message.edit_text(text, reply_markup=build_hwid_menu_kb(key_ref, tg_id))
@router.callback_query(
@@ -121,13 +122,14 @@ async def handle_hwid_reset(
callback_data: AdminUserEditorCallback,
session: AsyncSession,
):
- email = callback_data.data
+ key_ref = str(callback_data.data)
tg_id = callback_data.tg_id
- client_id = await get_client_id_by_email(session, email)
- if not client_id:
- await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.")
+ key_obj = await resolve_admin_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Не удалось найти ключ.", reply_markup=build_editor_kb(tg_id))
return
+ client_id = key_obj.client_id
remna_api_url = await resolve_remnawave_api_url(session, "", fallback_any=True)
if not remna_api_url:
diff --git a/handlers/admin/users/users_keys.py b/handlers/admin/users/users_keys.py
index 82d5b562..f0571725 100644
--- a/handlers/admin/users/users_keys.py
+++ b/handlers/admin/users/users_keys.py
@@ -8,26 +8,33 @@ from handlers.buttons import BACK
import pytz
from aiogram import F, Router, types
+from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, REMNAWAVE_TOKEN_LOGIN_ENABLED, USE_COUNTRY_SELECTION
from core.bootstrap import MODES_CONFIG
from database import (
+ check_server_name_by_cluster,
delete_key,
delete_user_data,
+ get_active_tariffs_by_group_code,
+ get_key_by_email,
get_key_details,
+ get_keys,
+ get_server_names,
get_servers,
get_tariff_by_id,
get_tariffs_for_cluster,
mark_key_as_frozen,
mark_key_as_unfrozen,
+ save_admin_key_config,
+ update_key_subscription_links,
update_key_expiry,
)
-from database.models import Key, Server, Tariff
+from database.models import Key
from filters.admin import IsAdminFilter
from middlewares.session import release_session_early
from handlers.keys.operations import (
@@ -58,6 +65,7 @@ from .keyboard import (
build_users_key_expiry_kb,
build_users_key_show_kb,
)
+from .utils import resolve_admin_key
from .users_states import RenewTariffState, UserEditorState
@@ -66,6 +74,14 @@ MOSCOW_TZ = pytz.timezone("Europe/Moscow")
router = Router()
+async def _resolve_callback_key(
+ session: AsyncSession,
+ tg_id: int,
+ key_ref: str | int | None,
+) -> Key | None:
+ return await resolve_admin_key(session, tg_id, key_ref)
+
+
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_key_edit"),
IsAdminFilter(),
@@ -76,9 +92,8 @@ async def handle_key_edit(
session: AsyncSession,
update: bool = False,
):
- email = callback_data.data
- result = await session.execute(select(Key).where(Key.email == email))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_ref = callback_data.data
+ key_obj = await _resolve_callback_key(session, callback_data.tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
@@ -87,6 +102,10 @@ async def handle_key_edit(
)
return
+ email = key_obj.email
+ key_details = await get_key_details(session, email)
+ is_frozen = bool(key_details.get("is_frozen")) if key_details else bool(getattr(key_obj, "is_frozen", False))
+
key_value = key_obj.key or key_obj.remnawave_link or "—"
alias_part = f" ({key_obj.alias})" if key_obj.alias else ""
@@ -96,10 +115,26 @@ async def handle_key_edit(
else:
created_at = "—"
- if key_obj.expiry_time:
+ if is_frozen:
+ frozen_left_ms = int((key_details or {}).get("expiry_time") or 0)
+ total_minutes = max(frozen_left_ms // 60000, 0)
+ days, rem_minutes = divmod(total_minutes, 24 * 60)
+ hours, minutes = divmod(rem_minutes, 60)
+ frozen_parts: list[str] = []
+ if days:
+ frozen_parts.append(f"{days} дн.")
+ if hours:
+ frozen_parts.append(f"{hours} ч.")
+ if minutes or not frozen_parts:
+ frozen_parts.append(f"{minutes} мин.")
+ expiry_label = "⏳ Остаток:"
+ expiry_date = " ".join(frozen_parts)
+ elif key_obj.expiry_time:
expiry_dt = datetime.fromtimestamp(int(key_obj.expiry_time) / 1000, tz=MOSCOW_TZ)
+ expiry_label = "⏰ Истекает:"
expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M")
else:
+ expiry_label = "⏰ Истекает:"
expiry_date = "—"
tariff_name = "—"
@@ -109,24 +144,14 @@ async def handle_key_edit(
base_traffic = None
is_configurable = False
if key_obj.tariff_id:
- result = await session.execute(
- select(
- Tariff.name,
- Tariff.subgroup_title,
- Tariff.group_code,
- Tariff.device_limit,
- Tariff.traffic_limit,
- Tariff.configurable,
- ).where(Tariff.id == key_obj.tariff_id)
- )
- row = result.first()
- if row:
- tariff_name = row[0]
- subgroup_title = row[1] or "—"
- group_code = row[2] or "—"
- base_devices = row[3]
- base_traffic = row[4]
- is_configurable = bool(row[5])
+ tariff = await get_tariff_by_id(session, key_obj.tariff_id)
+ if tariff:
+ tariff_name = tariff.get("name", "—")
+ subgroup_title = tariff.get("subgroup_title") or "—"
+ group_code = tariff.get("group_code") or "—"
+ base_devices = tariff.get("device_limit")
+ base_traffic = tariff.get("traffic_limit")
+ is_configurable = bool(tariff.get("configurable"))
devices_line = ""
traffic_line = ""
@@ -156,7 +181,8 @@ async def handle_key_edit(
"
"
f"🔗 Ключ{alias_part}: {key_value}\n"
f"📆 Создан: {created_at} (МСК)\n"
- f"⏰ Истекает: {expiry_date} (МСК)\n"
+ f"{'⛔ Статус: отключена\n' if is_frozen else ''}"
+ f"{expiry_label} {expiry_date}{' (МСК)' if not is_frozen and expiry_date != '—' else ''}\n"
f"🌐 Кластер: {key_obj.server_id or '—'}\n"
f"🆔 ID клиента: {key_obj.tg_id or '—'}\n"
f"🏷️ Тарифная группа: {group_code}\n"
@@ -168,7 +194,9 @@ async def handle_key_edit(
)
if not update or not getattr(callback_data, "edit", False):
- kb_markup = build_key_edit_kb(key_obj.__dict__, email, is_configurable=is_configurable)
+ kb_key_details = dict(key_obj.__dict__)
+ kb_key_details["is_frozen"] = is_frozen
+ kb_markup = build_key_edit_kb(kb_key_details, email, is_configurable=is_configurable, key_ref=str(key_ref))
kb_builder = InlineKeyboardBuilder.from_markup(kb_markup)
hook_buttons = await process_admin_key_edit_menu(
email=email,
@@ -177,15 +205,28 @@ async def handle_key_edit(
tg_id=key_obj.tg_id,
)
kb_builder = insert_hook_buttons(kb_builder, hook_buttons)
- await callback_query.message.edit_text(
- text=text,
- reply_markup=kb_builder.as_markup(),
- )
+ try:
+ await callback_query.message.edit_text(
+ text=text,
+ reply_markup=kb_builder.as_markup(),
+ )
+ except TelegramBadRequest as e:
+ if "message is not modified" not in str(e):
+ raise
else:
- await callback_query.message.edit_text(
- text=text,
- reply_markup=await build_users_key_expiry_kb(session, callback_data.tg_id, email),
- )
+ try:
+ await callback_query.message.edit_text(
+ text=text,
+ reply_markup=await build_users_key_expiry_kb(
+ session,
+ callback_data.tg_id,
+ email,
+ key_ref=str(key_ref),
+ ),
+ )
+ except TelegramBadRequest as e:
+ if "message is not modified" not in str(e):
+ raise
@router.callback_query(
@@ -198,9 +239,19 @@ async def handle_change_expiry(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
- await callback_query.message.edit_reply_markup(reply_markup=await build_users_key_expiry_kb(session, tg_id, email))
+ await callback_query.message.edit_reply_markup(
+ reply_markup=await build_users_key_expiry_kb(session, tg_id, email, key_ref=key_ref)
+ )
@router.callback_query(
@@ -214,7 +265,15 @@ async def handle_expiry_add(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
days = callback_data.month
key_details = await get_key_details(session, email)
@@ -231,12 +290,12 @@ async def handle_expiry_add(
await handle_key_edit(callback_query, callback_data, session, True)
return
- await state.update_data(tg_id=tg_id, email=email, op_type="add")
+ await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="add")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
- reply_markup=build_users_key_show_kb(tg_id, email),
+ reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@@ -248,16 +307,25 @@ async def handle_expiry_take(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
+ session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
- await state.update_data(tg_id=tg_id, email=email, op_type="take")
+ await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="take")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:",
- reply_markup=build_users_key_show_kb(tg_id, email),
+ reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@@ -272,7 +340,15 @@ async def handle_expiry_set(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
key_details = await get_key_details(session, email)
@@ -283,7 +359,7 @@ async def handle_expiry_set(
)
return
- await state.update_data(tg_id=tg_id, email=email, op_type="set")
+ await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="set")
await state.set_state(UserEditorState.waiting_for_expiry_time)
text = (
@@ -294,7 +370,7 @@ async def handle_expiry_set(
await callback_query.message.edit_text(
text=text,
- reply_markup=build_users_key_show_kb(tg_id, email),
+ reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@@ -303,12 +379,13 @@ async def handle_expiry_time_input(message: Message, state: FSMContext, session:
data = await state.get_data()
tg_id = data.get("tg_id")
email = data.get("email")
+ key_ref = data.get("key_ref")
op_type = data.get("op_type")
if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0):
await message.answer(
text="🚫 Пожалуйста, введите корректное количество дней!",
- reply_markup=build_users_key_show_kb(tg_id, email),
+ reply_markup=build_users_key_show_kb(tg_id, key_ref) if key_ref else build_editor_kb(tg_id),
)
return
@@ -347,7 +424,10 @@ async def handle_expiry_time_input(message: Message, state: FSMContext, session:
except Exception as e:
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
- await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
+ await message.answer(
+ text=text,
+ reply_markup=build_users_key_show_kb(tg_id, key_ref) if key_ref else build_editor_kb(tg_id),
+ )
@router.callback_query(
@@ -357,9 +437,14 @@ async def handle_expiry_time_input(message: Message, state: FSMContext, session:
async def handle_reissue_menu(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
+ session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
text = (
"🔄 Перевыпуск подписки\n\n"
@@ -373,7 +458,7 @@ async def handle_reissue_menu(
await callback_query.message.edit_text(
text=text,
- reply_markup=build_reissue_menu_kb(email, tg_id),
+ reply_markup=build_reissue_menu_kb(key_ref, tg_id),
)
@@ -387,14 +472,19 @@ async def handle_update_key(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
+ email = key_obj.email
await callback_query.message.edit_text(
text=f"📡 Выберите кластер, на котором пересоздать ключ {email}:",
reply_markup=await build_cluster_selection_kb(
session,
tg_id,
- email,
+ key_ref,
action="confirm_admin_key_reissue",
),
)
@@ -402,8 +492,13 @@ async def handle_update_key(
@router.callback_query(F.data.startswith("confirm_admin_key_reissue|"), IsAdminFilter())
async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: AsyncSession, state: FSMContext):
- _, tg_id, email, cluster_id = callback_query.data.split("|")
+ _, tg_id, key_ref, cluster_id = callback_query.data.split("|")
tg_id = int(tg_id)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
+ email = key_obj.email
try:
servers = await get_servers(session)
@@ -424,7 +519,7 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
- data=email,
+ data=key_ref,
).pack(),
)
)
@@ -442,17 +537,21 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn
if use_country_selection:
unique_countries = {srv["server_name"] for srv in cluster_servers}
- await state.update_data(tg_id=tg_id, email=email, cluster_id=cluster_id)
+ await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, cluster_id=cluster_id)
builder = InlineKeyboardBuilder()
for country in sorted(unique_countries):
builder.button(
text=country,
- callback_data=f"admin_reissue_country|{tg_id}|{email}|{country}",
+ callback_data=f"admin_reissue_country|{tg_id}|{key_ref}|{country}",
)
builder.row(
InlineKeyboardButton(
text=BACK,
- callback_data=f"users_key_edit|{email}",
+ callback_data=AdminUserEditorCallback(
+ action="users_key_edit",
+ tg_id=tg_id,
+ data=key_ref,
+ ).pack(),
)
)
await callback_query.message.edit_text(
@@ -461,8 +560,8 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn
)
return
- result = await session.execute(select(Key.remnawave_link).where(Key.email == email))
- remnawave_link = result.scalar_one_or_none()
+ key_link = await get_key_by_email(session, email)
+ remnawave_link = key_link.remnawave_link if key_link else None
await update_subscription(
tg_id,
@@ -474,7 +573,7 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn
await handle_key_edit(
callback_query,
- AdminUserEditorCallback(tg_id=tg_id, data=email, action="view_key"),
+ AdminUserEditorCallback(tg_id=tg_id, data=key_ref, action="view_key"),
session,
True,
)
@@ -485,8 +584,13 @@ async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Asyn
@router.callback_query(F.data.startswith("admin_reissue_country|"), IsAdminFilter())
async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSession, state: FSMContext):
- _, tg_id, email, country = callback_query.data.split("|")
+ _, tg_id, key_ref, country = callback_query.data.split("|")
tg_id = int(tg_id)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
+ if not key_obj:
+ await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
+ email = key_obj.email
try:
data = await state.get_data()
@@ -508,7 +612,7 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
- data=email,
+ data=key_ref,
).pack(),
)
)
@@ -522,8 +626,8 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
)
return
- result = await session.execute(select(Key.remnawave_link, Key.tariff_id).where(Key.email == email))
- remnawave_link, _tariff_id = result.one_or_none() or (None, None)
+ key_link = await get_key_by_email(session, email)
+ remnawave_link = key_link.remnawave_link if key_link else None
await update_subscription(
tg_id=tg_id,
@@ -535,7 +639,7 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
await handle_key_edit(
callback_query,
- AdminUserEditorCallback(tg_id=tg_id, data=email, action="view_key"),
+ AdminUserEditorCallback(tg_id=tg_id, data=key_ref, action="view_key"),
session,
True,
)
@@ -554,10 +658,8 @@ async def handle_recreate_key_start(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
-
- result = await session.execute(select(Key).where(Key.email == email))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_ref = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
@@ -566,6 +668,8 @@ async def handle_recreate_key_start(
)
return
+ email = key_obj.email
+
tariff_name = "—"
if key_obj.tariff_id:
tariff = await get_tariff_by_id(session, key_obj.tariff_id)
@@ -584,13 +688,13 @@ async def handle_recreate_key_start(
builder.row(
InlineKeyboardButton(
text="✅ Пересоздать",
- callback_data=f"confirm_recreate|{tg_id}|{email}",
+ callback_data=f"confirm_recreate|{tg_id}|{key_ref}",
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref).pack(),
)
)
@@ -602,12 +706,11 @@ async def handle_recreate_key_confirm(
callback_query: CallbackQuery,
session: AsyncSession,
):
- _, tg_id, old_email = callback_query.data.split("|")
+ _, tg_id, key_ref = callback_query.data.split("|")
tg_id = int(tg_id)
try:
- result = await session.execute(select(Key).where(Key.email == old_email))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
@@ -616,6 +719,8 @@ async def handle_recreate_key_confirm(
)
return
+ old_email = key_obj.email
+
await callback_query.message.edit_text("⏳ Пересоздание ссылки подписки...")
client_id = key_obj.client_id
@@ -683,15 +788,7 @@ async def handle_recreate_key_confirm(
)
return
- await session.execute(
- update(Key)
- .where(Key.email == old_email)
- .values(
- key=new_link,
- remnawave_link=new_link,
- )
- )
- await session.commit()
+ await update_key_subscription_links(session, old_email, new_link)
try:
user_text = (
@@ -740,7 +837,7 @@ async def handle_recreate_key_confirm(
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
- data=old_email,
+ data=key_ref,
).pack(),
)
)
@@ -768,10 +865,16 @@ async def handle_delete_key(
state: FSMContext,
session: AsyncSession,
):
- email = callback_data.data
+ key_obj = await _resolve_callback_key(session, callback_data.tg_id, callback_data.data)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Ключ не найден!",
+ reply_markup=build_editor_kb(callback_data.tg_id),
+ )
+ return
- result = await session.execute(select(Key.client_id).where(Key.email == email))
- client_id = result.scalar_one_or_none()
+ email = key_obj.email
+ client_id = key_obj.client_id
if client_id is None:
await callback_query.message.edit_text(
@@ -809,8 +912,7 @@ async def handle_delete_key_confirm(
await callback_query.answer("Данные устарели", show_alert=True)
return
- result = await session.execute(select(Key.client_id).where(Key.email == email))
- client_id = result.scalar_one_or_none()
+ client_id = key_obj.client_id
kb = build_editor_kb(callback_data.tg_id)
@@ -862,8 +964,7 @@ async def handle_delete_user_confirm(
):
tg_id = callback_data.tg_id
- result = await session.execute(select(Key.email, Key.client_id).where(Key.tg_id == tg_id))
- key_records = result.all()
+ key_records = [(row.email, row.client_id) for row in await get_keys(session, tg_id)]
await release_session_early(session)
async def delete_keys_from_servers():
@@ -903,7 +1004,11 @@ async def handle_user_traffic(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
+ key_obj = await _resolve_callback_key(session, tg_id, callback_data.data)
+ if not key_obj:
+ await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
+ email = key_obj.email
await callback_query.message.edit_text("⏳ Получаем данные о трафике, пожалуйста, подождите...")
@@ -952,9 +1057,7 @@ async def handle_create_key_start(
if use_country_selection:
await state.set_state(UserEditorState.selecting_country)
- stmt = select(Server.server_name).distinct().order_by(Server.server_name)
- result = await session.execute(stmt)
- countries = [row[0] for row in result.all()]
+ countries = await get_server_names(session)
if not countries:
await callback_query.message.edit_text(
@@ -1007,14 +1110,13 @@ async def handle_create_key_country(callback_query: CallbackQuery, state: FSMCon
builder = InlineKeyboardBuilder()
- result = await session.execute(select(Server.cluster_name).where(Server.server_name == country))
- row = result.mappings().first()
+ cluster_info = await check_server_name_by_cluster(session, country)
- if not row:
+ if not cluster_info:
await callback_query.message.edit_text("❌ Сервер не найден.")
return
- cluster_name = row["cluster_name"]
+ cluster_name = cluster_info["cluster_name"]
await state.update_data(cluster_name=cluster_name)
tariffs = await get_tariffs_for_cluster(session, cluster_name)
@@ -1146,20 +1248,16 @@ async def handle_reset_traffic(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = callback_data.data
-
- stmt = select(Key.server_id, Key.client_id).where((Key.tg_id == tg_id) & (Key.email == email))
- result = await session.execute(stmt)
- record = result.first()
-
- if not record:
+ key_obj = await _resolve_callback_key(session, tg_id, callback_data.data)
+ if not key_obj:
await callback_query.message.edit_text(
"❌ Ключ не найден в базе данных.",
reply_markup=build_editor_kb(tg_id),
)
return
- cluster_id, _client_id = record
+ email = key_obj.email
+ cluster_id = key_obj.server_id
try:
await reset_traffic_in_cluster(cluster_id, email, session)
@@ -1185,7 +1283,14 @@ async def handle_admin_freeze_subscription(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, callback_data.data)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
try:
record = await get_key_details(session, email)
@@ -1202,7 +1307,7 @@ async def handle_admin_freeze_subscription(
result = await toggle_client_on_cluster(cluster_id, email, client_id, enable=False, session=session)
if result["status"] != "success":
text_error = (
- f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
+ f"Произошла ошибка при отключении подписки.\nДетали: {result.get('error') or result.get('results')}"
)
await callback_query.message.edit_text(
text_error,
@@ -1217,8 +1322,9 @@ async def handle_admin_freeze_subscription(
await mark_key_as_frozen(session, record["tg_id"], client_id, time_left)
await session.commit()
+ session.expire_all()
- await callback_query.answer("✅ Подписка заморожена")
+ await callback_query.answer("✅ Подписка отключена")
await handle_key_edit(
callback_query=callback_query,
@@ -1227,7 +1333,7 @@ async def handle_admin_freeze_subscription(
update=False,
)
except Exception as e:
- await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
+ await handle_error(tg_id, callback_query, f"Ошибка при отключении подписки: {e}")
@router.callback_query(
@@ -1240,7 +1346,14 @@ async def handle_admin_unfreeze_subscription(
session: AsyncSession,
):
tg_id = callback_data.tg_id
- email = str(callback_data.data)
+ key_obj = await _resolve_callback_key(session, tg_id, callback_data.data)
+ if not key_obj:
+ await callback_query.message.edit_text(
+ text="🚫 Информация о ключе не найдена.",
+ reply_markup=build_editor_kb(tg_id),
+ )
+ return
+ email = key_obj.email
try:
record = await get_key_details(session, email)
@@ -1286,6 +1399,7 @@ async def handle_admin_unfreeze_subscription(
await mark_key_as_unfrozen(session, record["tg_id"], client_id, new_expiry_time)
await session.commit()
+ session.expire_all()
await release_session_early(session)
await renew_key_in_cluster(
@@ -1300,7 +1414,7 @@ async def handle_admin_unfreeze_subscription(
plan=record.get("tariff_id"),
)
- await callback_query.answer("✅ Подписка разморожена")
+ await callback_query.answer("✅ Подписка включена")
await handle_key_edit(
callback_query=callback_query,
@@ -1313,20 +1427,15 @@ async def handle_admin_unfreeze_subscription(
async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession) -> Exception | None:
- result = await session.execute(
- select(
- Key.client_id,
- Key.tariff_id,
- Key.server_id,
- Key.current_device_limit,
- Key.current_traffic_limit,
- ).where(Key.email == email)
- )
- row = result.first()
- if not row:
+ key_obj = await get_key_by_email(session, email)
+ if not key_obj:
return ValueError(f"User with email {email} was not found")
- client_id, tariff_id, server_id, key_device_limit, key_traffic_limit = row
+ client_id = key_obj.client_id
+ tariff_id = key_obj.tariff_id
+ server_id = key_obj.server_id
+ key_device_limit = key_obj.current_device_limit
+ key_traffic_limit = key_obj.current_traffic_limit
if server_id is None:
return ValueError(f"Key with client_id {client_id} was not found")
@@ -1334,17 +1443,12 @@ async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession
device_limit = None
key_subgroup = None
if tariff_id:
- result = await session.execute(
- select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where(
- Tariff.id == tariff_id,
- Tariff.is_active.is_(True),
- )
- )
- tariff = result.first()
+ tariff = await get_tariff_by_id(session, tariff_id)
if tariff:
- traffic_limit = int(tariff[0]) if tariff[0] is not None else 0
- device_limit = int(tariff[1]) if tariff[1] is not None else 0
- key_subgroup = tariff[2]
+ traffic_limit = int(tariff.get("traffic_limit") or 0)
+ raw_device_limit = tariff.get("device_limit")
+ device_limit = int(raw_device_limit) if raw_device_limit is not None else 0
+ key_subgroup = tariff.get("subgroup_title")
if key_device_limit is not None:
device_limit = key_device_limit
@@ -1395,16 +1499,17 @@ async def handle_edit_config_start(
state: FSMContext,
session: AsyncSession,
):
- email = callback_data.data
+ key_ref = str(callback_data.data)
tg_id = callback_data.tg_id
- result = await session.execute(select(Key).where(Key.email == email))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_obj = await _resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
return
+ email = key_obj.email
+
if not key_obj.tariff_id:
await callback_query.message.edit_text(
"❌ У ключа не назначен тариф.",
@@ -1431,6 +1536,7 @@ async def handle_edit_config_start(
await state.set_state(UserEditorState.config_menu)
await state.update_data(
email=email,
+ key_ref=key_ref,
tg_id=tg_id,
tariff_id=key_obj.tariff_id,
cfg_base_devices=base_devices,
@@ -1445,6 +1551,7 @@ async def handle_edit_config_start(
async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
data = await state.get_data()
email = data.get("email")
+ key_ref = data.get("key_ref")
tg_id = data.get("tg_id")
tariff_id = data.get("tariff_id")
@@ -1461,8 +1568,7 @@ async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, s
traffic_to_show = base_traffic
if traffic_to_show is None and email:
- result = await session.execute(select(Key).where(Key.email == email))
- key_obj = result.scalar_one_or_none()
+ key_obj = await get_key_by_email(session, email)
if key_obj:
traffic_to_show = key_obj.selected_traffic_limit or key_obj.current_traffic_limit
if traffic_to_show is None and tariff:
@@ -1501,7 +1607,7 @@ async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, s
builder.row(
InlineKeyboardButton(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", data=key_ref, tg_id=tg_id).pack(),
)
)
@@ -1668,6 +1774,7 @@ async def handle_cfg_input_addon(message: Message, state: FSMContext, session: A
data = await state.get_data()
param = data.get("cfg_param")
email = data.get("email")
+ key_ref = data.get("key_ref")
tg_id = data.get("tg_id")
tariff_id = data.get("tariff_id")
@@ -1721,7 +1828,7 @@ async def handle_cfg_input_addon(message: Message, state: FSMContext, session: A
builder.row(
InlineKeyboardButton(
text=BACK,
- callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
+ callback_data=AdminUserEditorCallback(action="users_key_edit", data=key_ref, tg_id=tg_id).pack(),
)
)
@@ -1760,8 +1867,7 @@ async def handle_cfg_save(callback_query: CallbackQuery, state: FSMContext, sess
selected_price = base_price + devices_extra_price + traffic_extra_price
- result = await session.execute(select(Key).where(Key.email == email))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_obj = await get_key_by_email(session, email)
if not key_obj:
await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
@@ -1782,18 +1888,15 @@ async def handle_cfg_save(callback_query: CallbackQuery, state: FSMContext, sess
plan=tariff_id,
)
- await session.execute(
- update(Key)
- .where(Key.email == email)
- .values(
- selected_device_limit=base_devices,
- current_device_limit=total_devices,
- selected_traffic_limit=base_traffic,
- current_traffic_limit=total_traffic,
- selected_price_rub=selected_price,
- )
+ await save_admin_key_config(
+ session,
+ email=email,
+ base_devices=base_devices,
+ total_devices=total_devices,
+ base_traffic=base_traffic,
+ total_traffic=total_traffic,
+ selected_price=selected_price,
)
- await session.commit()
await state.clear()
await callback_query.answer("✅ Конфигурация сохранена", show_alert=True)
diff --git a/handlers/admin/users/users_tariffs.py b/handlers/admin/users/users_tariffs.py
index 565868cf..2ef72c45 100644
--- a/handlers/admin/users/users_tariffs.py
+++ b/handlers/admin/users/users_tariffs.py
@@ -5,19 +5,26 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from handlers.buttons import BACK
from core.settings.tariffs_config import normalize_tariff_config
-from database import get_tariff_by_id
-from database.models import Key, Tariff
+from database import (
+ get_active_tariffs_by_group_code,
+ get_key_by_email,
+ get_tariff_by_id,
+ get_tariff_group_codes,
+ reset_key_tariff_state,
+ save_key_tariff_selection,
+)
+from database.models import Tariff
from filters.admin import IsAdminFilter
from middlewares.session import release_session_early
from handlers.keys.operations import renew_key_in_cluster
from logger import logger
-from .keyboard import AdminUserEditorCallback
+from .keyboard import AdminUserEditorCallback, build_editor_kb
+from .utils import resolve_admin_key
from .users_states import RenewTariffState
from .users_keys import handle_key_edit
@@ -60,14 +67,17 @@ async def handle_user_choose_tariff_group(
session: AsyncSession,
state: FSMContext,
):
- email = callback_data.data
tg_id = callback_data.tg_id
+ key_obj = await resolve_admin_key(session, tg_id, callback_data.data)
+ if not key_obj:
+ await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
+ return
+ email = key_obj.email
await state.set_state(RenewTariffState.selecting_group)
await state.update_data(email=email, tg_id=tg_id)
- result = await session.execute(select(Tariff.group_code).distinct())
- groups = [row[0] for row in result.fetchall()]
+ groups = await get_tariff_group_codes(session)
builder = InlineKeyboardBuilder()
for group_code in groups:
@@ -91,10 +101,7 @@ async def handle_user_choose_tariff(
await state.update_data(group_code=group_code)
await state.set_state(RenewTariffState.selecting_tariff)
- result = await session.execute(
- select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True)).order_by(Tariff.id)
- )
- tariffs = result.scalars().all()
+ tariffs = await get_active_tariffs_by_group_code(session, group_code)
if not tariffs:
await callback_query.message.edit_text("❌ Нет активных тарифов в группе.")
@@ -134,8 +141,7 @@ async def handle_user_renew_confirm(
await state.clear()
return
- result = await session.execute(select(Key).where(Key.email == email, Key.tg_id == tg_id))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_obj = await get_key_by_email(session, email, tg_id)
if not key_obj:
await callback_query.message.edit_text("❌ Ключ не найден.")
await state.clear()
@@ -311,29 +317,15 @@ async def handle_user_renew_confirm(
old_tariff_id = key_obj.tariff_id
old_subgroup = None
if old_tariff_id:
- old_subgroup = (
- await session.execute(select(Tariff.subgroup_title).where(Tariff.id == old_tariff_id))
- ).scalar_one_or_none()
+ old_tariff = await get_tariff_by_id(session, old_tariff_id)
+ old_subgroup = old_tariff.get("subgroup_title") if old_tariff else None
- new_subgroup = (
- await session.execute(select(Tariff.subgroup_title).where(Tariff.id == tariff_id))
- ).scalar_one_or_none()
+ new_tariff = await get_tariff_by_id(session, tariff_id)
+ new_subgroup = new_tariff.get("subgroup_title") if new_tariff else None
new_expiry_time = int(key_obj.expiry_time or 0) or int(datetime.utcnow().timestamp() * 1000)
- await session.execute(
- update(Key)
- .where(Key.tg_id == tg_id, Key.email == email)
- .values(
- tariff_id=tariff_id,
- selected_device_limit=None,
- current_device_limit=None,
- selected_traffic_limit=None,
- current_traffic_limit=None,
- selected_price_rub=None,
- )
- )
- await session.commit()
+ await reset_key_tariff_state(session, tg_id, email, tariff_id)
await release_session_early(session)
try:
@@ -629,8 +621,7 @@ async def handle_cfg_renew_apply(callback_query: CallbackQuery, session: AsyncSe
await state.clear()
return
- result = await session.execute(select(Key).where(Key.email == email, Key.tg_id == tg_id))
- key_obj: Key | None = result.scalar_one_or_none()
+ key_obj = await get_key_by_email(session, email, tg_id)
if not key_obj:
await callback_query.message.edit_text("❌ Ключ не найден.")
await state.clear()
@@ -639,33 +630,15 @@ async def handle_cfg_renew_apply(callback_query: CallbackQuery, session: AsyncSe
old_tariff_id = key_obj.tariff_id
old_subgroup = None
if old_tariff_id:
- old_subgroup = (
- await session.execute(select(Tariff.subgroup_title).where(Tariff.id == old_tariff_id))
- ).scalar_one_or_none()
+ old_tariff = await get_tariff_by_id(session, old_tariff_id)
+ old_subgroup = old_tariff.get("subgroup_title") if old_tariff else None
- new_subgroup = (
- await session.execute(select(Tariff.subgroup_title).where(Tariff.id == tariff_id))
- ).scalar_one_or_none()
+ new_tariff = await get_tariff_by_id(session, tariff_id)
+ new_subgroup = new_tariff.get("subgroup_title") if new_tariff else None
new_expiry_time = int(key_obj.expiry_time or 0) or int(datetime.utcnow().timestamp() * 1000)
- await session.execute(
- update(Key)
- .where(Key.tg_id == tg_id, Key.email == email)
- .values(
- tariff_id=tariff_id,
- selected_device_limit=int(selected_devices) if selected_devices is not None else None,
- current_device_limit=int(selected_devices) if selected_devices is not None else None,
- selected_traffic_limit=int(selected_traffic_gb)
- if (selected_traffic_gb is not None and int(selected_traffic_gb) > 0)
- else None,
- current_traffic_limit=int(selected_traffic_gb)
- if (selected_traffic_gb is not None and int(selected_traffic_gb) > 0)
- else None,
- selected_price_rub=None,
- )
- )
- await session.commit()
+ await save_key_tariff_selection(session, tg_id, email, tariff_id, selected_devices, selected_traffic_gb)
await release_session_early(session)
try:
@@ -706,8 +679,7 @@ async def handle_back_to_group(
state: FSMContext,
session: AsyncSession,
):
- result = await session.execute(select(Tariff.group_code).distinct())
- groups = [row[0] for row in result.fetchall()]
+ groups = await get_tariff_group_codes(session)
builder = InlineKeyboardBuilder()
for group_code in groups:
diff --git a/handlers/admin/users/utils.py b/handlers/admin/users/utils.py
new file mode 100644
index 00000000..a4ee2550
--- /dev/null
+++ b/handlers/admin/users/utils.py
@@ -0,0 +1,12 @@
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from handlers.keys.utils import build_key_ref, resolve_key
+from database.models import Key
+
+
+def build_admin_key_ref(client_id: str | None, email: str | None = None) -> str:
+ return build_key_ref(client_id, email)
+
+
+async def resolve_admin_key(session: AsyncSession, tg_id: int, key_ref: str | int | None) -> Key | None:
+ return await resolve_key(session, tg_id, key_ref)
diff --git a/handlers/buttons.py b/handlers/buttons.py
index 0e36a2ca..0e1e98fe 100644
--- a/handlers/buttons.py
+++ b/handlers/buttons.py
@@ -1,10 +1,4 @@
BUTTON_ICON_CONFIG: dict[str, dict[str, str]] = {
- # "partner": {"icon_custom_emoji_id": "5310169226856644648", "style": "primary"},
- # Примеры по callback_data:
- # "profile": {"icon_custom_emoji_id": "5310169226856644648", "style": "primary"}, # синяя
- # "view_keys": {"icon_custom_emoji_id": "5310169226856644648", "style": "success"}, # зеленая
- # "pay": {"style": "primary"},
- # "cancel_broadcast": {"style": "danger"}, # красная
}
# Общие кнопки
@@ -45,8 +39,8 @@ HWID_BUTTON = "♻️ Сбросить привязку"
QR = "📷 Показать QR-код"
DELETE = "❌ Удалить"
CHANGE_LOCATION = "🌍 Сменить локацию"
-FREEZE = "🛑 Заморозить подписку"
-UNFREEZE = "🟢 Разморозить подписку"
+FREEZE = "Отключить подписку"
+UNFREEZE = "Включить подписку"
ALIAS = "✏️"
# Реферальная система
diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py
index e3a35665..f9f83cf4 100644
--- a/handlers/instructions/instructions.py
+++ b/handlers/instructions/instructions.py
@@ -42,7 +42,7 @@ from handlers.texts import (
ROUTER_MESSAGE,
SUBSCRIPTION_DETAILS_TEXT,
)
-from handlers.keys.utils import key_owned_by_user
+from handlers.keys.utils import build_key_callback, key_owned_by_user, resolve_key
from handlers.utils import edit_or_send_message, is_full_remnawave_cluster
from hooks.processors import process_remnawave_webapp_override
@@ -75,7 +75,9 @@ async def send_instructions(callback_query_or_message: CallbackQuery | Message):
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
@@ -93,9 +95,9 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any):
return
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=PC_PC, callback_data=f"windows_menu|{key_name}"))
- builder.row(InlineKeyboardButton(text=PC_MACOS, callback_data=f"macos_menu|{key_name}"))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{key_name}"))
+ builder.row(InlineKeyboardButton(text=PC_PC, callback_data=build_key_callback("windows_menu", record.get("client_id"), key_name)))
+ builder.row(InlineKeyboardButton(text=PC_MACOS, callback_data=build_key_callback("macos_menu", record.get("client_id"), key_name)))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_device", record.get("client_id"), key_name)))
await edit_or_send_message(
target_message=callback_query.message,
@@ -107,7 +109,9 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data.startswith("windows_menu|"))
async def process_windows_menu(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
@@ -132,7 +136,7 @@ async def process_windows_menu(callback_query: CallbackQuery, session: Any):
builder.row(InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=windows_url))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_pc", record.get("client_id"), key_name)))
await edit_or_send_message(
target_message=callback_query.message,
@@ -144,7 +148,9 @@ async def process_windows_menu(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data.startswith("macos_menu|"))
async def process_macos_menu(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
@@ -169,7 +175,7 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any):
builder.row(InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=macos_url))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_pc", record.get("client_id"), key_name)))
await edit_or_send_message(
target_message=callback_query.message,
@@ -181,7 +187,9 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data.startswith("connect_tv|"))
async def process_connect_tv(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|", 1)[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
@@ -216,10 +224,14 @@ async def process_connect_tv(callback_query: CallbackQuery, session: Any):
await callback_query.answer("❌ Ссылка подписки не найдена", show_alert=True)
return
- back_callback = f"view_key|{key_name}" if is_remnawave_webapp else f"connect_device|{key_name}"
+ back_callback = (
+ build_key_callback("view_key", record.get("client_id"), key_name)
+ if is_remnawave_webapp
+ else build_key_callback("connect_device", record.get("client_id"), key_name)
+ )
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=TV_CONTINUE, callback_data=f"continue_tv|{key_name}"))
+ builder.row(InlineKeyboardButton(text=TV_CONTINUE, callback_data=build_key_callback("continue_tv", record.get("client_id"), key_name)))
builder.row(InlineKeyboardButton(text=BACK, callback_data=back_callback))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
@@ -236,7 +248,9 @@ async def process_connect_tv(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data.startswith("continue_tv|"))
async def process_continue_tv(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
@@ -245,7 +259,7 @@ async def process_continue_tv(callback_query: CallbackQuery, session: Any):
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=key_link)
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_tv|{key_name}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_tv", record.get("client_id"), key_name)))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -258,7 +272,9 @@ async def process_continue_tv(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data.startswith("connect_router|"))
async def process_connect_router(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
@@ -278,7 +294,7 @@ async def process_connect_router(callback_query: CallbackQuery, session: Any):
message_text = ROUTER_MESSAGE.format(subscription_link=key_link)
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("view_key", record.get("client_id"), key_name)))
await edit_or_send_message(
target_message=callback_query.message,
diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py
index 959efc08..766cb2f8 100644
--- a/handlers/keys/__init__.py
+++ b/handlers/keys/__init__.py
@@ -3,7 +3,6 @@ __all__ = ("router",)
from aiogram import Router
from .key_connect import router as connect_router
-from .key_freeze import router as freeze_router
from .key_mode import router as key_mode_router
from .key_renew import router as renew_router
from .key_view import router as view_router
@@ -16,7 +15,6 @@ router.include_routers(
keys_router,
view_router,
renew_router,
- freeze_router,
connect_router,
key_mode_router,
)
diff --git a/handlers/keys/key_connect.py b/handlers/keys/key_connect.py
index 90bc7d94..bbd53f75 100644
--- a/handlers/keys/key_connect.py
+++ b/handlers/keys/key_connect.py
@@ -8,7 +8,6 @@ import qrcode
from aiogram import F, Router, types
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import (
@@ -19,7 +18,7 @@ from config import (
DOWNLOAD_IOS,
INSTRUCTIONS_BUTTON,
)
-from database import Key, get_key_details, get_subscription_link
+from database import get_key_details, get_subscription_link
from handlers.buttons import (
ANDROID,
BACK,
@@ -39,7 +38,7 @@ from handlers.texts import (
IOS_DESCRIPTION_TEMPLATE,
SUBSCRIPTION_DESCRIPTION,
)
-from handlers.keys.utils import key_owned_by_user
+from handlers.keys.utils import build_key_callback, key_owned_by_user, resolve_key
from handlers.utils import edit_or_send_message
from hooks.hook_buttons import insert_hook_buttons
from hooks.processors import process_connect_device_menu
@@ -67,18 +66,23 @@ def generate_key_qr_file(qr_data: str, email: str) -> str:
@router.callback_query(F.data.startswith("connect_device|"))
async def handle_connect_device(callback_query: CallbackQuery, session: AsyncSession):
try:
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
return
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=IPHONE, callback_data=f"connect_ios|{key_name}"))
- builder.row(InlineKeyboardButton(text=ANDROID, callback_data=f"connect_android|{key_name}"))
- builder.row(InlineKeyboardButton(text=PC, callback_data=f"connect_pc|{key_name}"))
- builder.row(InlineKeyboardButton(text=TV, callback_data=f"connect_tv|{key_name}"))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
+ client_id = record.get("client_id")
+ builder.row(InlineKeyboardButton(text=IPHONE, callback_data=build_key_callback("connect_ios", client_id, key_name)))
+ builder.row(
+ InlineKeyboardButton(text=ANDROID, callback_data=build_key_callback("connect_android", client_id, key_name))
+ )
+ builder.row(InlineKeyboardButton(text=PC, callback_data=build_key_callback("connect_pc", client_id, key_name)))
+ builder.row(InlineKeyboardButton(text=TV, callback_data=build_key_callback("connect_tv", client_id, key_name)))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("view_key", client_id, key_name)))
hook_builder = InlineKeyboardBuilder()
hook_builder.attach(builder)
@@ -104,7 +108,9 @@ async def handle_connect_device(callback_query: CallbackQuery, session: AsyncSes
@router.callback_query(F.data.startswith("connect_phone|"))
async def process_callback_connect_phone(callback_query: CallbackQuery, session: AsyncSession):
- email = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
try:
record = await get_key_details(session, email)
@@ -142,7 +148,7 @@ async def process_callback_connect_phone(callback_query: CallbackQuery, session:
)
if INSTRUCTIONS_BUTTON:
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("view_key", record.get("client_id"), email)))
await edit_or_send_message(
target_message=callback_query.message,
@@ -154,7 +160,9 @@ async def process_callback_connect_phone(callback_query: CallbackQuery, session:
@router.callback_query(F.data.startswith("connect_ios|"))
async def process_callback_connect_ios(callback_query: CallbackQuery, session: AsyncSession):
- email = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
try:
record = await get_key_details(session, email)
@@ -185,7 +193,9 @@ async def process_callback_connect_ios(callback_query: CallbackQuery, session: A
builder.row(InlineKeyboardButton(text=IMPORT_IOS, url=ios_url))
if INSTRUCTIONS_BUTTON:
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}"))
+ builder.row(
+ InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_device", record.get("client_id"), email))
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -198,7 +208,9 @@ async def process_callback_connect_ios(callback_query: CallbackQuery, session: A
@router.callback_query(F.data.startswith("connect_android|"))
async def process_callback_connect_android(callback_query: CallbackQuery, session: AsyncSession):
- email = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
try:
record = await get_key_details(session, email)
@@ -229,7 +241,9 @@ async def process_callback_connect_android(callback_query: CallbackQuery, sessio
builder.row(InlineKeyboardButton(text=IMPORT_ANDROID, url=android_url))
if INSTRUCTIONS_BUTTON:
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}"))
+ builder.row(
+ InlineKeyboardButton(text=BACK, callback_data=build_key_callback("connect_device", record.get("client_id"), email))
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -243,11 +257,8 @@ async def process_callback_connect_android(callback_query: CallbackQuery, sessio
@router.callback_query(F.data.startswith("show_qr|"))
async def show_qr_code(callback_query: types.CallbackQuery, session: AsyncSession):
try:
- key_name = callback_query.data.split("|")[1]
-
- stmt = select(Key).where(Key.email == key_name)
- result = await session.execute(stmt)
- record = result.scalars().first()
+ key_ref = callback_query.data.split("|", 1)[1]
+ record = await resolve_key(session, callback_query.from_user.id, key_ref)
if not record:
await callback_query.message.answer("❌ Подписка не найдена.")
@@ -266,7 +277,12 @@ async def show_qr_code(callback_query: types.CallbackQuery, session: AsyncSessio
qr_path = await run_cpu(generate_key_qr_file, qr_data, record.email)
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record.email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", record.client_id, record.email),
+ )
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py
deleted file mode 100644
index ecf487f6..00000000
--- a/handlers/keys/key_freeze.py
+++ /dev/null
@@ -1,228 +0,0 @@
-import time
-
-from typing import Any
-
-from aiogram import F, Router
-from aiogram.types import CallbackQuery, InlineKeyboardButton
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-
-from database import (
- get_key_details,
- get_servers,
- get_tariff_by_id,
- mark_key_as_frozen,
- mark_key_as_unfrozen,
-)
-from handlers.buttons import APPLY, BACK, CANCEL
-from handlers.keys.operations import renew_key_in_cluster, toggle_client_on_cluster
-from handlers.texts import (
- FREEZE_SUBSCRIPTION_CONFIRM_MSG,
- SUBSCRIPTION_FROZEN_MSG,
- SUBSCRIPTION_UNFROZEN_MSG,
- UNFREEZE_SUBSCRIPTION_CONFIRM_MSG,
-)
-from handlers.keys.utils import key_owned_by_user
-from handlers.utils import edit_or_send_message, handle_error
-from middlewares.session import release_session_early
-from logger import logger
-
-
-router = Router()
-
-
-@router.callback_query(F.data.startswith("unfreeze_subscription|"))
-async def process_callback_unfreeze_subscription(callback_query: CallbackQuery, session: Any):
- key_name = callback_query.data.split("|")[1]
- record = await get_key_details(session, key_name)
- if not key_owned_by_user(record, callback_query.from_user.id):
- await callback_query.answer("Доступ запрещён.", show_alert=True)
- return
- confirm_text = UNFREEZE_SUBSCRIPTION_CONFIRM_MSG
-
- builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text=APPLY,
- callback_data=f"unfreeze_subscription_confirm|{key_name}",
- ),
- InlineKeyboardButton(
- text=CANCEL,
- callback_data=f"view_key|{key_name}",
- ),
- )
-
- await edit_or_send_message(
- target_message=callback_query.message,
- text=confirm_text,
- reply_markup=builder.as_markup(),
- )
-
-
-@router.callback_query(F.data.startswith("unfreeze_subscription_confirm|"))
-async def process_callback_unfreeze_subscription_confirm(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(session, key_name)
- if not record:
- await callback_query.message.answer("Ключ не найден.")
- return
- if not key_owned_by_user(record, callback_query.from_user.id):
- await callback_query.answer("Доступ запрещён.", show_alert=True)
- return
-
- email = record["email"]
- client_id = record["client_id"]
- cluster_id = record["server_id"]
-
- result = await toggle_client_on_cluster(cluster_id, email, client_id, enable=True, session=session)
- if result["status"] != "success":
- logger.warning(f"Не удалось включить подписку: {result.get('error') or result.get('results')}")
-
- servers = await get_servers(session)
- cluster_servers = servers.get(cluster_id, [])
-
- if not cluster_servers:
- text_error = "Сервер не найден."
- builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
- await edit_or_send_message(callback_query.message, text_error, builder.as_markup())
- return
-
- tariff = await get_tariff_by_id(session, record["tariff_id"]) if record.get("tariff_id") else None
-
- if not tariff:
- logger.info("[Unfreeze] Тариф не найден — применяем дефолтные значения.")
- total_gb = 0
- hwid_limit = 0
- else:
- total_gb = int(tariff.get("traffic_limit") or 0)
- hwid_limit = int(tariff.get("device_limit") or 0)
-
- now_ms = int(time.time() * 1000)
- leftover = record["expiry_time"]
- if leftover < 0:
- leftover = 0
- new_expiry_time = now_ms + leftover
-
- await mark_key_as_unfrozen(session, record["tg_id"], client_id, new_expiry_time)
- await session.commit()
- await release_session_early(session)
-
- max(leftover / (1000 * 86400), 0.01)
- logger.info(
- f"[Unfreeze Debug] Запуск renew_key_in_cluster с expiry={new_expiry_time}, gb={total_gb}, hwid={hwid_limit}"
- )
-
- await renew_key_in_cluster(
- cluster_id=cluster_id,
- email=email,
- client_id=client_id,
- new_expiry_time=new_expiry_time,
- total_gb=total_gb,
- session=session,
- hwid_device_limit=hwid_limit,
- reset_traffic=False,
- plan=record.get("tariff_id"),
- )
-
- text_ok = SUBSCRIPTION_UNFROZEN_MSG
- builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
- await edit_or_send_message(callback_query.message, text_ok, builder.as_markup())
-
- except Exception as e:
- await handle_error(tg_id, callback_query, f"Ошибка при включении подписки: {e}")
-
-
-@router.callback_query(F.data.startswith("freeze_subscription|"))
-async def process_callback_freeze_subscription(callback_query: CallbackQuery, session: Any):
- """
- Показывает пользователю диалог подтверждения заморозки (отключения) подписки.
- """
- key_name = callback_query.data.split("|")[1]
- record = await get_key_details(session, key_name)
- if not key_owned_by_user(record, callback_query.from_user.id):
- await callback_query.answer("Доступ запрещён.", show_alert=True)
- return
-
- confirm_text = FREEZE_SUBSCRIPTION_CONFIRM_MSG
-
- builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text=APPLY,
- callback_data=f"freeze_subscription_confirm|{key_name}",
- ),
- InlineKeyboardButton(
- text=CANCEL,
- callback_data=f"view_key|{key_name}",
- ),
- )
-
- await edit_or_send_message(
- target_message=callback_query.message,
- text=confirm_text,
- reply_markup=builder.as_markup(),
- )
-
-
-@router.callback_query(F.data.startswith("freeze_subscription_confirm|"))
-async def process_callback_freeze_subscription_confirm(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(session, key_name)
- if not record:
- await callback_query.message.answer("Ключ не найден.")
- return
- if not key_owned_by_user(record, callback_query.from_user.id):
- await callback_query.answer("Доступ запрещён.", show_alert=True)
- return
-
- email = record["email"]
- client_id = record["client_id"]
- cluster_id = record["server_id"]
-
- result = await toggle_client_on_cluster(cluster_id, email, client_id, enable=False, session=session)
-
- if result["status"] == "success":
- now_ms = int(time.time() * 1000)
- time_left = record["expiry_time"] - now_ms
- if time_left < 0:
- time_left = 0
-
- await mark_key_as_frozen(session, record["tg_id"], client_id, time_left)
- await session.commit()
-
- text_ok = SUBSCRIPTION_FROZEN_MSG
- builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
- await edit_or_send_message(
- target_message=callback_query.message,
- text=text_ok,
- reply_markup=builder.as_markup(),
- )
-
- else:
- text_error = (
- f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
- )
- builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
- await edit_or_send_message(
- target_message=callback_query.message,
- text=text_error,
- reply_markup=builder.as_markup(),
- )
-
- except Exception as e:
- await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py
index 3d9bbc65..9c57ee27 100644
--- a/handlers/keys/key_mode/key_cluster_mode.py
+++ b/handlers/keys/key_mode/key_cluster_mode.py
@@ -33,6 +33,7 @@ from handlers.buttons import (
TV_BUTTON,
)
from handlers.keys.operations import create_key_on_cluster
+from handlers.keys.utils import build_key_callback
from database import get_vless_enabled
from handlers.tariffs.tariff_display import (
build_key_created_message,
@@ -214,7 +215,7 @@ async def key_cluster_mode(
builder = InlineKeyboardBuilder()
if vless_enabled:
- builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=f"connect_router|{key_name}"))
+ builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=build_key_callback("connect_router", client_id, key_name)))
else:
if await is_full_remnawave_cluster(least_loaded_cluster, session):
use_webapp = bool(MODES_CONFIG.get("REMNAWAVE_WEBAPP_ENABLED", REMNAWAVE_WEBAPP))
@@ -239,13 +240,13 @@ async def key_cluster_mode(
else:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
if tv_button_enabled:
- builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"))
+ builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=build_key_callback("connect_tv", client_id, email)))
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=build_key_callback("connect_device", client_id, key_name)))
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=build_key_callback("connect_device", client_id, key_name)))
- builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=build_key_callback("view_key", client_id, key_name)))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py
index 6897feec..971c1362 100644
--- a/handlers/keys/key_mode/key_country_mode.py
+++ b/handlers/keys/key_mode/key_country_mode.py
@@ -47,6 +47,7 @@ from handlers.buttons import (
SUPPORT,
TV_BUTTON,
)
+from handlers.keys.utils import build_key_callback, resolve_key
from handlers.keys.operations import create_client_on_server
from handlers.keys.operations.aggregated_links import make_aggregated_link
from handlers.tariffs.tariff_display import (
@@ -258,7 +259,9 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
await callback_query.answer("❌ Некорректные данные", show_alert=True)
return
- old_key_name = data[1]
+ old_key_ref = data[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, old_key_ref)
+ old_key_name = key_obj.email if key_obj else old_key_ref
record = await get_key_details(session, old_key_name)
if not record:
await callback_query.answer("❌ Ключ не найден", show_alert=True)
@@ -351,7 +354,12 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
available_servers = [s["server_name"] for s in filtered_servers]
else:
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", record.get("client_id"), old_key_name),
+ )
+ )
await edit_or_send_message(
target_message=callback_query.message,
text="❌ Нет доступных стран для смены локации.",
@@ -373,7 +381,12 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
if not available_servers:
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", record.get("client_id"), old_key_name),
+ )
+ )
await edit_or_send_message(
target_message=callback_query.message,
text="❌ Нет доступных стран для смены локации.",
@@ -386,11 +399,16 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
for i in range(0, len(available_servers), 2):
row_buttons = []
for country in available_servers[i : i + 2]:
- callback_data = f"select_country|{country}|{ts}|{old_key_name}"
+ callback_data = f"select_country|{country}|{ts}|{old_key_ref}"
row_buttons.append(InlineKeyboardButton(text=country, callback_data=callback_data))
builder.row(*row_buttons)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", record.get("client_id"), old_key_name),
+ )
+ )
await edit_or_send_message(
target_message=callback_query.message,
@@ -487,6 +505,8 @@ async def finalize_key_creation(
old_key_details: dict[str, Any] | None = None
if old_key_name:
+ key_obj = await resolve_key(session, tg_id, old_key_name)
+ old_key_name = key_obj.email if key_obj else old_key_name
old_key_details = await get_key_details(session, old_key_name)
if not old_key_details:
await callback_query.message.answer("❌ Ключ не найден. Попробуйте снова.")
@@ -769,7 +789,7 @@ async def finalize_key_creation(
if panel_type == "remnawave" or is_full_remnawave:
if is_vless:
- builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=f"connect_router|{key_name}"))
+ builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=build_key_callback("connect_router", client_id, key_name)))
else:
if use_webapp and webapp_url:
if open_in_browser:
@@ -777,13 +797,23 @@ async def finalize_key_creation(
else:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=webapp_url)))
if tv_button_enabled:
- builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"))
+ builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=build_key_callback("connect_tv", client_id, key_name)))
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=CONNECT_DEVICE,
+ callback_data=build_key_callback("connect_device", client_id, key_name),
+ )
+ )
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=CONNECT_DEVICE,
+ callback_data=build_key_callback("connect_device", client_id, key_name),
+ )
+ )
- builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=build_key_callback("view_key", client_id, key_name)))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
diff --git a/handlers/keys/key_mode/key_discount_mode.py b/handlers/keys/key_mode/key_discount_mode.py
index a6fa16f5..d732ab01 100644
--- a/handlers/keys/key_mode/key_discount_mode.py
+++ b/handlers/keys/key_mode/key_discount_mode.py
@@ -12,6 +12,7 @@ from core.bootstrap import NOTIFICATIONS_CONFIG
from database import get_keys, get_tariffs, get_tariffs_for_cluster
from database.models import Notification
from handlers.buttons import MAIN_MENU, RENEW_KEY_NOTIFICATION
+from handlers.keys.utils import build_key_callback
from handlers.notifications.notify_kb import build_tariffs_keyboard
from handlers.tariffs.buy.key_tariffs import select_tariff_plan
from handlers.texts import DISCOUNT_TARIFF, DISCOUNT_TARIFF_MAX
@@ -49,7 +50,12 @@ async def handle_discount_entry(callback: CallbackQuery, session: AsyncSession):
if keys and len(keys) > 0:
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=RENEW_KEY_NOTIFICATION, callback_data=f"renew_key|{keys[0].email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=RENEW_KEY_NOTIFICATION,
+ callback_data=build_key_callback("renew_key", keys[0].client_id, keys[0].email),
+ )
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
expires_at = last_time + timedelta(hours=discount_active_hours)
@@ -127,7 +133,12 @@ async def handle_ultra_discount(callback: CallbackQuery, session: AsyncSession):
if keys and len(keys) > 0:
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=RENEW_KEY_NOTIFICATION, callback_data=f"renew_key|{keys[0].email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=RENEW_KEY_NOTIFICATION,
+ callback_data=build_key_callback("renew_key", keys[0].client_id, keys[0].email),
+ )
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
expires_at = last_time + timedelta(hours=discount_active_hours)
diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py
index f6ce7ddc..6e4a3b9e 100644
--- a/handlers/keys/key_renew.py
+++ b/handlers/keys/key_renew.py
@@ -56,7 +56,7 @@ from hooks.processors import (
)
from logger import logger
-from .utils import add_tariff_button_generic, key_owned_by_user
+from .utils import add_tariff_button_generic, build_key_callback, key_owned_by_user, resolve_key
router = Router()
@@ -79,7 +79,9 @@ def normalize_expiry_ms(raw_value: int | float | None) -> int:
async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
"""Обрабатывает нажатие кнопки продления конкретного ключа."""
tg_id = callback_query.message.chat.id
- key_name = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
try:
record = await get_key_details(session, key_name)
@@ -103,7 +105,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
if now_utc < available_from_utc:
dt_msk = available_from_utc.astimezone(moscow_tz).strftime("%d.%m.%Y %H:%M")
kb = InlineKeyboardBuilder()
- kb.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
+ kb.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("view_key", client_id, key_name)))
hook_commands = await process_process_callback_renew_key(
callback_query=callback_query, state=state, session=session
@@ -118,7 +120,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
)
return
- await state.update_data(renew_key_name=key_name, renew_client_id=client_id)
+ await state.update_data(renew_key_name=key_name, renew_client_id=client_id, renew_key_ref=key_ref)
logger.info(f"[RENEW] Получение тарифов для server_id={server_id}")
@@ -218,7 +220,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
)
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=build_key_callback("view_key", client_id, key_name)))
hook_builder = InlineKeyboardBuilder()
hook_builder.attach(builder)
@@ -372,7 +374,12 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
callback_prefix="renew_plan",
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"renew_key|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("renew_key", data.get("renew_client_id"), key_name),
+ )
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
hook_builder = InlineKeyboardBuilder()
@@ -884,7 +891,7 @@ async def complete_key_renewal(
await reset_key_current_limits_to_selected(session, effective_client_id)
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{email}"))
+ builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=build_key_callback("view_key", client_id, email)))
hook_commands = await process_renewal_complete(
chat_id=tg_id, admin=False, session=session, email=email, client_id=client_id
)
diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py
index ae35eacc..a58d4b7f 100644
--- a/handlers/keys/key_view.py
+++ b/handlers/keys/key_view.py
@@ -22,7 +22,6 @@ from config import (
QRCODE,
REMNAWAVE_WEBAPP,
REMNAWAVE_WEBAPP_OPEN_IN_BROWSER,
- TOGGLE_CLIENT,
USE_COUNTRY_SELECTION,
)
from core.bootstrap import BUTTONS_CONFIG, MODES_CONFIG
@@ -43,14 +42,12 @@ from handlers.buttons import (
CHANGE_LOCATION,
CONNECT_DEVICE,
DELETE,
- FREEZE,
HWID_BUTTON,
MAIN_MENU,
QR,
RENEW_KEY,
ROUTER_BUTTON,
TV_BUTTON,
- UNFREEZE,
)
from database import get_vless_enabled_batch
from handlers.tariffs.tariff_display import GB, get_key_tariff_addons_state
@@ -63,7 +60,7 @@ from handlers.texts import (
RENAME_KEY_PROMPT,
key_message,
)
-from handlers.keys.utils import key_owned_by_user
+from handlers.keys.utils import build_key_callback, build_key_ref, key_owned_by_user, resolve_key
from handlers.utils import (
edit_or_send_message,
format_days,
@@ -106,9 +103,9 @@ async def process_callback_or_message_view_keys(
records = await get_keys(session, tg_id)
if records and len(records) == 1:
- key_name = records[0].email
+ key_ref = build_key_ref(records[0].client_id, records[0].email)
image_path = os.path.join("img", "pic_view.jpg")
- await render_key_info(target_message, session, key_name, image_path)
+ await render_key_info(target_message, session, key_ref, image_path)
return
inline_keyboard, response_message = await build_keys_response(records, session, page=page)
@@ -186,7 +183,7 @@ async def build_keys_response(records: list[Key] | None, session: AsyncSession,
key_button = InlineKeyboardButton(
text=f"{icon} {key_display}",
- callback_data=f"view_key|{email}",
+ callback_data=build_key_callback("view_key", client_id, email),
)
rename_button = InlineKeyboardButton(
text=ALIAS,
@@ -292,24 +289,26 @@ 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: AsyncSession):
- key_name = callback_query.data.split("|")[1]
- record = await get_key_details(session, key_name)
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ record = await get_key_details(session, key_obj.email) if key_obj else None
if not key_owned_by_user(record, callback_query.from_user.id):
await safe_answer_callback(callback_query, "Доступ запрещён.", show_alert=True)
return
image_path = os.path.join("img", "pic_view.jpg")
- await render_key_info(callback_query.message, session, key_name, image_path)
+ await render_key_info(callback_query.message, session, key_ref, image_path)
-async def build_key_view_payload(session: AsyncSession, key_name: str):
+async def build_key_view_payload(session: AsyncSession, tg_id: int, key_ref_or_email: str):
+ key_obj = await resolve_key(session, tg_id, key_ref_or_email)
+ key_name = key_obj.email if key_obj else key_ref_or_email
record = await get_key_details(session, key_name)
if not record:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
return "Информация о подписке не найдена.", builder.as_markup(), False
- db_key_result = await session.execute(select(Key).where(Key.email == key_name))
- db_key: Key | None = db_key_result.scalar_one_or_none()
+ db_key = key_obj
is_frozen = record["is_frozen"]
client_id = record.get("client_id")
@@ -318,7 +317,6 @@ async def build_key_view_payload(session: AsyncSession, key_name: str):
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"))
return FROZEN_SUBSCRIPTION_MSG, builder.as_markup(), True
@@ -420,22 +418,47 @@ async def build_key_view_payload(session: AsyncSession, key_name: str):
if is_full_remnawave and final_link and use_webapp and not happ_cryptolink_enabled:
if vless_enabled:
- builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=f"connect_router|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=ROUTER_BUTTON,
+ callback_data=build_key_callback("connect_router", client_id, key_name),
+ )
+ )
elif open_in_browser:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, url=final_link))
if tv_button_enabled:
- builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=TV_BUTTON,
+ callback_data=build_key_callback("connect_tv", client_id, key_name),
+ )
+ )
else:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
if tv_button_enabled:
- builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=TV_BUTTON,
+ callback_data=build_key_callback("connect_tv", client_id, key_name),
+ )
+ )
else:
if vless_enabled:
- builder.row(InlineKeyboardButton(text=ROUTER_BUTTON, callback_data=f"connect_router|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=ROUTER_BUTTON,
+ callback_data=build_key_callback("connect_router", client_id, key_name),
+ )
+ )
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=CONNECT_DEVICE,
+ callback_data=build_key_callback("connect_device", client_id, key_name),
+ )
+ )
- builder.row(InlineKeyboardButton(text=RENEW_KEY, callback_data=f"renew_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=RENEW_KEY, callback_data=build_key_callback("renew_key", client_id, key_name)))
if is_tariff_configurable and (addons_devices_enabled or addons_traffic_enabled):
if addons_devices_enabled and addons_traffic_enabled:
@@ -444,27 +467,24 @@ async def build_key_view_payload(session: AsyncSession, key_name: str):
addons_text = ADDONS_BUTTON_DEVICES
else:
addons_text = ADDONS_BUTTON_TRAFFIC
- builder.row(InlineKeyboardButton(text=addons_text, callback_data=f"key_addons|{key_name}"))
+ builder.row(InlineKeyboardButton(text=addons_text, callback_data=build_key_callback("key_addons", client_id, key_name)))
hwid_reset_enabled = bool(BUTTONS_CONFIG.get("HWID_RESET_BUTTON_ENABLE", HWID_RESET_BUTTON))
qrcode_enabled = bool(BUTTONS_CONFIG.get("QRCODE_BUTTON_ENABLE", QRCODE))
delete_key_enabled = bool(BUTTONS_CONFIG.get("DELETE_KEY_BUTTON_ENABLE", ENABLE_DELETE_KEY_BUTTON))
- toggle_client_enabled = bool(BUTTONS_CONFIG.get("TOGGLE_CLIENT_BUTTON_ENABLE", TOGGLE_CLIENT))
-
if hwid_reset_enabled and hwid_count > 0:
- builder.row(InlineKeyboardButton(text=HWID_BUTTON, callback_data=f"reset_hwid|{key_name}"))
+ builder.row(InlineKeyboardButton(text=HWID_BUTTON, callback_data=build_key_callback("reset_hwid", client_id, key_name)))
if qrcode_enabled:
- builder.row(InlineKeyboardButton(text=QR, callback_data=f"show_qr|{key_name}"))
+ builder.row(InlineKeyboardButton(text=QR, callback_data=build_key_callback("show_qr", client_id, key_name)))
if delete_key_enabled:
- builder.row(InlineKeyboardButton(text=DELETE, callback_data=f"delete_key|{key_name}"))
+ builder.row(InlineKeyboardButton(text=DELETE, callback_data=build_key_callback("delete_key", client_id, key_name)))
if country_selection_enabled:
- builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}"))
-
- if toggle_client_enabled:
- builder.row(InlineKeyboardButton(text=FREEZE, callback_data=f"freeze_subscription|{key_name}"))
+ builder.row(
+ InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=build_key_callback("change_location", client_id, key_name))
+ )
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
@@ -475,12 +495,12 @@ async def build_key_view_payload(session: AsyncSession, key_name: str):
async def build_key_view_message(session: AsyncSession, email: str):
- text, reply_markup, _ = await build_key_view_payload(session, email)
+ text, reply_markup, _ = await build_key_view_payload(session, 0, email)
return text, reply_markup
-async def render_key_info(message: Message, session: AsyncSession, key_name: str, image_path: str):
- text, reply_markup, _ = await build_key_view_payload(session, key_name)
+async def render_key_info(message: Message, session: AsyncSession, key_ref_or_email: str, image_path: str):
+ text, reply_markup, _ = await build_key_view_payload(session, message.chat.id, key_ref_or_email)
await edit_or_send_message(
target_message=message,
text=text,
@@ -491,8 +511,9 @@ async def render_key_info(message: Message, session: AsyncSession, key_name: str
@router.callback_query(F.data.startswith("reset_hwid|"))
async def handle_reset_hwid(callback_query: CallbackQuery, session: AsyncSession):
- key_name = callback_query.data.split("|")[1]
-
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_name = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_name)
if not record:
await safe_answer_callback(callback_query, "❌ Ключ не найден.", show_alert=True)
@@ -562,4 +583,4 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: AsyncSession
return
image_path = os.path.join("img", "pic_view.jpg")
- await render_key_info(callback_query.message, session, key_name, image_path)
+ await render_key_info(callback_query.message, session, key_ref, image_path)
diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py
index 5f108ec9..9a278ee2 100644
--- a/handlers/keys/keys.py
+++ b/handlers/keys/keys.py
@@ -7,7 +7,7 @@ from database import delete_key, get_key_details
from handlers.buttons import APPLY, BACK, CANCEL
from handlers.keys.key_view import process_callback_view_key
from handlers.keys.operations import delete_key_from_cluster, update_subscription
-from handlers.keys.utils import key_owned_by_user
+from handlers.keys.utils import build_key_callback, key_owned_by_user, resolve_key
from handlers.texts import DELETE_KEY_CONFIRM_MSG, KEY_DELETED_MSG_SIMPLE
from handlers.utils import edit_or_send_message, handle_error
from middlewares.session import release_session_early
@@ -20,7 +20,9 @@ router = Router()
@router.callback_query(F.data.startswith("update_subscription|"))
async def process_callback_update_subscription(callback_query: CallbackQuery, session: AsyncSession):
tg_id = callback_query.message.chat.id
- email = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
try:
record = await get_key_details(session, email)
@@ -42,15 +44,17 @@ async def process_callback_update_subscription(callback_query: CallbackQuery, se
@router.callback_query(F.data.startswith("delete_key|"))
async def process_callback_delete_key(callback_query: CallbackQuery, session: AsyncSession):
- key_identifier = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
try:
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ key_identifier = key_obj.email if key_obj else key_ref
record = await get_key_details(session, key_identifier)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
return
confirmation_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
- [types.InlineKeyboardButton(text=APPLY, callback_data=f"confirm_delete|{key_identifier}")],
+ [types.InlineKeyboardButton(text=APPLY, callback_data=build_key_callback("confirm_delete", record.get("client_id"), key_identifier))],
[types.InlineKeyboardButton(text=CANCEL, callback_data="view_keys")],
]
)
@@ -68,8 +72,10 @@ async def process_callback_delete_key(callback_query: CallbackQuery, session: As
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(callback_query: CallbackQuery, session: AsyncSession):
- email = callback_query.data.split("|")[1]
+ key_ref = callback_query.data.split("|", 1)[1]
try:
+ key_obj = await resolve_key(session, callback_query.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
record = await get_key_details(session, email)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py
index 73174e92..d3d5d572 100644
--- a/handlers/keys/subscriptions.py
+++ b/handlers/keys/subscriptions.py
@@ -213,7 +213,6 @@ def prepare_headers(
"announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"),
"profile-web-page-url": f"https://t.me/{USERNAME_BOT}",
"subscription-userinfo": subscription_userinfo,
- # "routing": "happ://routing/onadd/...",
}
elif "Hiddify" in user_agent:
parts = subscription_info.split(" - ")[0].split(": ")
diff --git a/handlers/keys/utils.py b/handlers/keys/utils.py
index 198df76b..f97d6e18 100644
--- a/handlers/keys/utils.py
+++ b/handlers/keys/utils.py
@@ -1,9 +1,13 @@
+import hashlib
+
from typing import Any
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
+from database import get_key_by_client_id, get_key_by_email, get_keys
+from database.models import Key
from handlers.payments.currency_rates import format_for_user
@@ -12,6 +16,36 @@ def key_owned_by_user(record: dict | None, user_id: int) -> bool:
return record is not None and record.get("tg_id") == user_id
+def build_key_ref(client_id: str | None, email: str | None = None) -> str:
+ source = str(client_id or email or "")
+ return hashlib.blake2s(source.encode(), digest_size=6).hexdigest()
+
+
+def build_key_callback(prefix: str, client_id: str | None, email: str | None = None) -> str:
+ return f"{prefix}|{build_key_ref(client_id, email)}"
+
+
+async def resolve_key(session: AsyncSession, tg_id: int, key_ref: str | int | None) -> Key | None:
+ if key_ref is None:
+ return None
+
+ key_ref_str = str(key_ref)
+
+ key_obj = await get_key_by_email(session, key_ref_str, tg_id)
+ if key_obj:
+ return key_obj
+
+ key_obj = await get_key_by_client_id(session, key_ref_str, tg_id)
+ if key_obj:
+ return key_obj
+
+ for candidate in await get_keys(session, tg_id):
+ if build_key_ref(candidate.client_id, candidate.email) == key_ref_str:
+ return await get_key_by_client_id(session, candidate.client_id, tg_id)
+
+ return None
+
+
async def add_tariff_button_generic(
builder: InlineKeyboardBuilder,
tariff: dict[str, Any],
diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py
index 635926e5..2dffe615 100644
--- a/handlers/notifications/general_notifications.py
+++ b/handlers/notifications/general_notifications.py
@@ -215,7 +215,7 @@ async def send_expiry_warning(ctx: NotificationContext, key, hours_left: int, ph
tariff_details=expiry_data["tariff_details"],
)
- keyboard = build_notification_kb(email)
+ keyboard = build_notification_kb(email, getattr(key, "client_id", None))
return await send_notification(ctx.bot, tg_id, photo, message_text, keyboard)
@@ -233,7 +233,7 @@ async def send_cannot_renew(ctx: NotificationContext, key, photo: str) -> bool:
tariff_details=expiry_data["tariff_details"],
)
- keyboard = build_change_tariff_kb(email)
+ keyboard = build_change_tariff_kb(email, getattr(key, "client_id", None))
return await send_notification(ctx.bot, tg_id, photo, message_text, keyboard)
@@ -254,7 +254,7 @@ async def send_expired_notification(ctx: NotificationContext, key, delay_minutes
else:
message_text = KEY_EXPIRED_NO_DELAY_MSG.format(email=email)
- keyboard = build_notification_kb(email)
+ keyboard = build_notification_kb(email, getattr(key, "client_id", None))
return await send_notification(ctx.bot, tg_id, "notify_expired.jpg", message_text, keyboard)
@@ -498,7 +498,7 @@ async def notify_expiring_keys(
tariff_name=expiry_data["tariff_name"],
tariff_details=expiry_data["tariff_details"],
)
- keyboard = build_notification_kb(email)
+ keyboard = build_notification_kb(email, getattr(key, "client_id", None))
messages.append({
"tg_id": tg_id,
"text": notification_text,
diff --git a/handlers/notifications/notify_kb.py b/handlers/notifications/notify_kb.py
index 20e61e12..7113a39e 100644
--- a/handlers/notifications/notify_kb.py
+++ b/handlers/notifications/notify_kb.py
@@ -1,9 +1,10 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from handlers.buttons import CHANGE_TARIFF, DISCOUNT_TARIFF, MAIN_MENU, MAX_DISCOUNT_TARIFF, RENEW_KEY_NOTIFICATION
+from handlers.keys.utils import build_key_callback
-def build_notification_kb(email: str) -> InlineKeyboardMarkup:
+def build_notification_kb(email: str, client_id: str | None = None) -> InlineKeyboardMarkup:
"""
Формирует inline-клавиатуру для уведомлений.
Кнопки: "🔄 Продлить VPN" (callback_data содержит email) и "👤 Личный кабинет".
@@ -11,13 +12,13 @@ def build_notification_kb(email: str) -> InlineKeyboardMarkup:
from aiogram.utils.keyboard import InlineKeyboardBuilder
builder = InlineKeyboardBuilder()
- builder.button(text=RENEW_KEY_NOTIFICATION, callback_data=f"renew_key|{email}")
+ builder.button(text=RENEW_KEY_NOTIFICATION, callback_data=build_key_callback("renew_key", client_id, email))
builder.button(text=MAIN_MENU, callback_data="profile")
builder.adjust(1)
return builder.as_markup()
-def build_change_tariff_kb(email: str) -> InlineKeyboardMarkup:
+def build_change_tariff_kb(email: str, client_id: str | None = None) -> InlineKeyboardMarkup:
"""
Формирует inline-клавиатуру для смены тарифа.
Кнопки: "🔄 Сменить тариф" (callback_data содержит email) и "👤 Личный кабинет".
@@ -25,7 +26,7 @@ def build_change_tariff_kb(email: str) -> InlineKeyboardMarkup:
from aiogram.utils.keyboard import InlineKeyboardBuilder
builder = InlineKeyboardBuilder()
- builder.button(text=CHANGE_TARIFF, callback_data=f"renew_key|{email}")
+ builder.button(text=CHANGE_TARIFF, callback_data=build_key_callback("renew_key", client_id, email))
builder.button(text=MAIN_MENU, callback_data="profile")
builder.adjust(1)
return builder.as_markup()
diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py
index e476b0c4..79fcd7af 100644
--- a/handlers/notifications/special_notifications.py
+++ b/handlers/notifications/special_notifications.py
@@ -21,6 +21,7 @@ from database import add_notification, check_notifications_bulk
from database.models import Key, User
from database.tariffs import get_tariffs
from handlers.buttons import CONNECT_DEVICE, MAIN_MENU, SUPPORT, TRIAL_BONUS
+from handlers.keys.utils import build_key_callback
from handlers.keys.operations import get_user_traffic
from handlers.notifications.notify_utils import send_messages_with_limit
from handlers.texts import (
@@ -209,10 +210,20 @@ async def notify_users_no_traffic(bot: Bot, session: AsyncSession, current_time:
else:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
else:
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=CONNECT_DEVICE,
+ callback_data=build_key_callback("connect_device", key.client_id, email),
+ )
+ )
except Exception as error:
logger.error(f"Ошибка при определении типа панели для {email}: {error}")
- builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=CONNECT_DEVICE,
+ callback_data=build_key_callback("connect_device", key.client_id, email),
+ )
+ )
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
diff --git a/handlers/tariffs/addons/key_addons_main.py b/handlers/tariffs/addons/key_addons_main.py
index 2aba344f..ecef8ab4 100644
--- a/handlers/tariffs/addons/key_addons_main.py
+++ b/handlers/tariffs/addons/key_addons_main.py
@@ -37,6 +37,7 @@ from hooks.processors import process_addon_purchase_complete, process_addons_men
from logger import logger
from ..buy.key_tariffs import calculate_config_price
+from ...keys.utils import build_key_callback, resolve_key
from .utils import (
KeyAddonConfigState,
build_addons_screen_text,
@@ -295,7 +296,12 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
)
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", data.get("addon_key_client_id"), email),
+ )
+ )
module_buttons = await process_addons_menu(email=email, session=session)
builder = insert_hook_buttons(builder, module_buttons)
@@ -310,7 +316,9 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
@router.callback_query(F.data.startswith("key_addons|"))
async def start_key_addons(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
- email = callback.data.split("|")[1]
+ key_ref = callback.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
logger.debug(f"[ADDONS] start_key_addons: tg_id={callback.from_user.id} email={email}")
record = await get_key_details(session, email)
@@ -428,6 +436,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
await state.update_data(
addon_key_email=email,
+ addon_key_client_id=record.get("client_id"),
addon_tariff_id=int(tariff_id),
addon_tariff_config=cfg_for_state,
addon_current_device_limit=current_devices,
@@ -581,7 +590,12 @@ async def handle_addons_downgrade(callback: CallbackQuery, state: FSMContext, se
callback_data="key_addons_downgrade_apply",
)
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"key_addons|{email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("key_addons", data.get("addon_key_client_id"), email),
+ )
+ )
await edit_or_send_message(
target_message=callback.message,
diff --git a/handlers/tariffs/addons/key_addons_pack.py b/handlers/tariffs/addons/key_addons_pack.py
index d24bba4e..9b86842b 100644
--- a/handlers/tariffs/addons/key_addons_pack.py
+++ b/handlers/tariffs/addons/key_addons_pack.py
@@ -37,6 +37,7 @@ from hooks.processors import process_addon_purchase_complete, process_addons_men
from logger import logger
from ..buy.key_tariffs import calculate_config_price
+from ...keys.utils import build_key_callback, resolve_key
from .utils import (
KeyAddonConfigState,
build_addons_pack_screen_text,
@@ -373,7 +374,12 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
callback_data="key_addons_confirm",
)
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=build_key_callback("view_key", data.get("addon_key_client_id"), email),
+ )
+ )
module_buttons = await process_addons_menu(email=email, session=session)
builder = insert_hook_buttons(builder, module_buttons)
@@ -388,7 +394,9 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
@router.callback_query(F.data.startswith("key_addons|"))
async def start_key_addons(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
- email = callback.data.split("|")[1]
+ key_ref = callback.data.split("|", 1)[1]
+ key_obj = await resolve_key(session, callback.from_user.id, key_ref)
+ email = key_obj.email if key_obj else key_ref
logger.debug(f"[ADDONS] PACK_MODE start_key_addons: tg_id={callback.from_user.id} email={email}")
record = await get_key_details(session, email)
@@ -546,6 +554,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
await state.update_data(
addon_key_email=email,
+ addon_key_client_id=record.get("client_id"),
addon_tariff_id=int(tariff_id),
addon_tariff_config=cfg_for_state,
addon_current_device_limit=current_devices,
diff --git a/utils/backup.py b/utils/backup.py
index 585f576e..b25a7c5a 100644
--- a/utils/backup.py
+++ b/utils/backup.py
@@ -42,9 +42,6 @@ async def backup_database() -> Exception | None:
"""
from core.executor import run_io
- # Создаём бэкап в пуле потоков (run_io), а не процессов (run_cpu), чтобы файл
- # создавался в том же процессе, что и отправка — иначе путь может быть недоступен
- # (воркер уведомлений и воркер пула процессов могут иметь разный cwd/окружение).
if BACKUP_CREATE_ARCHIVE:
if not any([BACKUP_INCLUDE_DB, BACKUP_INCLUDE_CONFIG, BACKUP_INCLUDE_TEXTS, BACKUP_INCLUDE_IMG]):
backup_file_path, exception = await run_io(_create_database_backup)
diff --git a/utils/versioning.py b/utils/versioning.py
index 7fe3bc1a..91fafc52 100644
--- a/utils/versioning.py
+++ b/utils/versioning.py
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
def get_version() -> str:
- return f"a02032612 {get_git_commit_number()}"
+ return f"a02031919 {get_git_commit_number()}"