import asyncpg from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from database import check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter router = Router() class UserEditorState(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(F.data == "servers_editor", IsAdminFilter()) async def handle_servers_editor(callback_query: types.CallbackQuery): servers = await get_servers_from_db() builder = InlineKeyboardBuilder() for cluster_name, cluster_servers in servers.items(): builder.row( InlineKeyboardButton( text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}" ) ) builder.row( InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster") ) builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) await callback_query.message.answer( "🔧 Управление кластерами\n\n" "📌 Здесь вы можете добавить новый кластер.\n\n" "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n", parse_mode="HTML", reply_markup=builder.as_markup(), ) @router.callback_query(F.data == "add_cluster", IsAdminFilter()) async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext): await callback_query.message.answer( "🔧 Введите имя нового кластера:\n\n" "Имя кластера должно быть уникальным и на английском языке.\n" "Пример: cluster2 или us_east_1", parse_mode="HTML", ) await state.set_state(UserEditorState.waiting_for_cluster_name) @router.message(UserEditorState.waiting_for_cluster_name, IsAdminFilter()) async def handle_cluster_name_input(message: types.Message, state: FSMContext): cluster_name = message.text.strip() if cluster_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔧 Управление кластерами", callback_data="servers_editor" ) ) await message.answer( "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), ) return if not cluster_name: await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.") return await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") ) await message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию сервера в имени.\n\n" "Пример: server-asia, server-europe", parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_server_name) @router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) async def handle_server_name_input(message: types.Message, state: FSMContext): server_name = message.text.strip() if server_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔧 Управление кластерами", callback_data="servers_editor" ) ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), ) return if not server_name: await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.") return server_unique = await check_unique_server_name(server_name) if not server_unique: await message.answer( "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." ) return user_data = await state.get_data() cluster_name = user_data.get("cluster_name") await state.update_data(server_name=server_name) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") ) await message.answer( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" "API URL должен быть в следующем формате:\n\n" "https://your_domain:port/panel_path\n\n" "URL должен быть без слэша на конце!\n", parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_api_url) @router.message(UserEditorState.waiting_for_api_url, IsAdminFilter()) async def handle_api_url_input(message: types.Message, state: FSMContext): api_url = message.text.strip() if api_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔧 Управление кластерами", callback_data="servers_editor" ) ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), ) return if not api_url.startswith("https://"): await message.answer( "❌ API URL должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return api_url = api_url.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) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") ) await message.answer( 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 в информации о клиенте.", parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_subscription_url) @router.message(UserEditorState.waiting_for_subscription_url, IsAdminFilter()) async def handle_subscription_url_input(message: types.Message, state: FSMContext): subscription_url = message.text.strip() if subscription_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔧 Управление кластерами", callback_data="servers_editor" ) ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), ) return if not subscription_url.startswith("https://"): await message.answer( "❌ subscription_url должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return subscription_url = subscription_url.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) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") ) await message.answer( f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n", parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_inbound_id) @router.message(UserEditorState.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( "❌ 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() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔙 Назад к кластерам", callback_data="servers_editor" ) ) await message.answer( f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", reply_markup=builder.as_markup(), ) await state.clear() @router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter()) async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext): cluster_name = callback_query.data.split("|")[1] servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) builder = InlineKeyboardBuilder() for server in cluster_servers: builder.row( InlineKeyboardButton( text=f"🌍 {server['server_name']}", callback_data=f"manage_server|{server['server_name']}", ) ) builder.row( InlineKeyboardButton( text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}" ) ) builder.row( InlineKeyboardButton( text="🌐 Доступность серверов", callback_data=f"server_availability|{cluster_name}", ) ) builder.row( InlineKeyboardButton( text="💾 Создать бэкап кластера", callback_data=f"backup_cluster|{cluster_name}", ) ) builder.row( InlineKeyboardButton( text="🔙 Назад в управление кластерами", callback_data="servers_editor" ) ) await callback_query.message.answer( f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter()) async def handle_check_server_availability(callback_query: types.CallbackQuery): cluster_name = callback_query.data.split("|")[1] servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) if not cluster_servers: await callback_query.answer(f"Кластер '{cluster_name}' не содержит серверов.") return in_progress_message = await callback_query.message.answer( f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n" "Это может занять до 1 минуты, пожалуйста, подождите..." ) availability_message = ( 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()) availability_message += ( f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" ) except Exception as e: availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" ) ) await in_progress_message.edit_text( availability_message, reply_markup=builder.as_markup() ) await callback_query.answer() @router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter()) async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext): server_name = callback_query.data.split("|")[1] servers = await get_servers_from_db() server = None cluster_name = None for cluster, cluster_servers in servers.items(): server = next( (s for s in cluster_servers if s["server_name"] == server_name), None ) if server: cluster_name = cluster break if server: api_url = server["api_url"] subscription_url = server["subscription_url"] inbound_id = server["inbound_id"] builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🗑️ Удалить", callback_data=f"delete_server|{server_name}" ) ) builder.row( InlineKeyboardButton( text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" ) ) await callback_query.message.answer( f"🔧 Информация о сервере {server_name}:\n\n" f"📡 API URL: {api_url}\n" f"🌐 Subscription URL: {subscription_url}\n" f"🔑 Inbound ID: {inbound_id}", parse_mode="HTML", reply_markup=builder.as_markup(), ) else: await callback_query.message.answer("❌ Сервер не найден.") @router.callback_query(F.data.startswith("delete_server|"), IsAdminFilter()) async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMContext): server_name = callback_query.data.split("|")[1] builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="✅ Да", callback_data=f"confirm_delete_server|{server_name}" ), InlineKeyboardButton( text="❌ Нет", callback_data=f"manage_server|{server_name}" ), ) await callback_query.message.answer( f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) async def handle_confirm_delete_server( callback_query: types.CallbackQuery, state: FSMContext ): server_name = callback_query.data.split("|")[1] conn = await asyncpg.connect(DATABASE_URL) await conn.execute( """ DELETE FROM servers WHERE server_name = $1 """, server_name, ) await conn.close() builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🔙 Назад в управление кластерами", callback_data="servers_editor" ) ) await callback_query.message.answer( f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup() ) @router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) async def handle_add_server(callback_query: types.CallbackQuery, state: FSMContext): cluster_name = callback_query.data.split("|")[1] await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") ) await callback_query.message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию сервера в имени.\n\n" "Пример: server-asia, server-europe", parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_server_name) @router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter()) async def handle_backup_cluster(callback_query: types.CallbackQuery): cluster_name = callback_query.data.split("|")[1] 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) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) await callback_query.message.answer( f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" f"🔔 Бэкапы отправлены в боты панелей.", parse_mode="HTML", reply_markup=builder.as_markup(), ) await callback_query.answer()