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 = ( "🔄 Перевыпуск подписки\n\n" "📦 Полный перевыпуск\n" "Пересоздаёт подписку на сервере с возможностью выбора кластера. " "Используйте для переноса на другой сервер или обновления данных.\n\n" "🔗 Сменить ссылку\n" "Генерирует новую ссылку подписки. Старая ссылка перестанет работать. " "Все данные подписки сохранятся." ) 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"📡 Выберите кластер, на котором пересоздать ключ {email}:", 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"🚫 Невозможно пересоздать подписку\n\n" f"📊 Информация о кластере:\n
" f"🌐 Кластер: {cluster_id}\n" f"⚠️ Статус: Нет привязанного тарифа\n
" f"💡 Привяжите тариф к кластеру", 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"🚫 Невозможно пересоздать подписку\n\n" f"📊 Информация о кластере:\n
" f"🌐 Кластер: {cluster_id}\n" f"⚠️ Статус: Нет привязанного тарифа\n
" f"💡 Привяжите тариф к кластеру", 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 = ( "🔁 Пересоздание ссылки подписки\n\n" f"📦 Тариф: {tariff_name}\n\n" "⚠️ Будет сгенерирована новая ссылка подписки.\n" "Старая ссылка перестанет работать.\n\n" "✅ Все данные подписки сохранятся." ) 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 = ( "🔄 Ваша подписка была перевыпущена\n\n" f"🔗 Новая ссылка подписки:\n{new_link}\n\n" "Старая ссылка больше не работает." ) 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 = ( "✅ Ссылка подписки пересоздана\n\n" f"🔗 Старая ссылка:\n{old_link}\n\n" f"🔗 Новая ссылка:\n{new_link}\n\n" ) if notification_sent: text += "📨 Клиент уведомлён о новой ссылке." else: text += "⚠️ Не удалось уведомить клиента." 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( "🌍 Выберите страну для создания ключа:", 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( "🌐 Выберите кластер для создания ключа:", 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"🕒 Выберите срок действия ключа для страны {country}:", 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"🕒 Выберите срок действия ключа для кластера {cluster_name}:", 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"✅ Ключ успешно создан для страны {country} на {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"✅ Ключ успешно создан в кластере {cluster_name} на {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), )