Files
Solo_bot/handlers/admin/users/users_tariffs.py
T
2026-02-08 22:30:05 +03:00

720 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from aiogram.exceptions import TelegramBadRequest
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from handlers.buttons import BACK
from core.settings.tariffs_config import normalize_tariff_config
from database import get_tariff_by_id
from database.models import Key, Tariff
from 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=BACK, 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=BACK, 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=BACK, 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=BACK, 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 "")
)
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"
"Выберите параметры и нажмите «✅ Применить»."
)
try:
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
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=BACK, 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} ГБ"
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"
"Выберите параметры и нажмите «✅ Применить»."
)
try:
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
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=BACK, 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)