remove client subscription freeze / keep admin subscription toggle / update admin button labels

Made-with: Cursor
This commit is contained in:
Vladless
2026-03-19 01:40:23 +03:00
parent 3a304afc49
commit 352bc22be2
41 changed files with 882 additions and 700 deletions
-6
View File
@@ -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
+2 -2
View File
@@ -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:
-1
View File
@@ -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
-6
View File
@@ -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"),
+4
View File
@@ -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)
+134 -1
View File
@@ -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(
+2 -2
View File
@@ -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 {
+6
View File
@@ -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:
+12
View File
@@ -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)
+10 -3
View File
@@ -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",
]
+10 -8
View File
@@ -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",
)
+7 -4
View File
@@ -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",
)
@@ -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 рефералов",
+7 -5
View File
@@ -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",
)
@@ -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",
)
+71 -68
View File
@@ -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()
+1 -5
View File
@@ -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"<code>{created_at}</code> {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<i>Событий пока нет.</i>")
else:
lines.append("")
rev = list(reversed(events)) # хронология сверху вниз
rev = list(reversed(events))
if full_flow:
by_cat: dict[str, list] = {}
for e in rev:
+12 -10
View File
@@ -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"└ 🔄 <b>Обновлено:</b> {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:
+257 -154
View File
@@ -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" (<i>{key_obj.alias}</i>)" 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 = "⏳ <b>Остаток:</b>"
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 = "⏰ <b>Истекает:</b>"
expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M")
else:
expiry_label = "⏰ <b>Истекает:</b>"
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(
"<blockquote>"
f"🔗 <b>Ключ{alias_part}:</b> <code>{key_value}</code>\n"
f"📆 <b>Создан:</b> {created_at} (МСК)\n"
f"⏰ <b>Истекает:</b> {expiry_date} (МСК)\n"
f"{'⛔ <b>Статус:</b> отключена\n' if is_frozen else ''}"
f"{expiry_label} {expiry_date}{' (МСК)' if not is_frozen and expiry_date != '' else ''}\n"
f"🌐 <b>Кластер:</b> {key_obj.server_id or ''}\n"
f"🆔 <b>ID клиента:</b> {key_obj.tg_id or ''}\n"
f"🏷️ <b>Тарифная группа:</b> {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 = (
"<b>🔄 Перевыпуск подписки</b>\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"📡 Выберите кластер, на котором пересоздать ключ <b>{email}</b>:",
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)
+31 -59
View File
@@ -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:
+12
View File
@@ -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)
+2 -8
View File
@@ -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 = "✏️"
# Реферальная система
+32 -16
View File
@@ -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,
-2
View File
@@ -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,
)
+37 -21
View File
@@ -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(
-228
View File
@@ -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}")
+6 -5
View File
@@ -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"))
+40 -10
View File
@@ -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"))
+13 -2
View File
@@ -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)
+14 -7
View File
@@ -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
)
+57 -36
View File
@@ -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 "<b>Информация о подписке не найдена.</b>", 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)
+11 -5
View File
@@ -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)
-1
View File
@@ -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(": ")
+34
View File
@@ -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],
@@ -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,
+5 -4
View File
@@ -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()
@@ -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"))
+17 -3
View File
@@ -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,
+11 -2
View File
@@ -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,
-3
View File
@@ -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)
+1 -1
View File
@@ -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()}"