changing the tariff through the admin panel without recreating it
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from aiogram import Router
|
||||
|
||||
from . import users_balance, users_bans, users_hwid, users_keys, users_manage
|
||||
from . import users_balance, users_bans, users_hwid, users_keys, users_manage, users_tariffs
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -9,3 +9,4 @@ router.include_router(users_balance.router)
|
||||
router.include_router(users_hwid.router)
|
||||
router.include_router(users_keys.router)
|
||||
router.include_router(users_bans.router)
|
||||
router.include_router(users_tariffs.router)
|
||||
|
||||
@@ -3,7 +3,7 @@ import time
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
from core.settings.tariffs_config import normalize_tariff_config
|
||||
|
||||
import pytz
|
||||
|
||||
@@ -178,140 +178,6 @@ async def handle_key_edit(
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back:renew", IsAdminFilter())
|
||||
async def handle_back_to_key_menu(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
data = await state.get_data()
|
||||
email = data["email"]
|
||||
tg_id = data["tg_id"]
|
||||
await state.clear()
|
||||
|
||||
callback_data = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id)
|
||||
await handle_key_edit(
|
||||
callback_query=callback_query,
|
||||
callback_data=callback_data,
|
||||
session=session,
|
||||
update=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminUserEditorCallback.filter(F.action == "users_renew"),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
async def handle_user_choose_tariff_group(
|
||||
callback_query: CallbackQuery,
|
||||
callback_data: AdminUserEditorCallback,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
email = callback_data.data
|
||||
tg_id = callback_data.tg_id
|
||||
|
||||
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()]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group_code in groups:
|
||||
builder.button(text=group_code, callback_data=f"group:{group_code}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:renew")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="📁 <b>Выберите тарифную группу:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("group:"), IsAdminFilter())
|
||||
async def handle_user_choose_tariff(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
group_code = callback_query.data.split(":", 1)[1]
|
||||
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()
|
||||
|
||||
if not tariffs:
|
||||
await callback_query.message.edit_text("❌ Нет активных тарифов в группе.")
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for tariff in tariffs:
|
||||
builder.button(text=f"{tariff.name} – {int(tariff.price_rub)}₽", callback_data=f"confirm:{tariff.id}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:group")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"📦 <b>Выберите тариф для группы <code>{group_code}</code>:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm:"), IsAdminFilter())
|
||||
async def handle_user_renew_confirm(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
tariff_id = int(callback_query.data.split(":")[1])
|
||||
data = await state.get_data()
|
||||
email = data["email"]
|
||||
tg_id = data["tg_id"]
|
||||
|
||||
stmt = update(Key).where(Key.tg_id == tg_id, Key.email == email).values(tariff_id=tariff_id)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
await update_subscription(tg_id=tg_id, email=email, session=session)
|
||||
|
||||
await state.clear()
|
||||
|
||||
callback_data = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id)
|
||||
await handle_key_edit(
|
||||
callback_query=callback_query,
|
||||
callback_data=callback_data,
|
||||
session=session,
|
||||
update=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back:group", IsAdminFilter())
|
||||
async def handle_back_to_group(
|
||||
callback_query: CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
):
|
||||
await state.get_data()
|
||||
|
||||
result = await session.execute(select(Tariff.group_code).distinct())
|
||||
groups = [row[0] for row in result.fetchall()]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group_code in groups:
|
||||
builder.button(text=group_code, callback_data=f"group:{group_code}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:renew")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="📁 <b>Выберите тарифную группу:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.set_state(RenewTariffState.selecting_group)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
|
||||
IsAdminFilter(),
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
from datetime import datetime
|
||||
|
||||
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 core.settings.tariffs_config import normalize_tariff_config
|
||||
from database import get_tariff_by_id
|
||||
from database.models import Key, Tariff
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.keys.operations import renew_key_in_cluster
|
||||
from logger import logger
|
||||
|
||||
from .keyboard import AdminUserEditorCallback
|
||||
from .users_states import RenewTariffState
|
||||
from .users_keys import handle_key_edit
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back:renew", IsAdminFilter())
|
||||
async def handle_back_to_key_menu(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
data = await state.get_data()
|
||||
email = data.get("email")
|
||||
tg_id = data.get("tg_id")
|
||||
await state.clear()
|
||||
|
||||
if not email or not tg_id:
|
||||
await callback_query.message.edit_text("❌ Не найдены данные сессии.")
|
||||
return
|
||||
|
||||
callback_data = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id)
|
||||
|
||||
await handle_key_edit(
|
||||
callback_query=callback_query,
|
||||
callback_data=callback_data,
|
||||
session=session,
|
||||
update=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminUserEditorCallback.filter(F.action == "users_renew"),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
async def handle_user_choose_tariff_group(
|
||||
callback_query: CallbackQuery,
|
||||
callback_data: AdminUserEditorCallback,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
email = callback_data.data
|
||||
tg_id = callback_data.tg_id
|
||||
|
||||
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()]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group_code in groups:
|
||||
builder.button(text=group_code, callback_data=f"group:{group_code}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:renew")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="📁 <b>Выберите тарифную группу:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("group:"), IsAdminFilter())
|
||||
async def handle_user_choose_tariff(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
group_code = callback_query.data.split(":", 1)[1]
|
||||
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()
|
||||
|
||||
if not tariffs:
|
||||
await callback_query.message.edit_text("❌ Нет активных тарифов в группе.")
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for tariff in tariffs:
|
||||
builder.button(text=f"{tariff.name} – {int(tariff.price_rub)}₽", callback_data=f"confirm:{tariff.id}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:group")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"📦 <b>Выберите тариф для группы <code>{group_code}</code>:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm:"), IsAdminFilter())
|
||||
async def handle_user_renew_confirm(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
tariff_id = int(callback_query.data.split(":")[1])
|
||||
data = await state.get_data()
|
||||
email = data.get("email")
|
||||
tg_id = data.get("tg_id")
|
||||
|
||||
if not email or not tg_id:
|
||||
await callback_query.message.edit_text("❌ Не найдены данные сессии.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
tariff = await get_tariff_by_id(session, tariff_id)
|
||||
if not tariff:
|
||||
await callback_query.message.edit_text("❌ Тариф не найден.")
|
||||
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()
|
||||
if not key_obj:
|
||||
await callback_query.message.edit_text("❌ Ключ не найден.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if tariff.get("configurable"):
|
||||
raw_device_options = tariff.get("device_options")
|
||||
raw_traffic_options = tariff.get("traffic_options_gb")
|
||||
|
||||
raw_device_options = raw_device_options if isinstance(raw_device_options, list) else []
|
||||
raw_traffic_options = raw_traffic_options if isinstance(raw_traffic_options, list) else []
|
||||
|
||||
try:
|
||||
device_options = sorted(raw_device_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
device_options = raw_device_options
|
||||
|
||||
try:
|
||||
traffic_options = sorted(raw_traffic_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
traffic_options = raw_traffic_options
|
||||
|
||||
device_int_options: list[int] = []
|
||||
for value in device_options:
|
||||
try:
|
||||
device_int_options.append(int(value))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
traffic_int_options: list[int] = []
|
||||
for value in traffic_options:
|
||||
try:
|
||||
traffic_int_options.append(int(value))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
if not device_int_options and not traffic_int_options:
|
||||
await callback_query.message.edit_text(
|
||||
"❌ Конфигуратор для этого тарифа не настроен. Попробуйте выбрать другой тариф."
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
cfg = normalize_tariff_config(tariff)
|
||||
|
||||
positive_device_values = [v for v in device_int_options if v > 0]
|
||||
positive_traffic_values = [v for v in traffic_int_options if v > 0]
|
||||
|
||||
base_device_limit = cfg.get("base_device_limit")
|
||||
if base_device_limit is None:
|
||||
base_device_limit = tariff.get("device_limit")
|
||||
if base_device_limit is None:
|
||||
if positive_device_values:
|
||||
base_device_limit = min(positive_device_values)
|
||||
elif device_int_options:
|
||||
base_device_limit = device_int_options[0]
|
||||
base_device_limit = int(base_device_limit) if base_device_limit is not None else None
|
||||
|
||||
base_traffic_gb = cfg.get("base_traffic_gb")
|
||||
if base_traffic_gb is None:
|
||||
traffic_limit_raw = tariff.get("traffic_limit")
|
||||
if traffic_limit_raw is not None:
|
||||
try:
|
||||
base_traffic_gb = int(traffic_limit_raw)
|
||||
except (TypeError, ValueError):
|
||||
base_traffic_gb = None
|
||||
if base_traffic_gb is None:
|
||||
if positive_traffic_values:
|
||||
base_traffic_gb = min(positive_traffic_values)
|
||||
elif traffic_int_options:
|
||||
base_traffic_gb = traffic_int_options[0]
|
||||
base_traffic_gb = int(base_traffic_gb) if base_traffic_gb is not None else None
|
||||
|
||||
selected_devices = (
|
||||
base_device_limit if base_device_limit is not None else (device_int_options[0] if device_int_options else None)
|
||||
)
|
||||
selected_traffic_gb = (
|
||||
base_traffic_gb if base_traffic_gb is not None else (traffic_int_options[0] if traffic_int_options else None)
|
||||
)
|
||||
|
||||
await state.update_data(
|
||||
renew_tariff_id=tariff_id,
|
||||
renew_selected_device_limit=selected_devices,
|
||||
renew_selected_traffic_gb=selected_traffic_gb,
|
||||
renew_mode="renew",
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
device_buttons: list[InlineKeyboardButton] = []
|
||||
traffic_buttons: list[InlineKeyboardButton] = []
|
||||
|
||||
if device_int_options and len(device_int_options) > 1:
|
||||
sel = int(selected_devices or 0)
|
||||
for value in device_int_options:
|
||||
mark = " ✅" if value == sel else ""
|
||||
caption = "Безлимит устройств" if value == 0 else f"{value} устройств"
|
||||
device_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{caption}{mark}",
|
||||
callback_data=f"cfg_renew_devices|{tariff_id}|{value}",
|
||||
)
|
||||
)
|
||||
|
||||
if traffic_int_options and len(traffic_int_options) > 1:
|
||||
sel = int(selected_traffic_gb or 0)
|
||||
for value in traffic_int_options:
|
||||
mark = " ✅" if value == sel else ""
|
||||
caption = "Безлимит трафика" if value == 0 else f"{value} ГБ"
|
||||
traffic_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{caption}{mark}",
|
||||
callback_data=f"cfg_renew_traffic|{tariff_id}|{value}",
|
||||
)
|
||||
)
|
||||
|
||||
if device_buttons and traffic_buttons:
|
||||
max_len = max(len(device_buttons), len(traffic_buttons))
|
||||
for i in range(max_len):
|
||||
row = []
|
||||
if i < len(device_buttons):
|
||||
row.append(device_buttons[i])
|
||||
if i < len(traffic_buttons):
|
||||
row.append(traffic_buttons[i])
|
||||
builder.row(*row)
|
||||
elif device_buttons:
|
||||
for b in device_buttons:
|
||||
builder.row(b)
|
||||
elif traffic_buttons:
|
||||
for b in traffic_buttons:
|
||||
builder.row(b)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}"))
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group"))
|
||||
|
||||
devices_label = (
|
||||
"Безлимит устройств"
|
||||
if (selected_devices is not None and int(selected_devices) <= 0)
|
||||
else (f"{int(selected_devices)} устройств" if selected_devices is not None else "—")
|
||||
)
|
||||
traffic_label = (
|
||||
"Безлимит трафика"
|
||||
if (selected_traffic_gb is not None and int(selected_traffic_gb) <= 0)
|
||||
else (f"{int(selected_traffic_gb)} ГБ" if selected_traffic_gb is not None else "—")
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=(
|
||||
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
||||
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
||||
f"📱 <b>Устройства:</b> {devices_label}\n"
|
||||
f"📊 <b>Трафик:</b> {traffic_label}\n\n"
|
||||
"Выберите параметры и нажмите «✅ Применить»."
|
||||
),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
return
|
||||
|
||||
device_limit = int(tariff.get("device_limit") or 0)
|
||||
|
||||
raw_traffic_limit = tariff.get("traffic_limit")
|
||||
traffic_gb = 0
|
||||
if raw_traffic_limit is not None:
|
||||
try:
|
||||
traffic_gb = int(raw_traffic_limit)
|
||||
except (TypeError, ValueError):
|
||||
traffic_gb = 0
|
||||
|
||||
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()
|
||||
|
||||
new_subgroup = (
|
||||
await session.execute(select(Tariff.subgroup_title).where(Tariff.id == tariff_id))
|
||||
).scalar_one_or_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()
|
||||
|
||||
try:
|
||||
ok = await renew_key_in_cluster(
|
||||
cluster_id=key_obj.server_id,
|
||||
email=email,
|
||||
client_id=key_obj.client_id,
|
||||
new_expiry_time=new_expiry_time,
|
||||
total_gb=traffic_gb,
|
||||
session=session,
|
||||
hwid_device_limit=device_limit,
|
||||
reset_traffic=False,
|
||||
target_subgroup=new_subgroup,
|
||||
old_subgroup=old_subgroup,
|
||||
plan=tariff_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[AdminRenew] renew_key_in_cluster failed: tg_id={tg_id} email={email} tariff_id={tariff_id}: {e}")
|
||||
ok = False
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not ok:
|
||||
await callback_query.message.answer("❌ Не удалось обновить подписку на серверах (renew).")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("cfg_renew_devices|"), IsAdminFilter())
|
||||
async def handle_cfg_renew_devices(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
_, tariff_id_str, value_str = callback_query.data.split("|", 2)
|
||||
tariff_id = int(tariff_id_str)
|
||||
value = int(value_str)
|
||||
|
||||
data = await state.get_data()
|
||||
if int(data.get("renew_tariff_id") or 0) != tariff_id:
|
||||
await callback_query.answer("⚠️ Сессия устарела", show_alert=True)
|
||||
return
|
||||
|
||||
await state.update_data(renew_selected_device_limit=value)
|
||||
|
||||
tariff = await get_tariff_by_id(session, tariff_id)
|
||||
if not tariff:
|
||||
await callback_query.message.edit_text("❌ Тариф не найден.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
selected_devices = int((await state.get_data()).get("renew_selected_device_limit") or 0)
|
||||
selected_traffic_gb = (await state.get_data()).get("renew_selected_traffic_gb")
|
||||
|
||||
raw_device_options = tariff.get("device_options")
|
||||
raw_traffic_options = tariff.get("traffic_options_gb")
|
||||
|
||||
raw_device_options = raw_device_options if isinstance(raw_device_options, list) else []
|
||||
raw_traffic_options = raw_traffic_options if isinstance(raw_traffic_options, list) else []
|
||||
|
||||
try:
|
||||
device_options = sorted(raw_device_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
device_options = raw_device_options
|
||||
|
||||
try:
|
||||
traffic_options = sorted(raw_traffic_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
traffic_options = raw_traffic_options
|
||||
|
||||
device_int_options: list[int] = []
|
||||
for v in device_options:
|
||||
try:
|
||||
device_int_options.append(int(v))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
traffic_int_options: list[int] = []
|
||||
for v in traffic_options:
|
||||
try:
|
||||
traffic_int_options.append(int(v))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
device_buttons: list[InlineKeyboardButton] = []
|
||||
traffic_buttons: list[InlineKeyboardButton] = []
|
||||
|
||||
if device_int_options and len(device_int_options) > 1:
|
||||
for v in device_int_options:
|
||||
mark = " ✅" if v == selected_devices else ""
|
||||
caption = "Безлимит устройств" if v == 0 else f"{v} устройств"
|
||||
device_buttons.append(
|
||||
InlineKeyboardButton(text=f"{caption}{mark}", callback_data=f"cfg_renew_devices|{tariff_id}|{v}")
|
||||
)
|
||||
|
||||
if traffic_int_options and len(traffic_int_options) > 1:
|
||||
sel_tr = int(selected_traffic_gb or 0)
|
||||
for v in traffic_int_options:
|
||||
mark = " ✅" if v == sel_tr else ""
|
||||
caption = "Безлимит трафика" if v == 0 else f"{v} ГБ"
|
||||
traffic_buttons.append(
|
||||
InlineKeyboardButton(text=f"{caption}{mark}", callback_data=f"cfg_renew_traffic|{tariff_id}|{v}")
|
||||
)
|
||||
|
||||
if device_buttons and traffic_buttons:
|
||||
max_len = max(len(device_buttons), len(traffic_buttons))
|
||||
for i in range(max_len):
|
||||
row = []
|
||||
if i < len(device_buttons):
|
||||
row.append(device_buttons[i])
|
||||
if i < len(traffic_buttons):
|
||||
row.append(traffic_buttons[i])
|
||||
builder.row(*row)
|
||||
elif device_buttons:
|
||||
for b in device_buttons:
|
||||
builder.row(b)
|
||||
elif traffic_buttons:
|
||||
for b in traffic_buttons:
|
||||
builder.row(b)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}"))
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group"))
|
||||
|
||||
devices_label = "Безлимит устройств" if selected_devices <= 0 else f"{selected_devices} устройств"
|
||||
traffic_label = (
|
||||
"Безлимит трафика"
|
||||
if (selected_traffic_gb is not None and int(selected_traffic_gb) <= 0)
|
||||
else (f"{int(selected_traffic_gb)} ГБ" if selected_traffic_gb is not None else "—")
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=(
|
||||
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
||||
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
||||
f"📱 <b>Устройства:</b> {devices_label}\n"
|
||||
f"📊 <b>Трафик:</b> {traffic_label}\n\n"
|
||||
"Выберите параметры и нажмите «✅ Применить»."
|
||||
),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("cfg_renew_traffic|"), IsAdminFilter())
|
||||
async def handle_cfg_renew_traffic(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
_, tariff_id_str, value_str = callback_query.data.split("|", 2)
|
||||
tariff_id = int(tariff_id_str)
|
||||
value = int(value_str)
|
||||
|
||||
data = await state.get_data()
|
||||
if int(data.get("renew_tariff_id") or 0) != tariff_id:
|
||||
await callback_query.answer("⚠️ Сессия устарела", show_alert=True)
|
||||
return
|
||||
|
||||
await state.update_data(renew_selected_traffic_gb=value)
|
||||
|
||||
tariff = await get_tariff_by_id(session, tariff_id)
|
||||
if not tariff:
|
||||
await callback_query.message.edit_text("❌ Тариф не найден.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
selected_devices = (await state.get_data()).get("renew_selected_device_limit")
|
||||
selected_traffic_gb = int((await state.get_data()).get("renew_selected_traffic_gb") or 0)
|
||||
|
||||
raw_device_options = tariff.get("device_options")
|
||||
raw_traffic_options = tariff.get("traffic_options_gb")
|
||||
|
||||
raw_device_options = raw_device_options if isinstance(raw_device_options, list) else []
|
||||
raw_traffic_options = raw_traffic_options if isinstance(raw_traffic_options, list) else []
|
||||
|
||||
try:
|
||||
device_options = sorted(raw_device_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
device_options = raw_device_options
|
||||
|
||||
try:
|
||||
traffic_options = sorted(raw_traffic_options, key=lambda v: (int(v) == 0, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
traffic_options = raw_traffic_options
|
||||
|
||||
device_int_options: list[int] = []
|
||||
for v in device_options:
|
||||
try:
|
||||
device_int_options.append(int(v))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
traffic_int_options: list[int] = []
|
||||
for v in traffic_options:
|
||||
try:
|
||||
traffic_int_options.append(int(v))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
device_buttons: list[InlineKeyboardButton] = []
|
||||
traffic_buttons: list[InlineKeyboardButton] = []
|
||||
|
||||
if device_int_options and len(device_int_options) > 1:
|
||||
sel_dev = int(selected_devices or 0)
|
||||
for v in device_int_options:
|
||||
mark = " ✅" if v == sel_dev else ""
|
||||
caption = "Безлимит устройств" if v == 0 else f"{v} устройств"
|
||||
device_buttons.append(
|
||||
InlineKeyboardButton(text=f"{caption}{mark}", callback_data=f"cfg_renew_devices|{tariff_id}|{v}")
|
||||
)
|
||||
|
||||
if traffic_int_options and len(traffic_int_options) > 1:
|
||||
for v in traffic_int_options:
|
||||
mark = " ✅" if v == selected_traffic_gb else ""
|
||||
caption = "Безлимит трафика" if v == 0 else f"{v} ГБ"
|
||||
traffic_buttons.append(
|
||||
InlineKeyboardButton(text=f"{caption}{mark}", callback_data=f"cfg_renew_traffic|{tariff_id}|{v}")
|
||||
)
|
||||
|
||||
if device_buttons and traffic_buttons:
|
||||
max_len = max(len(device_buttons), len(traffic_buttons))
|
||||
for i in range(max_len):
|
||||
row = []
|
||||
if i < len(device_buttons):
|
||||
row.append(device_buttons[i])
|
||||
if i < len(traffic_buttons):
|
||||
row.append(traffic_buttons[i])
|
||||
builder.row(*row)
|
||||
elif device_buttons:
|
||||
for b in device_buttons:
|
||||
builder.row(b)
|
||||
elif traffic_buttons:
|
||||
for b in traffic_buttons:
|
||||
builder.row(b)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="✅ Применить", callback_data=f"cfg_renew_apply|{tariff_id}"))
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="back:group"))
|
||||
|
||||
devices_label = (
|
||||
"Безлимит устройств"
|
||||
if (selected_devices is not None and int(selected_devices) <= 0)
|
||||
else (f"{int(selected_devices)} устройств" if selected_devices is not None else "—")
|
||||
)
|
||||
traffic_label = "Безлимит трафика" if selected_traffic_gb <= 0 else f"{selected_traffic_gb} ГБ"
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=(
|
||||
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
||||
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
||||
f"📱 <b>Устройства:</b> {devices_label}\n"
|
||||
f"📊 <b>Трафик:</b> {traffic_label}\n\n"
|
||||
"Выберите параметры и нажмите «✅ Применить»."
|
||||
),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("cfg_renew_apply|"), IsAdminFilter())
|
||||
async def handle_cfg_renew_apply(callback_query: CallbackQuery, session: AsyncSession, state: FSMContext):
|
||||
_, tariff_id_str = callback_query.data.split("|", 1)
|
||||
tariff_id = int(tariff_id_str)
|
||||
|
||||
data = await state.get_data()
|
||||
email = data.get("email")
|
||||
tg_id = data.get("tg_id")
|
||||
|
||||
if not email or not tg_id:
|
||||
await callback_query.message.edit_text("❌ Не найдены данные сессии.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if int(data.get("renew_tariff_id") or 0) != tariff_id:
|
||||
await callback_query.message.edit_text("❌ Сессия устарела. Выберите тариф заново.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
selected_devices = data.get("renew_selected_device_limit")
|
||||
selected_traffic_gb = data.get("renew_selected_traffic_gb")
|
||||
|
||||
if selected_devices is None and selected_traffic_gb is None:
|
||||
await callback_query.message.edit_text("❌ Не выбраны параметры конфигурации.")
|
||||
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()
|
||||
if not key_obj:
|
||||
await callback_query.message.edit_text("❌ Ключ не найден.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
new_subgroup = (
|
||||
await session.execute(select(Tariff.subgroup_title).where(Tariff.id == tariff_id))
|
||||
).scalar_one_or_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()
|
||||
|
||||
try:
|
||||
ok = await renew_key_in_cluster(
|
||||
cluster_id=key_obj.server_id,
|
||||
email=email,
|
||||
client_id=key_obj.client_id,
|
||||
new_expiry_time=new_expiry_time,
|
||||
total_gb=int(selected_traffic_gb or 0),
|
||||
session=session,
|
||||
hwid_device_limit=int(selected_devices or 0),
|
||||
reset_traffic=False,
|
||||
target_subgroup=new_subgroup,
|
||||
old_subgroup=old_subgroup,
|
||||
plan=tariff_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[AdminRenewCfg] renew_key_in_cluster failed: tg_id={tg_id} email={email} tariff_id={tariff_id}: {e}")
|
||||
ok = False
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not ok:
|
||||
await callback_query.message.answer("❌ Не удалось обновить подписку на серверах (renew).")
|
||||
|
||||
callback_data_back = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=int(tg_id))
|
||||
|
||||
await handle_key_edit(callback_query=callback_query, callback_data=callback_data_back, session=session, update=False)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back:group", IsAdminFilter())
|
||||
async def handle_back_to_group(
|
||||
callback_query: CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
):
|
||||
result = await session.execute(select(Tariff.group_code).distinct())
|
||||
groups = [row[0] for row in result.fetchall()]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group_code in groups:
|
||||
builder.button(text=group_code, callback_data=f"group:{group_code}")
|
||||
builder.button(text="🔙 Назад", callback_data="back:renew")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="📁 <b>Выберите тарифную группу:</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.set_state(RenewTariffState.selecting_group)
|
||||
@@ -464,17 +464,26 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
|
||||
selected_devices_db = record.get("selected_device_limit")
|
||||
selected_traffic_db = record.get("selected_traffic_limit")
|
||||
|
||||
traffic_default = next(
|
||||
(x for x in traffic_options_sorted if x != 0),
|
||||
(traffic_options_sorted[0] if traffic_options_sorted else None),
|
||||
)
|
||||
device_default = next(
|
||||
(x for x in device_options_sorted if x != 0),
|
||||
(device_options_sorted[0] if device_options_sorted else None),
|
||||
)
|
||||
|
||||
if traffic_options_sorted:
|
||||
selected_traffic_db = int(selected_traffic_db) if selected_traffic_db is not None else traffic_options_sorted[0]
|
||||
selected_traffic_db = int(selected_traffic_db) if selected_traffic_db is not None else traffic_default
|
||||
if selected_traffic_db not in traffic_options_sorted:
|
||||
selected_traffic_db = traffic_options_sorted[0]
|
||||
selected_traffic_db = traffic_default
|
||||
else:
|
||||
selected_traffic_db = None
|
||||
|
||||
if device_options_sorted:
|
||||
selected_devices_db = int(selected_devices_db) if selected_devices_db is not None else device_options_sorted[0]
|
||||
selected_devices_db = int(selected_devices_db) if selected_devices_db is not None else device_default
|
||||
if selected_devices_db not in device_options_sorted:
|
||||
selected_devices_db = device_options_sorted[0]
|
||||
selected_devices_db = device_default
|
||||
else:
|
||||
selected_devices_db = None
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from panels._3xui import extend_client_key, get_xui_instance
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
|
||||
from .aggregated_links import make_aggregated_link
|
||||
from .subgroup_migration import migrate_between_subgroups
|
||||
from ...tariffs.subgroup_migration import migrate_between_subgroups
|
||||
|
||||
|
||||
async def resolve_cluster(session: AsyncSession, cluster_id: str):
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ from logger import (
|
||||
from panels._3xui import ClientConfig, add_client, extend_client_key, get_xui_instance
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
|
||||
from .deletion import delete_on_3xui, delete_on_remnawave
|
||||
from .utils import bytes_from_gb, norm_name, split_by_panel
|
||||
from ..keys.operations.deletion import delete_on_3xui, delete_on_remnawave
|
||||
from ..keys.operations.utils import bytes_from_gb, norm_name, split_by_panel
|
||||
|
||||
|
||||
async def ensure_on_remnawave(
|
||||
+1
-1
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
return f"v.5.1-b221229 {get_git_commit_number()}"
|
||||
return f"v.5.1-b261235 {get_git_commit_number()}"
|
||||
|
||||
Reference in New Issue
Block a user