413 lines
16 KiB
Python
413 lines
16 KiB
Python
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" (<i>{key_obj.alias}</i>)" 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 = "⏳ <b>Остаток:</b>"
|
|
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 = "⏰ <b>Истекает:</b>"
|
|
expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M")
|
|
else:
|
|
expiry_label = "⏰ <b>Истекает:</b>"
|
|
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"📱 <b>Устройства:</b> {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"📊 <b>Трафик:</b> {base_traf} ГБ{extra}\n"
|
|
|
|
text = (
|
|
"<b>🔑 Информация о подписке</b>\n\n"
|
|
"<blockquote>"
|
|
f"🔗 <b>Ключ{alias_part}:</b> <code>{key_value}</code>\n"
|
|
f"📆 <b>Создан:</b> {created_at} (МСК)\n"
|
|
f"{'⛔ <b>Статус:</b> отключена\n' if is_frozen else ''}"
|
|
f"{expiry_label} {expiry_date}{' (МСК)' if not is_frozen and expiry_date != '—' else ''}\n"
|
|
f"🌐 <b>Кластер:</b> {key_obj.server_id or '—'}\n"
|
|
f"🆔 <b>ID клиента:</b> {key_obj.tg_id or '—'}\n"
|
|
f"🏷️ <b>Тарифная группа:</b> {group_code}\n"
|
|
f"📁 <b>Подгруппа:</b> {subgroup_title}\n"
|
|
f"📦 <b>Тариф:</b> {tariff_name}\n"
|
|
f"{devices_line}"
|
|
f"{traffic_line}"
|
|
"</blockquote>"
|
|
)
|
|
|
|
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📌 Формат: <b>год-месяц-день час:минута</b>"
|
|
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"✅ Ко времени действия ключа добавлено <b>{days} дн.</b>"
|
|
elif op_type == "take":
|
|
days = int(message.text)
|
|
new_expiry_time = current_expiry_time - timedelta(days=days)
|
|
text = f"✅ Из времени действия ключа вычтено <b>{days} дн.</b>"
|
|
else:
|
|
new_expiry_time = datetime.strptime(message.text, "%Y-%m-%d %H:%M")
|
|
new_expiry_time = MOSCOW_TZ.localize(new_expiry_time)
|
|
text = f"✅ Время действия ключа изменено на <b>{message.text} (МСК)</b>"
|
|
|
|
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
|