import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.servers_kb import build_cancel_kb, build_manage_server_kb, \
build_delete_server_kb, \
build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback
router = Router()
class AdminServersEditor(StatesGroup):
waiting_for_cluster_name = State()
waiting_for_api_url = State()
waiting_for_inbound_id = State()
waiting_for_server_name = State()
waiting_for_subscription_url = State()
@router.callback_query(
AdminPanelCallback.filter(F.action == "servers"),
IsAdminFilter(),
)
async def handle_servers_editor(
callback_query: types.CallbackQuery
):
servers = await get_servers_from_db()
text = (
"🔧 Управление кластерами\n\n"
"📌 Здесь вы можете добавить новый кластер.\n\n"
"🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n"
"💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n"
"⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_clusters_editor_kb(servers),
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "clusters_add"),
IsAdminFilter(),
)
async def handle_add_cluster(
callback_query: types.CallbackQuery,
state: FSMContext
):
text = (
"🔧 Введите имя нового кластера:\n\n"
"Имя кластера должно быть уникальным!\n"
"Пример: cluster1 или us_east_1"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
)
await state.set_state(AdminServersEditor.waiting_for_cluster_name)
@router.message(
AdminServersEditor.waiting_for_cluster_name,
IsAdminFilter()
)
async def handle_cluster_name_input(
message: types.Message,
state: FSMContext
):
if not message.text:
await message.answer(
text="❌ Имя кластера не может быть пустым. Попробуйте снова."
)
return
cluster_name = message.text.strip()
await state.update_data(cluster_name=cluster_name)
text = (
f"Введите имя сервера для кластера {cluster_name}:\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"Пример: server-frankfurt1, fra1"
)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.message(
AdminServersEditor.waiting_for_server_name,
IsAdminFilter()
)
async def handle_server_name_input(
message: types.Message,
state: FSMContext
):
if not message.text:
await message.answer(
text="❌ Имя сервера не может быть пустым. Попробуйте снова."
)
return
server_name = message.text.strip()
if not await check_unique_server_name(server_name):
await message.answer(
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя."
)
return
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
await state.update_data(server_name=server_name)
text = (
f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n"
"API URL должен быть в следующем формате:\n\n"
"https://your_domain:port/panel_path\n\n"
"URL должен быть без слэша на конце!\n"
)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_api_url)
@router.message(
AdminServersEditor.waiting_for_api_url,
IsAdminFilter()
)
async def handle_api_url_input(
message: types.Message,
state: FSMContext
):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ API URL должен начинаться с https://. Попробуйте снова.",
parse_mode="HTML",
)
return
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"
"Subscription URL должен быть в следующем формате:\n\n"
"https://your_domain:port_sub/sub_path\n\n"
"URL должен быть без слэша и имени клиента на конце!\n"
"Его можно увидеть в панели 3x-ui в информации о клиенте."
)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_cancel_kb(),
)
await state.set_state(AdminServersEditor.waiting_for_subscription_url)
@router.message(
AdminServersEditor.waiting_for_subscription_url,
IsAdminFilter()
)
async def handle_subscription_url_input(
message: types.Message,
state: FSMContext
):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ subscription_url должен начинаться с https://. Попробуйте снова.",
parse_mode="HTML",
)
return
subscription_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(subscription_url=subscription_url)
text = (
f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n"
"Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n"
)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_inbound_id)
@router.message(
AdminServersEditor.waiting_for_inbound_id,
IsAdminFilter()
)
async def handle_inbound_id_input(
message: types.Message,
state: FSMContext
):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
text="❌ inbound_id должен быть числовым значением. Попробуйте снова."
)
return
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")
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
VALUES ($1, $2, $3, $4, $5)
""",
cluster_name,
server_name,
api_url,
subscription_url,
inbound_id,
)
await conn.close()
await message.edit_text(
text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=build_admin_back_kb("servers"),
)
await state.clear()
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_manage"),
IsAdminFilter()
)
async def handle_manage_cluster(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
await callback_query.message.edit_text(
text=f"🔧 Управление серверами для кластера {cluster_name}",
reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name),
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_availability"),
IsAdminFilter()
)
async def handle_check_server_availability(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
await callback_query.message.answer(
text=f"Кластер '{cluster_name}' не содержит серверов."
)
return
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n"
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
in_progress_message = await callback_query.message.answer(
text=text
)
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
for server in cluster_servers:
xui = AsyncApi(
server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD
)
try:
await xui.login()
online_users = len(await xui.client.online())
text += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
except Exception as e:
text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
await in_progress_message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_manage"),
IsAdminFilter()
)
async def handle_manage_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
servers = await get_servers_from_db()
cluster_name, server = next(
((c, s) for c, cs in servers.items()
for s in cs if s["server_name"] == server_name),
(None, None)
)
if server:
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
text = (
f"🔧 Информация о сервере {server_name}:\n\n"
f"📡 API URL: {api_url}\n"
f"🌐 Subscription URL: {subscription_url}\n"
f"🔑 Inbound ID: {inbound_id}"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
await callback_query.message.edit_text(
text="❌ Сервер не найден."
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete"),
IsAdminFilter()
)
async def handle_delete_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
await callback_query.message.edit_text(
text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=build_delete_server_kb(server_name),
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"),
IsAdminFilter()
)
async def handle_confirm_delete_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
DELETE FROM servers WHERE server_name = $1
""",
server_name,
)
await conn.close()
await callback_query.message.edit_text(
text=f"🗑️ Сервер {server_name} успешно удален.",
reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_add"),
IsAdminFilter()
)
async def handle_add_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
state: FSMContext
):
cluster_name = callback_data.data
await state.update_data(cluster_name=cluster_name)
text = (
f"Введите имя сервера для кластера {cluster_name}:\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"Пример: server-asia, server-europe"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_backup"),
IsAdminFilter()
)
async def handle_backup_cluster(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
for server in cluster_servers:
xui = AsyncApi(
server["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
await create_backup_and_send_to_admins(xui)
text = (
f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n"
f"🔔 Бэкапы отправлены в боты панелей."
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)