from typing import Any from aiogram import F from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database import check_unique_server_name, get_servers from database.models import Server from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .base import AdminClusterStates, router from .keyboard import ( AdminClusterCallback, AdminServerCallback, build_clusters_editor_kb, build_panel_type_kb, ) @router.callback_query( AdminPanelCallback.filter(F.action == "clusters"), IsAdminFilter(), ) async def handle_servers(callback_query: CallbackQuery, session: AsyncSession): servers = await get_servers(session, include_enabled=True) text = ( "🔧 Управление кластерами\n\n" "
" "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер." "
\n\n" "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" ) message = callback_query.message markup = build_clusters_editor_kb(servers) if message and message.text: await message.edit_text(text=text, reply_markup=markup) else: try: await message.delete() except Exception: pass await message.answer(text=text, reply_markup=markup) @router.callback_query(AdminClusterCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): text = ( "🔧 Введите имя нового кластера:\n\n" "Имя должно быть уникальным!\n" "Имя не должно превышать 12 символов!\n\n" "Пример: cluster1 или us_east_1" ) await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("clusters")) await state.set_state(AdminClusterStates.waiting_for_cluster_name) @router.message(AdminClusterStates.waiting_for_cluster_name, IsAdminFilter()) async def handle_cluster_name_input(message: Message, state: FSMContext): if not message.text: await message.answer( text="❌ Имя кластера не может быть пустым! Попробуйте снова.", reply_markup=build_admin_back_kb("clusters"), ) return if len(message.text) > 12: await message.answer( text="❌ Имя кластера не должно превышать 12 символов! Попробуйте снова.", reply_markup=build_admin_back_kb("clusters"), ) return cluster_name = message.text.strip() await state.update_data(cluster_name=cluster_name) text = ( f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию и номер сервера в имени.\n\n" "Пример: de1, fra1, fi2" ) await message.answer( text=text, reply_markup=build_admin_back_kb("clusters"), ) await state.set_state(AdminClusterStates.waiting_for_server_name) @router.message(AdminClusterStates.waiting_for_server_name, IsAdminFilter()) async def handle_server_name_input(message: Message, state: FSMContext, session: Any): if not message.text: await message.answer( text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("clusters"), ) return server_name = message.text.strip() if len(server_name) > 12: await message.answer( text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.", reply_markup=build_admin_back_kb("clusters"), ) return user_data = await state.get_data() cluster_name = user_data.get("cluster_name") if not await check_unique_server_name(session, server_name, cluster_name): await message.answer( text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.", reply_markup=build_admin_back_kb("clusters"), ) return await state.update_data(server_name=server_name) text = ( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" "🔍 Ссылку можно найти в адресной строке браузера при входе в панель управления сервером.\n\n" "ℹ️ Формат для 3X-UI:\n" "https://your-domain.com:port/panel_path/\n\n" "ℹ️ Формат для Remnawave:\n" "https://your-domain.com/api" ) await message.answer( text=text, reply_markup=build_admin_back_kb("clusters"), ) await state.set_state(AdminClusterStates.waiting_for_api_url) @router.message(AdminClusterStates.waiting_for_api_url, IsAdminFilter()) async def handle_api_url_input(message: Message, state: FSMContext): api_url = message.text.strip().rstrip("/") user_data = await state.get_data() cluster_name = user_data.get("cluster_name") server_name = user_data.get("server_name") await state.update_data(api_url=api_url) text = ( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" "Если вы используете Remnawave — введите 0\n\n" "Формат: https://your_domain:port/sub_path" ) await message.answer(text=text, reply_markup=build_admin_back_kb("clusters")) await state.set_state(AdminClusterStates.waiting_for_subscription_url) @router.message(AdminClusterStates.waiting_for_subscription_url, IsAdminFilter()) async def handle_subscription_url_input(message: Message, state: FSMContext): raw = message.text.strip() subscription_url = None if raw == "0" else raw.rstrip("/") user_data = await state.get_data() cluster_name = user_data.get("cluster_name") server_name = user_data.get("server_name") await state.update_data(subscription_url=subscription_url) await message.answer( text=f"Введите inbound_id/Squads для сервера {server_name} в кластере {cluster_name}:\n\n" f"Для Remnawave это UUID Squads, для 3x-ui — просто ID (например, 1).", reply_markup=build_admin_back_kb("clusters"), ) await state.set_state(AdminClusterStates.waiting_for_inbound_id) @router.message(AdminClusterStates.waiting_for_inbound_id, IsAdminFilter()) async def handle_inbound_id_input(message: Message, state: FSMContext): inbound_id = message.text.strip() await state.update_data(inbound_id=inbound_id) await message.answer( text=( "🧩 Выберите тип панели для этого сервера:\n\n" "⚠️ Внимание: Некоторые функции Remnawave находятся в разработке.\n" "Поддержка режима выбора стран — ограничена." ), reply_markup=build_panel_type_kb(), ) @router.callback_query( AdminClusterCallback.filter(F.action.in_(["panel_3xui", "panel_remnawave"])), IsAdminFilter(), ) async def handle_panel_type_selection( callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext, session: AsyncSession, ): panel_type = "3x-ui" if callback_data.action == "panel_3xui" else "remnawave" user_data = await state.get_data() cluster_name = user_data.get("cluster_name") server_name = user_data.get("server_name") api_url = user_data.get("api_url") subscription_url = user_data.get("subscription_url") inbound_id = user_data.get("inbound_id") result = await session.execute(select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1)) row = result.first() tariff_group = row[0] if row else None new_server = Server( cluster_name=cluster_name, server_name=server_name, api_url=api_url, subscription_url=subscription_url, inbound_id=inbound_id, panel_type=panel_type, tariff_group=tariff_group, ) session.add(new_server) await callback_query.message.edit_text( text=f"✅ Сервер {server_name} с панелью {panel_type} успешно добавлен в кластер {cluster_name}!", reply_markup=build_admin_back_kb("clusters"), ) await state.clear() @router.callback_query(AdminServerCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_add_server(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext): cluster_name = callback_data.data await state.update_data(cluster_name=cluster_name) text = ( f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию и номер сервера в имени.\n\n" "Пример: de1, fra1, fi2" ) await callback_query.message.edit_text( text=text, reply_markup=build_admin_back_kb("clusters"), ) await state.set_state(AdminClusterStates.waiting_for_server_name)