from ._common import * # noqa: F401,F403 @router.callback_query( AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter(), ) async def handle_key_edit( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback, session: AsyncSession, update: bool = False, ): key_ref = callback_data.data key_obj = await resolve_callback_key(session, callback_data.tg_id, key_ref) if not key_obj: await callback_query.message.edit_text( text="🚫 Информация о подписке не найдена.", reply_markup=build_editor_kb(callback_data.tg_id), ) return email = key_obj.email key_details = await get_key_details(session, email) is_frozen = bool(key_details.get("is_frozen")) if key_details else bool(getattr(key_obj, "is_frozen", False)) key_value = key_obj.key or key_obj.remnawave_link or "—" alias_part = f" ({key_obj.alias})" if key_obj.alias else "" if key_obj.created_at: created_at_dt = datetime.fromtimestamp(int(key_obj.created_at) / 1000, tz=MOSCOW_TZ) created_at = created_at_dt.strftime("%d %B %Y года %H:%M") else: created_at = "—" if is_frozen: frozen_left_ms = int((key_details or {}).get("expiry_time") or 0) total_minutes = max(frozen_left_ms // 60000, 0) days, rem_minutes = divmod(total_minutes, 24 * 60) hours, minutes = divmod(rem_minutes, 60) frozen_parts: list[str] = [] if days: frozen_parts.append(f"{days} дн.") if hours: frozen_parts.append(f"{hours} ч.") if minutes or not frozen_parts: frozen_parts.append(f"{minutes} мин.") expiry_label = "⏳ Остаток:" expiry_date = " ".join(frozen_parts) elif key_obj.expiry_time: expiry_dt = datetime.fromtimestamp(int(key_obj.expiry_time) / 1000, tz=MOSCOW_TZ) expiry_label = "⏰ Истекает:" expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M") else: expiry_label = "⏰ Истекает:" expiry_date = "—" tariff_name = "—" subgroup_title = "—" group_code = "—" base_devices = None base_traffic = None is_configurable = False if key_obj.tariff_id: tariff = await get_tariff_by_id(session, key_obj.tariff_id) if tariff: tariff_name = tariff.get("name", "—") subgroup_title = tariff.get("subgroup_title") or "—" group_code = tariff.get("group_code") or "—" base_devices = tariff.get("device_limit") base_traffic = tariff.get("traffic_limit") is_configurable = bool(tariff.get("configurable")) devices_line = "" traffic_line = "" if is_configurable: sel_dev, cur_dev = key_obj.selected_device_limit, key_obj.current_device_limit if sel_dev is not None or cur_dev is not None: base_dev = sel_dev if sel_dev is not None else (base_devices if base_devices is not None else cur_dev) extra = ( f" + {cur_dev - base_dev} (докуплено)" if (base_dev is not None and cur_dev is not None and cur_dev > base_dev) else "" ) devices_line = f"📱 Устройства: {base_dev}{extra}\n" sel_traf, cur_traf = key_obj.selected_traffic_limit, key_obj.current_traffic_limit if sel_traf is not None or cur_traf is not None: base_traf = sel_traf if sel_traf is not None else (base_traffic if base_traffic is not None else cur_traf) extra = ( f" + {cur_traf - base_traf} ГБ (докуплено)" if (base_traf is not None and cur_traf is not None and cur_traf > base_traf) else "" ) traffic_line = f"📊 Трафик: {base_traf} ГБ{extra}\n" text = ( "🔑 Информация о подписке\n\n" "
"
f"🔗 Ключ{alias_part}: {key_value}\n"
f"📆 Создан: {created_at} (МСК)\n"
f"{'⛔ Статус: отключена\n' if is_frozen else ''}"
f"{expiry_label} {expiry_date}{' (МСК)' if not is_frozen and expiry_date != '—' else ''}\n"
f"🌐 Кластер: {key_obj.server_id or '—'}\n"
f"🆔 ID клиента: {key_obj.tg_id or '—'}\n"
f"🏷️ Тарифная группа: {group_code}\n"
f"📁 Подгруппа: {subgroup_title}\n"
f"📦 Тариф: {tariff_name}\n"
f"{devices_line}"
f"{traffic_line}"
""
)
if not update or not getattr(callback_data, "edit", False):
kb_key_details = dict(key_obj.__dict__)
kb_key_details["is_frozen"] = is_frozen
kb_markup = build_key_edit_kb(kb_key_details, email, is_configurable=is_configurable, key_ref=str(key_ref))
kb_builder = InlineKeyboardBuilder.from_markup(kb_markup)
hook_buttons = await process_admin_key_edit_menu(
email=email,
session=session,
client_id=key_obj.client_id,
tg_id=key_obj.tg_id,
)
kb_builder = insert_hook_buttons(kb_builder, hook_buttons)
try:
await callback_query.message.edit_text(
text=text,
reply_markup=kb_builder.as_markup(),
)
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
else:
try:
await callback_query.message.edit_text(
text=text,
reply_markup=await build_users_key_expiry_kb(
session,
callback_data.tg_id,
email,
key_ref=str(key_ref),
),
)
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
IsAdminFilter(),
)
async def handle_change_expiry(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: AsyncSession,
):
tg_id = callback_data.tg_id
key_ref = str(callback_data.data)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
email = key_obj.email
await callback_query.message.edit_reply_markup(
reply_markup=await build_users_key_expiry_kb(session, tg_id, email, key_ref=key_ref)
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "add"),
IsAdminFilter(),
)
async def handle_expiry_add(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
session: AsyncSession,
):
tg_id = callback_data.tg_id
key_ref = str(callback_data.data)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
email = key_obj.email
days = callback_data.month
key_details = await get_key_details(session, email)
if not key_details:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
if days:
await change_expiry_time(key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session)
await handle_key_edit(callback_query, callback_data, session, True)
return
await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="add")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "take"),
IsAdminFilter(),
)
async def handle_expiry_take(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
session: AsyncSession,
):
tg_id = callback_data.tg_id
key_ref = str(callback_data.data)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
email = key_obj.email
await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="take")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "set"),
IsAdminFilter(),
)
async def handle_expiry_set(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
session: AsyncSession,
):
tg_id = callback_data.tg_id
key_ref = str(callback_data.data)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
email = key_obj.email
key_details = await get_key_details(session, email)
if not key_details:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, op_type="set")
await state.set_state(UserEditorState.waiting_for_expiry_time)
text = (
"✍️ Введите новое время действия ключа:"
"\n\n📌 Формат: год-месяц-день час:минута"
f"\n\n📄 Текущая дата: {datetime.fromtimestamp(key_details['expiry_time'] / 1000, tz=MOSCOW_TZ).strftime('%Y-%m-%d %H:%M')} (МСК)"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_key_show_kb(tg_id, key_ref),
)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(message: Message, state: FSMContext, session: AsyncSession):
data = await state.get_data()
tg_id = data.get("tg_id")
email = data.get("email")
key_ref = data.get("key_ref")
op_type = data.get("op_type")
if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0):
await message.answer(
text="🚫 Пожалуйста, введите корректное количество дней!",
reply_markup=build_users_key_show_kb(tg_id, key_ref) if key_ref else build_editor_kb(tg_id),
)
return
key_details = await get_key_details(session, email)
if not key_details:
await message.answer(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
try:
current_expiry_time = datetime.fromtimestamp(
key_details["expiry_time"] / 1000,
tz=MOSCOW_TZ,
)
if op_type == "add":
days = int(message.text)
new_expiry_time = current_expiry_time + timedelta(days=days)
text = f"✅ Ко времени действия ключа добавлено {days} дн."
elif op_type == "take":
days = int(message.text)
new_expiry_time = current_expiry_time - timedelta(days=days)
text = f"✅ Из времени действия ключа вычтено {days} дн."
else:
new_expiry_time = datetime.strptime(message.text, "%Y-%m-%d %H:%M")
new_expiry_time = MOSCOW_TZ.localize(new_expiry_time)
text = f"✅ Время действия ключа изменено на {message.text} (МСК)"
new_expiry_timestamp = int(new_expiry_time.timestamp() * 1000)
await change_expiry_time(new_expiry_timestamp, email, session)
except ValueError:
text = "🚫 Пожалуйста, используйте корректный формат даты (ГГГГ-ММ-ДД ЧЧ:ММ)!"
except Exception as e:
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
await message.answer(
text=text,
reply_markup=build_users_key_show_kb(tg_id, key_ref) if key_ref else build_editor_kb(tg_id),
)
async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession) -> Exception | None:
key_obj = await get_key_by_email(session, email)
if not key_obj:
return ValueError(f"User with email {email} was not found")
client_id = key_obj.client_id
tariff_id = key_obj.tariff_id
server_id = key_obj.server_id
key_device_limit = key_obj.current_device_limit
key_traffic_limit = key_obj.current_traffic_limit
if server_id is None:
return ValueError(f"Key with client_id {client_id} was not found")
traffic_limit = 0
device_limit = None
key_subgroup = None
if tariff_id:
tariff = await get_tariff_by_id(session, tariff_id)
if tariff:
traffic_limit = int(tariff.get("traffic_limit") or 0)
raw_device_limit = tariff.get("device_limit")
device_limit = int(raw_device_limit) if raw_device_limit is not None else 0
key_subgroup = tariff.get("subgroup_title")
if key_device_limit is not None:
device_limit = key_device_limit
if key_traffic_limit is not None:
traffic_limit = key_traffic_limit
servers = await get_servers(session=session)
if server_id in servers:
target_cluster = server_id
else:
target_cluster = None
for cluster_name, cluster_servers in servers.items():
if any(s.get("server_name") == server_id for s in cluster_servers):
target_cluster = cluster_name
break
if not target_cluster:
return ValueError(f"No suitable cluster found for server {server_id}")
await release_session_early(session)
await renew_key_in_cluster(
cluster_id=target_cluster,
email=email,
client_id=client_id,
new_expiry_time=expiry_time,
total_gb=traffic_limit,
session=session,
hwid_device_limit=device_limit,
reset_traffic=False,
target_subgroup=key_subgroup,
old_subgroup=key_subgroup,
plan=tariff_id,
)
await update_key_expiry(session, client_id, expiry_time)
return None