remove client subscription freeze / keep admin subscription toggle / update admin button labels
Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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 рефералов",
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 = "✏️"
|
||||
|
||||
# Реферальная система
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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(": ")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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()}"
|
||||
|
||||
Reference in New Issue
Block a user