add key configuration management

This commit is contained in:
Capybara-z
2025-12-14 18:51:00 +03:00
parent 3d2269a368
commit 5aa762aaf1
3 changed files with 431 additions and 2 deletions
+8 -1
View File
@@ -243,7 +243,7 @@ def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
return builder.as_markup()
def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = False) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
is_frozen = (
@@ -264,6 +264,13 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
text="📦 Тариф",
callback_data=AdminUserEditorCallback(action="users_renew", data=email, tg_id=key_details["tg_id"]).pack(),
)
if is_configurable:
builder.button(
text="📱 Конфигурация",
callback_data=AdminUserEditorCallback(
action="users_edit_config", data=email, tg_id=key_details["tg_id"]
).pack(),
)
builder.button(
text="❌ Удалить",
callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(),
+420 -1
View File
@@ -157,7 +157,7 @@ async def handle_key_edit(
)
if not update or not getattr(callback_data, "edit", False):
kb_markup = build_key_edit_kb(key_obj.__dict__, email)
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,
@@ -1227,3 +1227,422 @@ async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession
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 += f"📊 <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 += f"📊 <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)
+3
View File
@@ -11,6 +11,9 @@ class UserEditorState(StatesGroup):
selecting_cluster = State()
selecting_duration = State()
selecting_country = State()
config_menu = State()
config_select_base = State()
config_input_addon = State()
class RenewTariffState(StatesGroup):