1794 lines
67 KiB
Python
1794 lines
67 KiB
Python
import asyncio
|
||
import time
|
||
import uuid
|
||
|
||
from datetime import datetime, timedelta, timezone
|
||
from core.settings.tariffs_config import normalize_tariff_config
|
||
|
||
import pytz
|
||
|
||
from aiogram import F, Router, types
|
||
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 (
|
||
delete_key,
|
||
delete_user_data,
|
||
get_key_details,
|
||
get_servers,
|
||
get_tariff_by_id,
|
||
get_tariffs_for_cluster,
|
||
mark_key_as_frozen,
|
||
mark_key_as_unfrozen,
|
||
update_key_expiry,
|
||
)
|
||
from database.models import Key, Server, Tariff
|
||
from filters.admin import IsAdminFilter
|
||
from handlers.keys.operations import (
|
||
create_key_on_cluster,
|
||
delete_key_from_cluster,
|
||
get_user_traffic,
|
||
renew_key_in_cluster,
|
||
reset_traffic_in_cluster,
|
||
toggle_client_on_cluster,
|
||
update_subscription,
|
||
)
|
||
from handlers.utils import generate_random_email, handle_error
|
||
from hooks.hook_buttons import insert_hook_buttons
|
||
from hooks.processors import process_admin_key_edit_menu
|
||
from logger import logger
|
||
from panels.remnawave import RemnawaveAPI
|
||
|
||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn, build_admin_back_kb
|
||
from .keyboard import (
|
||
AdminUserEditorCallback,
|
||
AdminUserKeyEditorCallback,
|
||
build_cluster_selection_kb,
|
||
build_editor_kb,
|
||
build_key_delete_kb,
|
||
build_key_edit_kb,
|
||
build_reissue_menu_kb,
|
||
build_user_delete_kb,
|
||
build_users_key_expiry_kb,
|
||
build_users_key_show_kb,
|
||
)
|
||
from .users_states import RenewTariffState, UserEditorState
|
||
|
||
|
||
MOSCOW_TZ = pytz.timezone("Europe/Moscow")
|
||
|
||
router = Router()
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_key_edit"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_key_edit(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback,
|
||
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()
|
||
|
||
if not key_obj:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Информация о подписке не найдена.",
|
||
reply_markup=build_editor_kb(callback_data.tg_id),
|
||
)
|
||
return
|
||
|
||
key_value = key_obj.key or key_obj.remnawave_link or "—"
|
||
alias_part = f" (<i>{key_obj.alias}</i>)" if key_obj.alias else ""
|
||
|
||
if key_obj.created_at:
|
||
created_at_dt = datetime.fromtimestamp(int(key_obj.created_at) / 1000) + timedelta(hours=3)
|
||
created_at = created_at_dt.strftime("%d %B %Y года %H:%M")
|
||
else:
|
||
created_at = "—"
|
||
|
||
if key_obj.expiry_time:
|
||
expiry_dt = datetime.fromtimestamp(int(key_obj.expiry_time) / 1000)
|
||
expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M")
|
||
else:
|
||
expiry_date = "—"
|
||
|
||
tariff_name = "—"
|
||
subgroup_title = "—"
|
||
group_code = "—"
|
||
base_devices = None
|
||
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])
|
||
|
||
devices_line = ""
|
||
traffic_line = ""
|
||
if is_configurable:
|
||
sel_dev, cur_dev = key_obj.selected_device_limit, key_obj.current_device_limit
|
||
if sel_dev is not None or cur_dev is not None:
|
||
base_dev = sel_dev if sel_dev is not None else (base_devices if base_devices is not None else cur_dev)
|
||
extra = (
|
||
f" + {cur_dev - base_dev} (докуплено)"
|
||
if (base_dev is not None and cur_dev is not None and cur_dev > base_dev)
|
||
else ""
|
||
)
|
||
devices_line = f"📱 <b>Устройства:</b> {base_dev}{extra}\n"
|
||
|
||
sel_traf, cur_traf = key_obj.selected_traffic_limit, key_obj.current_traffic_limit
|
||
if sel_traf is not None or cur_traf is not None:
|
||
base_traf = sel_traf if sel_traf is not None else (base_traffic if base_traffic is not None else cur_traf)
|
||
extra = (
|
||
f" + {cur_traf - base_traf} ГБ (докуплено)"
|
||
if (base_traf is not None and cur_traf is not None and cur_traf > base_traf)
|
||
else ""
|
||
)
|
||
traffic_line = f"📊 <b>Трафик:</b> {base_traf} ГБ{extra}\n"
|
||
|
||
text = (
|
||
"<b>🔑 Информация о подписке</b>\n\n"
|
||
"<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> {key_obj.server_id or '—'}\n"
|
||
f"🆔 <b>ID клиента:</b> {key_obj.tg_id or '—'}\n"
|
||
f"🏷️ <b>Тарифная группа:</b> {group_code}\n"
|
||
f"📁 <b>Подгруппа:</b> {subgroup_title}\n"
|
||
f"📦 <b>Тариф:</b> {tariff_name}\n"
|
||
f"{devices_line}"
|
||
f"{traffic_line}"
|
||
"</blockquote>"
|
||
)
|
||
|
||
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_builder = InlineKeyboardBuilder.from_markup(kb_markup)
|
||
hook_buttons = await process_admin_key_edit_menu(
|
||
email=email,
|
||
session=session,
|
||
client_id=key_obj.client_id,
|
||
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(),
|
||
)
|
||
else:
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=await build_users_key_expiry_kb(session, callback_data.tg_id, email),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_change_expiry(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
await callback_query.message.edit_reply_markup(reply_markup=await build_users_key_expiry_kb(session, tg_id, email))
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserKeyEditorCallback.filter(F.action == "add"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_expiry_add(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserKeyEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
days = callback_data.month
|
||
|
||
key_details = await get_key_details(session, email)
|
||
|
||
if not key_details:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Информация о ключе не найдена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
if days:
|
||
await change_expiry_time(key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session)
|
||
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.set_state(UserEditorState.waiting_for_expiry_time)
|
||
|
||
await callback_query.message.edit_text(
|
||
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
|
||
reply_markup=build_users_key_show_kb(tg_id, email),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserKeyEditorCallback.filter(F.action == "take"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_expiry_take(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserKeyEditorCallback,
|
||
state: FSMContext,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
await state.update_data(tg_id=tg_id, email=email, 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),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserKeyEditorCallback.filter(F.action == "set"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_expiry_set(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserKeyEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
key_details = await get_key_details(session, email)
|
||
|
||
if not key_details:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Информация о ключе не найдена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
await state.update_data(tg_id=tg_id, email=email, op_type="set")
|
||
await state.set_state(UserEditorState.waiting_for_expiry_time)
|
||
|
||
text = (
|
||
"✍️ Введите новое время действия ключа:"
|
||
"\n\n📌 Формат: <b>год-месяц-день час:минута</b>"
|
||
f"\n\n📄 Текущая дата: {datetime.fromtimestamp(key_details['expiry_time'] / 1000).strftime('%Y-%m-%d %H:%M')}"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_users_key_show_kb(tg_id, email),
|
||
)
|
||
|
||
|
||
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
|
||
async def handle_expiry_time_input(message: Message, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tg_id = data.get("tg_id")
|
||
email = data.get("email")
|
||
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),
|
||
)
|
||
return
|
||
|
||
key_details = await get_key_details(session, email)
|
||
|
||
if not key_details:
|
||
await message.answer(
|
||
text="🚫 Информация о ключе не найдена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
try:
|
||
current_expiry_time = datetime.fromtimestamp(
|
||
key_details["expiry_time"] / 1000,
|
||
tz=MOSCOW_TZ,
|
||
)
|
||
|
||
if op_type == "add":
|
||
days = int(message.text)
|
||
new_expiry_time = current_expiry_time + timedelta(days=days)
|
||
text = f"✅ Ко времени действия ключа добавлено <b>{days} дн.</b>"
|
||
elif op_type == "take":
|
||
days = int(message.text)
|
||
new_expiry_time = current_expiry_time - timedelta(days=days)
|
||
text = f"✅ Из времени действия ключа вычтено <b>{days} дн.</b>"
|
||
else:
|
||
new_expiry_time = datetime.strptime(message.text, "%Y-%m-%d %H:%M")
|
||
new_expiry_time = MOSCOW_TZ.localize(new_expiry_time)
|
||
text = f"✅ Время действия ключа изменено на <b>{message.text} (МСК)</b>"
|
||
|
||
new_expiry_timestamp = int(new_expiry_time.timestamp() * 1000)
|
||
await change_expiry_time(new_expiry_timestamp, email, session)
|
||
except ValueError:
|
||
text = "🚫 Пожалуйста, используйте корректный формат даты (ГГГГ-ММ-ДД ЧЧ:ММ)!"
|
||
except Exception as e:
|
||
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
|
||
|
||
await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email))
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_reissue_menu"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_reissue_menu(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
text = (
|
||
"<b>🔄 Перевыпуск подписки</b>\n\n"
|
||
"<b>📦 Полный перевыпуск</b>\n"
|
||
"<i>Пересоздаёт подписку на сервере с возможностью выбора кластера. "
|
||
"Используйте для переноса на другой сервер или обновления данных.</i>\n\n"
|
||
"<b>🔗 Сменить ссылку</b>\n"
|
||
"<i>Генерирует новую ссылку подписки. Старая ссылка перестанет работать. "
|
||
"Все данные подписки сохранятся.</i>"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_reissue_menu_kb(email, tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_update_key"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_update_key(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"📡 Выберите кластер, на котором пересоздать ключ <b>{email}</b>:",
|
||
reply_markup=await build_cluster_selection_kb(
|
||
session,
|
||
tg_id,
|
||
email,
|
||
action="confirm_admin_key_reissue",
|
||
),
|
||
)
|
||
|
||
|
||
@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 = int(tg_id)
|
||
|
||
try:
|
||
servers = await get_servers(session)
|
||
cluster_servers = servers.get(cluster_id, [])
|
||
|
||
tariffs = await get_tariffs_for_cluster(session, cluster_id)
|
||
if not tariffs:
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔗 Привязать тариф",
|
||
callback_data=AdminPanelCallback(action="clusters").pack(),
|
||
)
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(
|
||
action="users_key_edit",
|
||
tg_id=tg_id,
|
||
data=email,
|
||
).pack(),
|
||
)
|
||
)
|
||
await callback_query.message.edit_text(
|
||
f"🚫 <b>Невозможно пересоздать подписку</b>\n\n"
|
||
f"📊 <b>Информация о кластере:</b>\n<blockquote>"
|
||
f"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
|
||
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
|
||
f"💡 <b>Привяжите тариф к кластеру</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
return
|
||
|
||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||
|
||
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)
|
||
builder = InlineKeyboardBuilder()
|
||
for country in sorted(unique_countries):
|
||
builder.button(
|
||
text=country,
|
||
callback_data=f"admin_reissue_country|{tg_id}|{email}|{country}",
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="Назад",
|
||
callback_data=f"users_key_edit|{email}",
|
||
)
|
||
)
|
||
await callback_query.message.edit_text(
|
||
"🌍 Выберите сервер (страну) для пересоздания подписки:",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
return
|
||
|
||
result = await session.execute(select(Key.remnawave_link).where(Key.email == email))
|
||
remnawave_link = result.scalar_one_or_none()
|
||
|
||
await update_subscription(
|
||
tg_id,
|
||
email,
|
||
session,
|
||
cluster_override=cluster_id,
|
||
remnawave_link=remnawave_link,
|
||
)
|
||
|
||
await handle_key_edit(
|
||
callback_query,
|
||
AdminUserEditorCallback(tg_id=tg_id, data=email, action="view_key"),
|
||
session,
|
||
True,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при перевыпуске ключа {email}: {e}")
|
||
await callback_query.message.answer(f"❗ Ошибка: {e}")
|
||
|
||
|
||
@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 = int(tg_id)
|
||
|
||
try:
|
||
data = await state.get_data()
|
||
cluster_id = data.get("cluster_id")
|
||
|
||
if cluster_id:
|
||
tariffs = await get_tariffs_for_cluster(session, cluster_id)
|
||
if not tariffs:
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔗 Привязать тариф",
|
||
callback_data=AdminPanelCallback(action="clusters").pack(),
|
||
)
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(
|
||
action="users_key_edit",
|
||
tg_id=tg_id,
|
||
data=email,
|
||
).pack(),
|
||
)
|
||
)
|
||
await callback_query.message.edit_text(
|
||
f"🚫 <b>Невозможно пересоздать подписку</b>\n\n"
|
||
f"📊 <b>Информация о кластере:</b>\n<blockquote>"
|
||
f"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
|
||
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
|
||
f"💡 <b>Привяжите тариф к кластеру</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
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)
|
||
|
||
await update_subscription(
|
||
tg_id=tg_id,
|
||
email=email,
|
||
session=session,
|
||
country_override=country,
|
||
remnawave_link=remnawave_link,
|
||
)
|
||
|
||
await handle_key_edit(
|
||
callback_query,
|
||
AdminUserEditorCallback(tg_id=tg_id, data=email, action="view_key"),
|
||
session,
|
||
True,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при перевыпуске ключа для страны {country}: {e}")
|
||
await callback_query.message.answer(f"❗ Ошибка: {e}")
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_recreate_key"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_recreate_key_start(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
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()
|
||
|
||
if not key_obj:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Ключ не найден.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
tariff_name = "—"
|
||
if key_obj.tariff_id:
|
||
tariff = await get_tariff_by_id(session, key_obj.tariff_id)
|
||
if tariff:
|
||
tariff_name = tariff.get("name", "—")
|
||
|
||
text = (
|
||
"<b>🔁 Пересоздание ссылки подписки</b>\n\n"
|
||
f"📦 <b>Тариф:</b> {tariff_name}\n\n"
|
||
"⚠️ <b>Будет сгенерирована новая ссылка подписки.</b>\n"
|
||
"Старая ссылка перестанет работать.\n\n"
|
||
"✅ <i>Все данные подписки сохранятся.</i>"
|
||
)
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="✅ Пересоздать",
|
||
callback_data=f"confirm_recreate|{tg_id}|{email}",
|
||
)
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
|
||
)
|
||
)
|
||
|
||
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
|
||
|
||
|
||
@router.callback_query(F.data.startswith("confirm_recreate|"), IsAdminFilter())
|
||
async def handle_recreate_key_confirm(
|
||
callback_query: CallbackQuery,
|
||
session: AsyncSession,
|
||
):
|
||
_, tg_id, old_email = 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()
|
||
|
||
if not key_obj:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Ключ не найден.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
await callback_query.message.edit_text("⏳ Пересоздание ссылки подписки...")
|
||
|
||
client_id = key_obj.client_id
|
||
cluster_id = key_obj.server_id
|
||
old_link = key_obj.remnawave_link or key_obj.key
|
||
|
||
servers = await get_servers(session)
|
||
cluster = servers.get(cluster_id)
|
||
|
||
if not cluster:
|
||
for _, server_list in servers.items():
|
||
for server_info in server_list:
|
||
if server_info.get("server_name", "").lower() == cluster_id.lower():
|
||
cluster = [server_info]
|
||
break
|
||
if cluster:
|
||
break
|
||
|
||
if not cluster:
|
||
await callback_query.message.edit_text(
|
||
text=f"❗ Кластер {cluster_id} не найден.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
remnawave_servers = [s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave"]
|
||
|
||
if not remnawave_servers:
|
||
await callback_query.message.edit_text(
|
||
text="❗ Revoke доступен только для Remnawave. Для 3x-ui используйте перевыпуск.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
api_url = remnawave_servers[0].get("api_url")
|
||
if not api_url:
|
||
await callback_query.message.edit_text(
|
||
text="❗ У Remnawave сервера не задан api_url.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
api = RemnawaveAPI(api_url)
|
||
try:
|
||
if not REMNAWAVE_TOKEN_LOGIN_ENABLED:
|
||
await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
|
||
|
||
user_data = await api.revoke_user_subscription(client_id)
|
||
finally:
|
||
await api.aclose()
|
||
|
||
if not user_data:
|
||
await callback_query.message.edit_text(
|
||
text="❗ Не удалось выполнить revoke. Проверьте логи.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
new_link = user_data.get("subscriptionUrl")
|
||
|
||
if not new_link:
|
||
await callback_query.message.edit_text(
|
||
text="❗ Revoke выполнен, но новая ссылка не получена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
await session.execute(
|
||
update(Key)
|
||
.where(Key.email == old_email)
|
||
.values(
|
||
key=new_link,
|
||
remnawave_link=new_link,
|
||
)
|
||
)
|
||
await session.commit()
|
||
|
||
try:
|
||
user_text = (
|
||
"🔄 <b>Ваша подписка была перевыпущена</b>\n\n"
|
||
f"🔗 <b>Новая ссылка подписки:</b>\n<code>{new_link}</code>\n\n"
|
||
"<i>Старая ссылка больше не работает.</i>"
|
||
)
|
||
user_kb = InlineKeyboardBuilder()
|
||
user_kb.row(
|
||
InlineKeyboardButton(
|
||
text="📱 Мои подписки",
|
||
callback_data="view_keys",
|
||
)
|
||
)
|
||
user_kb.row(
|
||
InlineKeyboardButton(
|
||
text="👤 Личный кабинет",
|
||
callback_data="profile",
|
||
)
|
||
)
|
||
|
||
await callback_query.bot.send_message(
|
||
chat_id=tg_id,
|
||
text=user_text,
|
||
reply_markup=user_kb.as_markup(),
|
||
)
|
||
notification_sent = True
|
||
except Exception as e:
|
||
logger.warning(f"Не удалось отправить уведомление клиенту {tg_id}: {e}")
|
||
notification_sent = False
|
||
|
||
text = (
|
||
"✅ <b>Ссылка подписки пересоздана</b>\n\n"
|
||
f"🔗 <b>Старая ссылка:</b>\n<code>{old_link}</code>\n\n"
|
||
f"🔗 <b>Новая ссылка:</b>\n<code>{new_link}</code>\n\n"
|
||
)
|
||
if notification_sent:
|
||
text += "📨 <i>Клиент уведомлён о новой ссылке.</i>"
|
||
else:
|
||
text += "⚠️ <i>Не удалось уведомить клиента.</i>"
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(
|
||
action="users_key_edit",
|
||
tg_id=tg_id,
|
||
data=old_email,
|
||
).pack(),
|
||
)
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при revoke ключа {old_email}: {e}")
|
||
await callback_query.message.edit_text(
|
||
text=f"❗ Ошибка при пересоздании: {e}",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_delete_key(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
email = callback_data.data
|
||
|
||
result = await session.execute(select(Key.client_id).where(Key.email == email))
|
||
client_id = result.scalar_one_or_none()
|
||
|
||
if client_id is None:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Ключ не найден!",
|
||
reply_markup=build_editor_kb(callback_data.tg_id),
|
||
)
|
||
return
|
||
|
||
await state.set_state(UserEditorState.confirm_delete_key)
|
||
await state.update_data(delete_key_email=email, delete_key_tg_id=int(callback_data.tg_id))
|
||
|
||
await callback_query.message.edit_text(
|
||
text="❓ Вы уверены, что хотите удалить ключ?",
|
||
reply_markup=build_key_delete_kb(callback_data.tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
|
||
UserEditorState.confirm_delete_key,
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_delete_key_confirm(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
email = data.get("delete_key_email")
|
||
expected_tg_id = data.get("delete_key_tg_id")
|
||
await state.clear()
|
||
|
||
if not email or int(expected_tg_id or 0) != int(callback_data.tg_id):
|
||
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()
|
||
|
||
kb = build_editor_kb(callback_data.tg_id)
|
||
|
||
if client_id:
|
||
clusters = await get_servers(session=session)
|
||
|
||
async def delete_key_from_servers():
|
||
tasks = []
|
||
for cluster_name, cluster_servers in clusters.items():
|
||
for _ in cluster_servers:
|
||
tasks.append(delete_key_from_cluster(cluster_name, email, client_id, session))
|
||
await asyncio.gather(*tasks, return_exceptions=True)
|
||
|
||
await delete_key_from_servers()
|
||
await delete_key(session, client_id)
|
||
|
||
await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb)
|
||
else:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Ключ не найден или уже удален.",
|
||
reply_markup=kb,
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_delete_user"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_delete_user(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
await callback_query.message.edit_text(
|
||
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
|
||
reply_markup=build_user_delete_kb(tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_delete_user_confirm(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
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()
|
||
|
||
async def delete_keys_from_servers():
|
||
try:
|
||
tasks = []
|
||
servers = await get_servers(session=session)
|
||
for email, client_id in key_records:
|
||
for cluster_id, _cluster in servers.items():
|
||
tasks.append(delete_key_from_cluster(cluster_id, email, client_id, session))
|
||
await asyncio.gather(*tasks, return_exceptions=True)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
|
||
|
||
await delete_keys_from_servers()
|
||
|
||
try:
|
||
await delete_user_data(session, tg_id)
|
||
await callback_query.message.edit_text(
|
||
text=f"🗑️ Пользователь с ID {tg_id} был удален.",
|
||
reply_markup=build_admin_back_kb(),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова.",
|
||
reply_markup=build_admin_back_kb(),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_traffic"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_user_traffic(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = callback_data.data
|
||
|
||
await callback_query.message.edit_text("⏳ Получаем данные о трафике, пожалуйста, подождите...")
|
||
|
||
traffic_data = await get_user_traffic(session, tg_id, email)
|
||
|
||
if traffic_data["status"] == "error":
|
||
await callback_query.message.edit_text(
|
||
traffic_data["message"],
|
||
reply_markup=build_editor_kb(tg_id, True),
|
||
)
|
||
return
|
||
|
||
total_traffic = 0
|
||
result_text = f"📊 <b>Трафик подписки {email}:</b>\n\n"
|
||
|
||
for server, traffic in traffic_data["traffic"].items():
|
||
if isinstance(traffic, str):
|
||
result_text += f"❌ {server}: {traffic}\n"
|
||
else:
|
||
result_text += f"🌍 {server}: <b>{traffic} ГБ</b>\n"
|
||
total_traffic += traffic
|
||
|
||
result_text += f"\n🔢 <b>Общий трафик:</b> {total_traffic:.2f} ГБ"
|
||
|
||
await callback_query.message.edit_text(
|
||
result_text,
|
||
reply_markup=build_editor_kb(tg_id, True),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_create_key"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_create_key_start(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
await state.update_data(tg_id=tg_id)
|
||
|
||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||
|
||
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()]
|
||
|
||
if not countries:
|
||
await callback_query.message.edit_text(
|
||
"❌ Нет доступных стран для создания ключа.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
for country in countries:
|
||
builder.button(text=country, callback_data=country)
|
||
builder.adjust(1)
|
||
builder.row(build_admin_back_btn())
|
||
|
||
await callback_query.message.edit_text(
|
||
"🌍 <b>Выберите страну для создания ключа:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
return
|
||
|
||
await state.set_state(UserEditorState.selecting_cluster)
|
||
|
||
servers = await get_servers(session=session)
|
||
cluster_names = list(servers.keys())
|
||
|
||
if not cluster_names:
|
||
await callback_query.message.edit_text(
|
||
"❌ Нет доступных кластеров для создания ключа.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
for cluster in cluster_names:
|
||
builder.button(text=f"🌐 {cluster}", callback_data=cluster)
|
||
builder.adjust(2)
|
||
builder.row(build_admin_back_btn())
|
||
|
||
await callback_query.message.edit_text(
|
||
"🌐 <b>Выберите кластер для создания ключа:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(UserEditorState.selecting_country, IsAdminFilter())
|
||
async def handle_create_key_country(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
country = callback_query.data
|
||
await state.update_data(country=country)
|
||
await state.set_state(UserEditorState.selecting_duration)
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
result = await session.execute(select(Server.cluster_name).where(Server.server_name == country))
|
||
row = result.mappings().first()
|
||
|
||
if not row:
|
||
await callback_query.message.edit_text("❌ Сервер не найден.")
|
||
return
|
||
|
||
cluster_name = row["cluster_name"]
|
||
await state.update_data(cluster_name=cluster_name)
|
||
|
||
tariffs = await get_tariffs_for_cluster(session, cluster_name)
|
||
|
||
for tariff in tariffs:
|
||
if tariff["duration_days"] < 1:
|
||
continue
|
||
builder.button(
|
||
text=f"{tariff['name']} — {tariff['price_rub']}₽",
|
||
callback_data=f"tariff_{tariff['id']}",
|
||
)
|
||
|
||
builder.adjust(1)
|
||
builder.row(build_admin_back_btn())
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"🕒 <b>Выберите срок действия ключа для страны <code>{country}</code>:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(UserEditorState.selecting_cluster, IsAdminFilter())
|
||
async def handle_create_key_cluster(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
cluster_name = callback_query.data
|
||
|
||
data = await state.get_data()
|
||
tg_id = data.get("tg_id")
|
||
|
||
if not tg_id:
|
||
await callback_query.message.edit_text("❌ Ошибка: tg_id клиента не найден.")
|
||
return
|
||
|
||
await state.update_data(cluster_name=cluster_name)
|
||
await state.set_state(UserEditorState.selecting_duration)
|
||
|
||
tariffs = await get_tariffs_for_cluster(session, cluster_name)
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
for tariff in tariffs:
|
||
if tariff["duration_days"] < 1:
|
||
continue
|
||
builder.button(
|
||
text=f"{tariff['name']} — {tariff['price_rub']}₽",
|
||
callback_data=f"tariff_{tariff['id']}",
|
||
)
|
||
|
||
builder.adjust(1)
|
||
builder.row(build_admin_back_btn())
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"🕒 <b>Выберите срок действия ключа для кластера <code>{cluster_name}</code>:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(UserEditorState.selecting_duration, IsAdminFilter())
|
||
async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tg_id = data.get("tg_id", callback_query.from_user.id)
|
||
|
||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||
|
||
try:
|
||
if not callback_query.data.startswith("tariff_"):
|
||
raise ValueError("Некорректный callback_data")
|
||
tariff_id = int(callback_query.data.replace("tariff_", ""))
|
||
|
||
tariff = await get_tariff_by_id(session, tariff_id)
|
||
if not tariff:
|
||
raise ValueError("Тариф не найден.")
|
||
|
||
duration_days = tariff["duration_days"]
|
||
client_id = str(uuid.uuid4())
|
||
email = await generate_random_email(session=session)
|
||
expiry = datetime.now(tz=timezone.utc) + timedelta(days=duration_days)
|
||
expiry_ms = int(expiry.timestamp() * 1000)
|
||
|
||
if use_country_selection and "country" in data:
|
||
country = data["country"]
|
||
await create_key_on_cluster(
|
||
country,
|
||
tg_id,
|
||
client_id,
|
||
email,
|
||
expiry_ms,
|
||
plan=tariff_id,
|
||
session=session,
|
||
)
|
||
|
||
await state.clear()
|
||
await callback_query.message.edit_text(
|
||
f"✅ Ключ успешно создан для страны <b>{country}</b> на {duration_days} дней.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
elif "cluster_name" in data:
|
||
cluster_name = data["cluster_name"]
|
||
await create_key_on_cluster(
|
||
cluster_name,
|
||
tg_id,
|
||
client_id,
|
||
email,
|
||
expiry_ms,
|
||
plan=tariff_id,
|
||
session=session,
|
||
)
|
||
|
||
await state.clear()
|
||
await callback_query.message.edit_text(
|
||
f"✅ Ключ успешно создан в кластере <b>{cluster_name}</b> на {duration_days} дней.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
else:
|
||
await callback_query.message.edit_text("❌ Не удалось определить источник — страна или кластер.")
|
||
except Exception as e:
|
||
logger.error(f"[CreateKey] Ошибка при создании ключа: {e}")
|
||
await callback_query.message.edit_text(
|
||
"❌ Не удалось создать ключ. Попробуйте позже.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_reset_traffic"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_reset_traffic(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
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:
|
||
await callback_query.message.edit_text(
|
||
"❌ Ключ не найден в базе данных.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
cluster_id, _client_id = record
|
||
|
||
try:
|
||
await reset_traffic_in_cluster(cluster_id, email, session)
|
||
await callback_query.message.edit_text(
|
||
f"✅ Трафик для ключа <b>{email}</b> успешно сброшен.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сбросе трафика: {e}")
|
||
await callback_query.message.edit_text(
|
||
"❌ Произошла ошибка при сбросе трафика. Попробуйте позже.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_freeze"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_admin_freeze_subscription(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = str(callback_data.data)
|
||
|
||
try:
|
||
record = await get_key_details(session, email)
|
||
if not record:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Информация о ключе не найдена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
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":
|
||
text_error = (
|
||
f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
|
||
)
|
||
await callback_query.message.edit_text(
|
||
text_error,
|
||
reply_markup=build_editor_kb(tg_id, True),
|
||
)
|
||
return
|
||
|
||
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()
|
||
|
||
await callback_query.answer("✅ Подписка заморожена")
|
||
|
||
await handle_key_edit(
|
||
callback_query=callback_query,
|
||
callback_data=callback_data,
|
||
session=session,
|
||
update=False,
|
||
)
|
||
except Exception as e:
|
||
await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_unfreeze"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_admin_unfreeze_subscription(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
session: AsyncSession,
|
||
):
|
||
tg_id = callback_data.tg_id
|
||
email = str(callback_data.data)
|
||
|
||
try:
|
||
record = await get_key_details(session, email)
|
||
if not record:
|
||
await callback_query.message.edit_text(
|
||
text="🚫 Информация о ключе не найдена.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
return
|
||
|
||
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":
|
||
text_error = (
|
||
f"Произошла ошибка при включении подписки.\nДетали: {result.get('error') or result.get('results')}"
|
||
)
|
||
await callback_query.message.edit_text(
|
||
text_error,
|
||
reply_markup=build_editor_kb(tg_id, True),
|
||
)
|
||
return
|
||
|
||
tariff = await get_tariff_by_id(session, record["tariff_id"]) if record.get("tariff_id") else None
|
||
if not tariff:
|
||
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)
|
||
|
||
if record.get("current_traffic_limit") is not None:
|
||
total_gb = record["current_traffic_limit"]
|
||
if record.get("current_device_limit") is not None:
|
||
hwid_limit = record["current_device_limit"]
|
||
|
||
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 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"),
|
||
)
|
||
|
||
await callback_query.answer("✅ Подписка разморожена")
|
||
|
||
await handle_key_edit(
|
||
callback_query=callback_query,
|
||
callback_data=callback_data,
|
||
session=session,
|
||
update=False,
|
||
)
|
||
except Exception as e:
|
||
await handle_error(tg_id, callback_query, f"Ошибка при включении подписки: {e}")
|
||
|
||
|
||
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:
|
||
return ValueError(f"User with email {email} was not found")
|
||
|
||
client_id, tariff_id, server_id, key_device_limit, key_traffic_limit = row
|
||
if server_id is None:
|
||
return ValueError(f"Key with client_id {client_id} was not found")
|
||
|
||
traffic_limit = 0
|
||
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()
|
||
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]
|
||
|
||
if key_device_limit is not None:
|
||
device_limit = key_device_limit
|
||
if key_traffic_limit is not None:
|
||
traffic_limit = key_traffic_limit
|
||
|
||
servers = await get_servers(session=session)
|
||
|
||
if server_id in servers:
|
||
target_cluster = server_id
|
||
else:
|
||
target_cluster = None
|
||
for cluster_name, cluster_servers in servers.items():
|
||
if any(s.get("server_name") == server_id for s in cluster_servers):
|
||
target_cluster = cluster_name
|
||
break
|
||
|
||
if not target_cluster:
|
||
return ValueError(f"No suitable cluster found for server {server_id}")
|
||
|
||
await renew_key_in_cluster(
|
||
cluster_id=target_cluster,
|
||
email=email,
|
||
client_id=client_id,
|
||
new_expiry_time=expiry_time,
|
||
total_gb=traffic_limit,
|
||
session=session,
|
||
hwid_device_limit=device_limit,
|
||
reset_traffic=False,
|
||
target_subgroup=key_subgroup,
|
||
old_subgroup=key_subgroup,
|
||
plan=tariff_id,
|
||
)
|
||
|
||
await update_key_expiry(session, client_id, expiry_time)
|
||
return None
|
||
|
||
|
||
@router.callback_query(
|
||
AdminUserEditorCallback.filter(F.action == "users_edit_config"),
|
||
IsAdminFilter(),
|
||
)
|
||
async def handle_edit_config_start(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminUserEditorCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
email = 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()
|
||
|
||
if not key_obj:
|
||
await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
|
||
return
|
||
|
||
if not key_obj.tariff_id:
|
||
await callback_query.message.edit_text(
|
||
"❌ У ключа не назначен тариф.",
|
||
reply_markup=build_key_edit_kb(key_obj.__dict__, email),
|
||
)
|
||
return
|
||
|
||
tariff = await get_tariff_by_id(session, key_obj.tariff_id)
|
||
if not tariff or not tariff.get("configurable"):
|
||
await callback_query.message.edit_text(
|
||
"❌ Тариф не поддерживает конфигурацию.",
|
||
reply_markup=build_key_edit_kb(key_obj.__dict__, email),
|
||
)
|
||
return
|
||
|
||
base_devices = key_obj.selected_device_limit or tariff.get("device_limit") or 1
|
||
current_devices = key_obj.current_device_limit or base_devices
|
||
extra_devices = max(0, current_devices - base_devices)
|
||
|
||
base_traffic = key_obj.selected_traffic_limit
|
||
current_traffic = key_obj.current_traffic_limit
|
||
extra_traffic = max(0, (current_traffic or 0) - (base_traffic or 0)) if current_traffic and base_traffic else 0
|
||
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await state.update_data(
|
||
email=email,
|
||
tg_id=tg_id,
|
||
tariff_id=key_obj.tariff_id,
|
||
cfg_base_devices=base_devices,
|
||
cfg_extra_devices=extra_devices,
|
||
cfg_base_traffic=base_traffic,
|
||
cfg_extra_traffic=extra_traffic,
|
||
)
|
||
|
||
await render_config_menu(callback_query, state, session)
|
||
|
||
|
||
async def render_config_menu(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
email = data.get("email")
|
||
tg_id = data.get("tg_id")
|
||
tariff_id = data.get("tariff_id")
|
||
|
||
tariff = await get_tariff_by_id(session, tariff_id)
|
||
if not tariff:
|
||
await callback_query.message.edit_text("❌ Тариф не найден.")
|
||
await state.clear()
|
||
return
|
||
|
||
base_devices = data.get("cfg_base_devices") or 1
|
||
extra_devices = data.get("cfg_extra_devices") or 0
|
||
base_traffic = data.get("cfg_base_traffic")
|
||
extra_traffic = data.get("cfg_extra_traffic") or 0
|
||
|
||
text = (
|
||
f"<b>⚙️ Конфигурация ключа</b>\n\n"
|
||
f"🔑 <b>Ключ:</b> <code>{email}</code>\n"
|
||
f"📦 <b>Тариф:</b> {tariff.get('name')}\n\n"
|
||
)
|
||
|
||
extra_dev_str = f" + {extra_devices} (докуплено)" if extra_devices > 0 else ""
|
||
text += f"📱 <b>Устройства:</b> {base_devices}{extra_dev_str}\n"
|
||
|
||
if base_traffic:
|
||
extra_traf_str = f" + {extra_traffic} ГБ (докуплено)" if extra_traffic > 0 else ""
|
||
text += f"📊 <b>Трафик:</b> {base_traffic} ГБ{extra_traf_str}\n"
|
||
else:
|
||
text += "📊 <b>Трафик:</b> безлимит\n"
|
||
|
||
text += "\n<i>Выберите что редактировать:</i>"
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(text="📦 Тариф (база)", callback_data="cfg_edit_base"),
|
||
InlineKeyboardButton(text="➕ Докупка", callback_data="cfg_edit_addon"),
|
||
)
|
||
builder.row(InlineKeyboardButton(text="💾 Сохранить", callback_data="cfg_save"))
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
|
||
)
|
||
)
|
||
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_edit_base", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_edit_base(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tariff = await get_tariff_by_id(session, data.get("tariff_id"))
|
||
device_options = tariff.get("device_options") or [] if tariff else []
|
||
traffic_options = tariff.get("traffic_options_gb") or [] if tariff else []
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
if device_options:
|
||
builder.row(InlineKeyboardButton(text="📱 Устройства", callback_data="cfg_base_devices"))
|
||
if traffic_options:
|
||
builder.row(InlineKeyboardButton(text="📊 Трафик", callback_data="cfg_base_traffic"))
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu"))
|
||
|
||
await callback_query.message.edit_text(
|
||
"<b>📦 Редактирование базы тарифа</b>\n\nВыберите параметр:",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_edit_addon", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_edit_addon(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tariff = await get_tariff_by_id(session, data.get("tariff_id"))
|
||
device_options = tariff.get("device_options") or [] if tariff else []
|
||
traffic_options = tariff.get("traffic_options_gb") or [] if tariff else []
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
if device_options:
|
||
builder.row(InlineKeyboardButton(text="📱 Устройства", callback_data="cfg_addon_devices"))
|
||
if traffic_options:
|
||
builder.row(InlineKeyboardButton(text="📊 Трафик", callback_data="cfg_addon_traffic"))
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu"))
|
||
|
||
await callback_query.message.edit_text(
|
||
"<b>➕ Редактирование докупки</b>\n\nВыберите параметр:",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_back_menu", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_back_menu(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
await render_config_menu(callback_query, state, session)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_base_devices", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_base_devices(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tariff = await get_tariff_by_id(session, data.get("tariff_id"))
|
||
device_options = tariff.get("device_options") or [] if tariff else []
|
||
base_devices = data.get("cfg_base_devices") or 1
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
for opt in sorted(device_options):
|
||
mark = " ✅" if int(opt) == int(base_devices) else ""
|
||
builder.button(text=f"{opt} устр.{mark}", callback_data=f"cfg_set_base_dev:{opt}")
|
||
builder.adjust(3)
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu"))
|
||
|
||
await state.set_state(UserEditorState.config_select_base)
|
||
await state.update_data(cfg_param="devices")
|
||
await callback_query.message.edit_text(
|
||
"<b>📱 Выберите базу устройств:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_base_traffic", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_base_traffic(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
tariff = await get_tariff_by_id(session, data.get("tariff_id"))
|
||
traffic_options = tariff.get("traffic_options_gb") or [] if tariff else []
|
||
base_traffic = data.get("cfg_base_traffic")
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
for opt in sorted(traffic_options):
|
||
is_sel = (base_traffic is None and opt == 0) or (base_traffic is not None and int(opt) == int(base_traffic))
|
||
mark = " ✅" if is_sel else ""
|
||
label = "безлимит" if opt == 0 else f"{opt} ГБ"
|
||
builder.button(text=f"{label}{mark}", callback_data=f"cfg_set_base_traf:{opt}")
|
||
builder.adjust(2)
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="cfg_back_menu"))
|
||
|
||
await state.set_state(UserEditorState.config_select_base)
|
||
await state.update_data(cfg_param="traffic")
|
||
await callback_query.message.edit_text(
|
||
"<b>📊 Выберите базу трафика:</b>",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("cfg_set_base_dev:"), UserEditorState.config_select_base, IsAdminFilter())
|
||
async def handle_cfg_set_base_dev(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
base_devices = int(callback_query.data.split(":")[1])
|
||
await state.update_data(cfg_base_devices=base_devices)
|
||
await callback_query.answer(f"✅ База устройств: {base_devices}")
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await render_config_menu(callback_query, state, session)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("cfg_set_base_traf:"), UserEditorState.config_select_base, IsAdminFilter())
|
||
async def handle_cfg_set_base_traf(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
traffic_gb = int(callback_query.data.split(":")[1])
|
||
await state.update_data(cfg_base_traffic=traffic_gb if traffic_gb > 0 else None)
|
||
label = "безлимит" if traffic_gb == 0 else f"{traffic_gb} ГБ"
|
||
await callback_query.answer(f"✅ База трафика: {label}")
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await render_config_menu(callback_query, state, session)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_addon_devices", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_addon_devices(callback_query: CallbackQuery, state: FSMContext):
|
||
data = await state.get_data()
|
||
extra_devices = data.get("cfg_extra_devices") or 0
|
||
|
||
await state.set_state(UserEditorState.config_input_addon)
|
||
await state.update_data(cfg_param="devices")
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔙 Отмена", callback_data="cfg_cancel_input"))
|
||
|
||
await callback_query.message.edit_text(
|
||
f"<b>📱 Докупка устройств</b>\n\n"
|
||
f"Текущее значение: <b>{extra_devices}</b>\n\n"
|
||
f"Введите новое количество докупленных устройств (число):",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_addon_traffic", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_addon_traffic(callback_query: CallbackQuery, state: FSMContext):
|
||
data = await state.get_data()
|
||
extra_traffic = data.get("cfg_extra_traffic") or 0
|
||
|
||
await state.set_state(UserEditorState.config_input_addon)
|
||
await state.update_data(cfg_param="traffic")
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔙 Отмена", callback_data="cfg_cancel_input"))
|
||
|
||
await callback_query.message.edit_text(
|
||
f"<b>📊 Докупка трафика</b>\n\n"
|
||
f"Текущее значение: <b>{extra_traffic} ГБ</b>\n\n"
|
||
f"Введите новое количество докупленного трафика в ГБ (число):",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_cancel_input", UserEditorState.config_input_addon, IsAdminFilter())
|
||
async def handle_cfg_cancel_input(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await render_config_menu(callback_query, state, session)
|
||
|
||
|
||
@router.message(UserEditorState.config_input_addon, IsAdminFilter())
|
||
async def handle_cfg_input_addon(message: Message, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
param = data.get("cfg_param")
|
||
email = data.get("email")
|
||
tg_id = data.get("tg_id")
|
||
tariff_id = data.get("tariff_id")
|
||
|
||
if not message.text or not message.text.isdigit():
|
||
await message.answer("❌ Введите корректное число.")
|
||
return
|
||
|
||
value = int(message.text)
|
||
if value < 0:
|
||
await message.answer("❌ Значение не может быть отрицательным.")
|
||
return
|
||
|
||
if param == "devices":
|
||
await state.update_data(cfg_extra_devices=value)
|
||
else:
|
||
await state.update_data(cfg_extra_traffic=value)
|
||
|
||
await state.set_state(UserEditorState.config_menu)
|
||
|
||
data = await state.get_data()
|
||
tariff = await get_tariff_by_id(session, tariff_id)
|
||
|
||
base_devices = data.get("cfg_base_devices") or 1
|
||
extra_devices = data.get("cfg_extra_devices") or 0
|
||
base_traffic = data.get("cfg_base_traffic")
|
||
extra_traffic = data.get("cfg_extra_traffic") or 0
|
||
|
||
text = (
|
||
f"<b>⚙️ Конфигурация ключа</b>\n\n"
|
||
f"🔑 <b>Ключ:</b> <code>{email}</code>\n"
|
||
f"📦 <b>Тариф:</b> {tariff.get('name') if tariff else '—'}\n\n"
|
||
)
|
||
|
||
extra_dev_str = f" + {extra_devices} (докуплено)" if extra_devices > 0 else ""
|
||
text += f"📱 <b>Устройства:</b> {base_devices}{extra_dev_str}\n"
|
||
|
||
if base_traffic:
|
||
extra_traf_str = f" + {extra_traffic} ГБ (докуплено)" if extra_traffic > 0 else ""
|
||
text += f"📊 <b>Трафик:</b> {base_traffic} ГБ{extra_traf_str}\n"
|
||
else:
|
||
text += "📊 <b>Трафик:</b> безлимит\n"
|
||
|
||
text += "\n<i>Выберите что редактировать:</i>"
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(
|
||
InlineKeyboardButton(text="📦 Тариф (база)", callback_data="cfg_edit_base"),
|
||
InlineKeyboardButton(text="➕ Докупка", callback_data="cfg_edit_addon"),
|
||
)
|
||
builder.row(InlineKeyboardButton(text="💾 Сохранить", callback_data="cfg_save"))
|
||
builder.row(
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
|
||
)
|
||
)
|
||
|
||
await message.answer(text=text, reply_markup=builder.as_markup())
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_save", UserEditorState.config_menu, IsAdminFilter())
|
||
async def handle_cfg_save(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
data = await state.get_data()
|
||
email = data.get("email")
|
||
tg_id = data.get("tg_id")
|
||
tariff_id = data.get("tariff_id")
|
||
|
||
base_devices = data.get("cfg_base_devices") or 1
|
||
extra_devices = data.get("cfg_extra_devices") or 0
|
||
total_devices = base_devices + extra_devices
|
||
|
||
base_traffic = data.get("cfg_base_traffic")
|
||
extra_traffic = data.get("cfg_extra_traffic") or 0
|
||
total_traffic = (base_traffic + extra_traffic) if base_traffic else None
|
||
|
||
tariff = await get_tariff_by_id(session, tariff_id)
|
||
selected_price = None
|
||
if tariff:
|
||
base_price = tariff.get("price_rub") or 0
|
||
|
||
device_step = tariff.get("device_step_rub") or 0
|
||
tariff_base_devices = tariff.get("device_limit") or 1
|
||
extra_base_devices = max(0, base_devices - tariff_base_devices)
|
||
devices_extra_price = extra_base_devices * device_step
|
||
|
||
traffic_step = tariff.get("traffic_step_rub") or 0
|
||
tariff_base_traffic = tariff.get("traffic_limit") or 0
|
||
extra_base_traffic = max(0, (base_traffic or 0) - tariff_base_traffic) if base_traffic else 0
|
||
traffic_extra_price = extra_base_traffic * traffic_step
|
||
|
||
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()
|
||
|
||
if not key_obj:
|
||
await callback_query.message.edit_text("❌ Ключ не найден.", reply_markup=build_editor_kb(tg_id))
|
||
await state.clear()
|
||
return
|
||
|
||
try:
|
||
await renew_key_in_cluster(
|
||
cluster_id=key_obj.server_id,
|
||
email=email,
|
||
client_id=key_obj.client_id,
|
||
new_expiry_time=key_obj.expiry_time,
|
||
total_gb=total_traffic or 0,
|
||
session=session,
|
||
hwid_device_limit=total_devices,
|
||
reset_traffic=False,
|
||
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 session.commit()
|
||
|
||
await state.clear()
|
||
await callback_query.answer("✅ Конфигурация сохранена", show_alert=True)
|
||
|
||
callback_data_back = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id)
|
||
await handle_key_edit(
|
||
callback_query=callback_query,
|
||
callback_data=callback_data_back,
|
||
session=session,
|
||
update=False,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"[EditConfig] Ошибка при сохранении конфигурации: {e}")
|
||
await callback_query.message.edit_text(
|
||
"❌ Не удалось сохранить конфигурацию. Попробуйте позже.",
|
||
reply_markup=build_editor_kb(tg_id),
|
||
)
|
||
await state.clear()
|
||
|
||
|
||
@router.callback_query(F.data == "cfg_back_menu", IsAdminFilter())
|
||
async def handle_cfg_back_menu_any(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||
await state.set_state(UserEditorState.config_menu)
|
||
await render_config_menu(callback_query, state, session)
|