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)