add key configuration management
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user