Files
2026-04-14 07:19:14 +00:00

771 lines
28 KiB
Python

from ._common import * # noqa: F401,F403
from .edit import handle_key_edit
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_reissue_menu"),
IsAdminFilter(),
)
async def handle_reissue_menu(
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("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
return
text = (
"<b>🔄 Перевыпуск подписки</b>\n\n"
"<b>📦 Полный перевыпуск</b>\n"
"<i>Пересоздаёт подписку на сервере с возможностью выбора кластера. "
"Используйте для переноса на другой сервер или обновления данных.</i>\n\n"
"<b>🔗 Сменить ссылку</b>\n"
"<i>Генерирует новую ссылку подписки. Старая ссылка перестанет работать. "
"Все данные подписки сохранятся.</i>"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_reissue_menu_kb(key_ref, tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_update_key"),
IsAdminFilter(),
)
async def handle_update_key(
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("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
return
email = key_obj.email
await callback_query.message.edit_text(
text=f"📡 Выберите кластер, на котором пересоздать ключ <b>{email}</b>:",
reply_markup=await build_cluster_selection_kb(
session,
tg_id,
key_ref,
action="confirm_admin_key_reissue",
),
)
@router.callback_query(F.data.startswith("confirm_admin_key_reissue|"), IsAdminFilter())
async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: AsyncSession, state: FSMContext):
_, tg_id, key_ref, cluster_id = callback_query.data.split("|")
tg_id = int(tg_id)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
return
email = key_obj.email
try:
servers = await get_servers(session)
cluster_servers = servers.get(cluster_id, [])
tariffs = await get_tariffs_for_cluster(session, cluster_id)
if not tariffs:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔗 Привязать тариф",
callback_data=AdminPanelCallback(action="clusters").pack(),
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=key_ref,
).pack(),
)
)
await callback_query.message.edit_text(
f"🚫 <b>Невозможно пересоздать подписку</b>\n\n"
f"📊 <b>Информация о кластере:</b>\n<blockquote>"
f"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
f"💡 <b>Привяжите тариф к кластеру</b>",
reply_markup=builder.as_markup(),
)
return
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
if use_country_selection:
unique_countries = {srv["server_name"] for srv in cluster_servers}
await state.update_data(tg_id=tg_id, email=email, key_ref=key_ref, cluster_id=cluster_id)
builder = InlineKeyboardBuilder()
for country in sorted(unique_countries):
builder.button(
text=country,
callback_data=f"admin_reissue_country|{tg_id}|{key_ref}|{country}",
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=key_ref,
).pack(),
)
)
await callback_query.message.edit_text(
"🌍 Выберите сервер (страну) для пересоздания подписки:",
reply_markup=builder.as_markup(),
)
return
key_link = await get_key_by_email(session, email)
remnawave_link = key_link.remnawave_link if key_link else None
await update_subscription(
tg_id,
email,
session,
cluster_override=cluster_id,
remnawave_link=remnawave_link,
)
await handle_key_edit(
callback_query,
AdminUserEditorCallback(tg_id=tg_id, data=key_ref, action="view_key"),
session,
True,
)
except Exception as e:
logger.error(f"Ошибка при перевыпуске ключа {email}: {e}")
await callback_query.message.answer(f"❗ Ошибка: {e}")
@router.callback_query(F.data.startswith("admin_reissue_country|"), IsAdminFilter())
async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSession, state: FSMContext):
_, tg_id, key_ref, country = callback_query.data.split("|")
tg_id = int(tg_id)
key_obj = await resolve_callback_key(session, tg_id, key_ref)
if not key_obj:
await callback_query.message.edit_text("🚫 Ключ не найден.", reply_markup=build_editor_kb(tg_id))
return
email = key_obj.email
try:
data = await state.get_data()
cluster_id = data.get("cluster_id")
if cluster_id:
tariffs = await get_tariffs_for_cluster(session, cluster_id)
if not tariffs:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔗 Привязать тариф",
callback_data=AdminPanelCallback(action="clusters").pack(),
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=key_ref,
).pack(),
)
)
await callback_query.message.edit_text(
f"🚫 <b>Невозможно пересоздать подписку</b>\n\n"
f"📊 <b>Информация о кластере:</b>\n<blockquote>"
f"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
f"💡 <b>Привяжите тариф к кластеру</b>",
reply_markup=builder.as_markup(),
)
return
key_link = await get_key_by_email(session, email)
remnawave_link = key_link.remnawave_link if key_link else None
await update_subscription(
tg_id=tg_id,
email=email,
session=session,
country_override=country,
remnawave_link=remnawave_link,
)
await handle_key_edit(
callback_query,
AdminUserEditorCallback(tg_id=tg_id, data=key_ref, action="view_key"),
session,
True,
)
except Exception as e:
logger.error(f"Ошибка при перевыпуске ключа для страны {country}: {e}")
await callback_query.message.answer(f"❗ Ошибка: {e}")
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_recreate_key"),
IsAdminFilter(),
)
async def handle_recreate_key_start(
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
tariff_name = ""
if key_obj.tariff_id:
tariff = await get_tariff_by_id(session, key_obj.tariff_id)
if tariff:
tariff_name = tariff.get("name", "")
text = (
"<b>🔁 Пересоздание ссылки подписки</b>\n\n"
f"📦 <b>Тариф:</b> {tariff_name}\n\n"
"⚠️ <b>Будет сгенерирована новая ссылка подписки.</b>\n"
"Старая ссылка перестанет работать.\n\n"
"✅ <i>Все данные подписки сохранятся.</i>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Пересоздать",
callback_data=f"confirm_recreate|{tg_id}|{key_ref}",
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=key_ref).pack(),
)
)
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("confirm_recreate|"), IsAdminFilter())
async def handle_recreate_key_confirm(
callback_query: CallbackQuery,
session: AsyncSession,
):
_, tg_id, key_ref = callback_query.data.split("|")
tg_id = int(tg_id)
try:
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
old_email = key_obj.email
await callback_query.message.edit_text("⏳ Пересоздание ссылки подписки...")
client_id = key_obj.client_id
cluster_id = key_obj.server_id
old_link = key_obj.remnawave_link or key_obj.key
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
for _, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
cluster = [server_info]
break
if cluster:
break
if not cluster:
await callback_query.message.edit_text(
text=f"❗ Кластер {cluster_id} не найден.",
reply_markup=build_editor_kb(tg_id),
)
return
remnawave_servers = [s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave"]
if not remnawave_servers:
await callback_query.message.edit_text(
text="❗ Revoke доступен только для Remnawave. Для 3x-ui используйте перевыпуск.",
reply_markup=build_editor_kb(tg_id),
)
return
api_url = remnawave_servers[0].get("api_url")
if not api_url:
await callback_query.message.edit_text(
text="❗ У Remnawave сервера не задан api_url.",
reply_markup=build_editor_kb(tg_id),
)
return
api = RemnawaveAPI(api_url)
try:
if not REMNAWAVE_TOKEN_LOGIN_ENABLED:
await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
user_data = await api.revoke_user_subscription(client_id)
finally:
await api.aclose()
if not user_data:
await callback_query.message.edit_text(
text="❗ Не удалось выполнить revoke. Проверьте логи.",
reply_markup=build_editor_kb(tg_id),
)
return
new_link = user_data.get("subscriptionUrl")
if not new_link:
await callback_query.message.edit_text(
text="❗ Revoke выполнен, но новая ссылка не получена.",
reply_markup=build_editor_kb(tg_id),
)
return
await update_key_subscription_links(session, old_email, new_link)
try:
user_text = (
"🔄 <b>Ваша подписка была перевыпущена</b>\n\n"
f"🔗 <b>Новая ссылка подписки:</b>\n<code>{new_link}</code>\n\n"
"<i>Старая ссылка больше не работает.</i>"
)
user_kb = InlineKeyboardBuilder()
user_kb.row(
InlineKeyboardButton(
text="📱 Мои подписки",
callback_data="view_keys",
)
)
user_kb.row(
InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile",
)
)
await callback_query.bot.send_message(
chat_id=tg_id,
text=user_text,
reply_markup=user_kb.as_markup(),
)
notification_sent = True
except Exception as e:
logger.warning(f"Не удалось отправить уведомление клиенту {tg_id}: {e}")
notification_sent = False
text = (
"✅ <b>Ссылка подписки пересоздана</b>\n\n"
f"🔗 <b>Старая ссылка:</b>\n<code>{old_link}</code>\n\n"
f"🔗 <b>Новая ссылка:</b>\n<code>{new_link}</code>\n\n"
)
if notification_sent:
text += "📨 <i>Клиент уведомлён о новой ссылке.</i>"
else:
text += "⚠️ <i>Не удалось уведомить клиента.</i>"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=key_ref,
).pack(),
)
)
await callback_query.message.edit_text(
text=text,
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при revoke ключа {old_email}: {e}")
await callback_query.message.edit_text(
text=f"❗ Ошибка при пересоздании: {e}",
reply_markup=build_editor_kb(tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
IsAdminFilter(),
)
async def handle_delete_key(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: AsyncSession,
):
key_obj = await resolve_callback_key(session, callback_data.tg_id, callback_data.data)
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
client_id = key_obj.client_id
if client_id is None:
await callback_query.message.edit_text(
text="🚫 Ключ не найден!",
reply_markup=build_editor_kb(callback_data.tg_id),
)
return
await state.set_state(UserEditorState.confirm_delete_key)
await state.update_data(
delete_key_email=email,
delete_key_tg_id=int(callback_data.tg_id),
delete_key_client_id=client_id,
)
await callback_query.message.edit_text(
text="❓ Вы уверены, что хотите удалить ключ?",
reply_markup=build_key_delete_kb(callback_data.tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
UserEditorState.confirm_delete_key,
IsAdminFilter(),
)
async def handle_delete_key_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: AsyncSession,
):
data = await state.get_data()
email = data.get("delete_key_email")
expected_tg_id = data.get("delete_key_tg_id")
client_id = data.get("delete_key_client_id")
await state.clear()
if not email or int(expected_tg_id or 0) != int(callback_data.tg_id):
await callback_query.answer("Данные устарели", show_alert=True)
return
if not client_id:
key_obj = await get_key_by_email(session, email, int(callback_data.tg_id))
client_id = key_obj.client_id if key_obj else None
kb = build_editor_kb(callback_data.tg_id)
if client_id:
clusters = await get_servers(session=session)
await release_session_early(session)
async def delete_key_from_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _ in cluster_servers:
tasks.append(delete_key_from_cluster(cluster_name, email, client_id, session))
await asyncio.gather(*tasks, return_exceptions=True)
await delete_key_from_servers()
await delete_key(session, client_id)
await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb)
else:
await callback_query.message.edit_text(
text="🚫 Ключ не найден или уже удален.",
reply_markup=kb,
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user"),
IsAdminFilter(),
)
async def handle_delete_user(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=build_user_delete_kb(tg_id),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"),
IsAdminFilter(),
)
async def handle_delete_user_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: AsyncSession,
):
tg_id = callback_data.tg_id
key_records = [(row.email, row.client_id) for row in await get_keys(session, tg_id)]
await release_session_early(session)
async def delete_keys_from_servers():
try:
tasks = []
servers = await get_servers(session=session)
for email, client_id in key_records:
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id, session))
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e:
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
await delete_keys_from_servers()
try:
await delete_user_data(session, tg_id)
await callback_query.message.edit_text(
text=f"🗑️ Пользователь с ID {tg_id} был удален.",
reply_markup=build_admin_back_kb(),
)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
await callback_query.message.edit_text(
text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова.",
reply_markup=build_admin_back_kb(),
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_create_key"),
IsAdminFilter(),
)
async def handle_create_key_start(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: AsyncSession,
):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id)
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
if use_country_selection:
await state.set_state(UserEditorState.selecting_country)
countries = await get_server_names(session)
if not countries:
await callback_query.message.edit_text(
"❌ Нет доступных стран для создания ключа.",
reply_markup=build_editor_kb(tg_id),
)
return
builder = InlineKeyboardBuilder()
for country in countries:
builder.button(text=country, callback_data=country)
builder.adjust(1)
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
"🌍 <b>Выберите страну для создания ключа:</b>",
reply_markup=builder.as_markup(),
)
return
await state.set_state(UserEditorState.selecting_cluster)
servers = await get_servers(session=session)
cluster_names = list(servers.keys())
if not cluster_names:
await callback_query.message.edit_text(
"❌ Нет доступных кластеров для создания ключа.",
reply_markup=build_editor_kb(tg_id),
)
return
builder = InlineKeyboardBuilder()
for cluster in cluster_names:
builder.button(text=f"🌐 {cluster}", callback_data=cluster)
builder.adjust(2)
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
"🌐 <b>Выберите кластер для создания ключа:</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(UserEditorState.selecting_country, IsAdminFilter())
async def handle_create_key_country(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
country = callback_query.data
await state.update_data(country=country)
await state.set_state(UserEditorState.selecting_duration)
builder = InlineKeyboardBuilder()
cluster_info = await check_server_name_by_cluster(session, country)
if not cluster_info:
await callback_query.message.edit_text("❌ Сервер не найден.")
return
cluster_name = cluster_info["cluster_name"]
await state.update_data(cluster_name=cluster_name)
tariffs = await get_tariffs_for_cluster(session, cluster_name)
for tariff in tariffs:
if tariff["duration_days"] < 1:
continue
builder.button(
text=f"{tariff['name']}{tariff['price_rub']}",
callback_data=f"tariff_{tariff['id']}",
)
builder.adjust(1)
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
text=f"🕒 <b>Выберите срок действия ключа для страны <code>{country}</code>:</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(UserEditorState.selecting_cluster, IsAdminFilter())
async def handle_create_key_cluster(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
cluster_name = callback_query.data
data = await state.get_data()
tg_id = data.get("tg_id")
if not tg_id:
await callback_query.message.edit_text("❌ Ошибка: tg_id клиента не найден.")
return
await state.update_data(cluster_name=cluster_name)
await state.set_state(UserEditorState.selecting_duration)
tariffs = await get_tariffs_for_cluster(session, cluster_name)
builder = InlineKeyboardBuilder()
for tariff in tariffs:
if tariff["duration_days"] < 1:
continue
builder.button(
text=f"{tariff['name']}{tariff['price_rub']}",
callback_data=f"tariff_{tariff['id']}",
)
builder.adjust(1)
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
text=f"🕒 <b>Выберите срок действия ключа для кластера <code>{cluster_name}</code>:</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(UserEditorState.selecting_duration, IsAdminFilter())
async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
data = await state.get_data()
tg_id = data.get("tg_id", callback_query.from_user.id)
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
try:
if not callback_query.data.startswith("tariff_"):
raise ValueError("Некорректный callback_data")
tariff_id = int(callback_query.data.replace("tariff_", ""))
tariff = await get_tariff_by_id(session, tariff_id)
if not tariff:
raise ValueError("Тариф не найден.")
duration_days = tariff["duration_days"]
client_id = str(uuid.uuid4())
email = await generate_random_email(session=session)
expiry = datetime.now(tz=timezone.utc) + timedelta(days=duration_days)
expiry_ms = int(expiry.timestamp() * 1000)
if use_country_selection and "country" in data:
country = data["country"]
await create_key_on_cluster(
country,
tg_id,
client_id,
email,
expiry_ms,
plan=tariff_id,
session=session,
)
await state.clear()
await callback_query.message.edit_text(
f"✅ Ключ успешно создан для страны <b>{country}</b> на {duration_days} дней.",
reply_markup=build_editor_kb(tg_id),
)
elif "cluster_name" in data:
cluster_name = data["cluster_name"]
await create_key_on_cluster(
cluster_name,
tg_id,
client_id,
email,
expiry_ms,
plan=tariff_id,
session=session,
)
await state.clear()
await callback_query.message.edit_text(
f"✅ Ключ успешно создан в кластере <b>{cluster_name}</b> на {duration_days} дней.",
reply_markup=build_editor_kb(tg_id),
)
else:
await callback_query.message.edit_text("❌ Не удалось определить источник — страна или кластер.")
except Exception as e:
logger.error(f"[CreateKey] Ошибка при создании ключа: {e}")
await callback_query.message.edit_text(
"❌ Не удалось создать ключ. Попробуйте позже.",
reply_markup=build_editor_kb(tg_id),
)