From 36a4ee339c163287a7a4be10eb48b7fb9657039b Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:07:57 +0300 Subject: [PATCH] Implement admin servers handler --- handlers/admin/admin_servers.py | 599 +++++++++++++------------------- keyboards/admin/servers_kb.py | 120 +++++++ 2 files changed, 357 insertions(+), 362 deletions(-) create mode 100644 keyboards/admin/servers_kb.py diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index a2ee4ed4..0cab2ec4 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,24 +1,22 @@ -import asyncio - 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 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 handlers.keys.key_utils import create_key_on_cluster -from logger import logger +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 UserEditorState(StatesGroup): +class AdminServersEditor(StatesGroup): waiting_for_cluster_name = State() waiting_for_api_url = State() waiting_for_inbound_id = State() @@ -26,112 +24,103 @@ class UserEditorState(StatesGroup): waiting_for_subscription_url = State() -@router.callback_query(F.data == "servers_editor", IsAdminFilter()) -async def handle_servers_editor(callback_query: types.CallbackQuery): +@router.callback_query( + AdminPanelCallback.filter(F.action == "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( + text = ( "🔧 Управление кластерами\n\n" "📌 Здесь вы можете добавить новый кластер.\n\n" "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" - "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n", - parse_mode="HTML", - reply_markup=builder.as_markup(), + "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" ) - -@router.callback_query(F.data == "add_cluster", IsAdminFilter()) -async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext): await callback_query.message.answer( + 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" - "Пример: cluster2 или us_east_1", + "Имя кластера должно быть уникальным!\n" + "Пример: cluster1 или us_east_1" + ) + + await callback_query.message.answer( + text=text, parse_mode="HTML", ) - await state.set_state(UserEditorState.waiting_for_cluster_name) + await state.set_state(AdminServersEditor.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" - ) - ) +@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( - "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), + text="❌ Имя кластера не может быть пустым. Попробуйте снова." ) return - if not cluster_name: - await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.") - return - + cluster_name = message.text.strip() await state.update_data(cluster_name=cluster_name) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-frankfurt1, fra1" ) await message.answer( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_server_name) + + await state.set_state(AdminServersEditor.waiting_for_server_name) -@router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) -async def handle_server_name_input(message: types.Message, state: FSMContext): +@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 server_name == "❌ Отменить": - await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + if not await check_unique_server_name(server_name): 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( - "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." + text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." ) return @@ -139,124 +128,109 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): 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( + text = ( 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(), + "URL должен быть без слэша на конце!\n" ) - await state.set_state(UserEditorState.waiting_for_api_url) + + await message.answer( + text=text, + parse_mode="HTML", + reply_markup=build_admin_back_kb("servers_editor"), + ) + + await state.set_state(AdminServersEditor.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" - ) - ) +@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( - "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), - ) - return - - if not api_url.startswith("https://"): - await message.answer( - "❌ API URL должен начинаться с https://. Попробуйте снова.", + text="❌ API URL должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return - api_url = api_url.rstrip("/") + 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) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) - - await message.answer( + 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 в информации о клиенте.", - parse_mode="HTML", - reply_markup=builder.as_markup(), + "Его можно увидеть в панели 3x-ui в информации о клиенте." ) - await state.set_state(UserEditorState.waiting_for_subscription_url) + + await message.answer( + text=text, + parse_mode="HTML", + reply_markup=build_cancel_kb(), + ) + + await state.set_state(AdminServersEditor.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" - ) - ) +@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( - "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), - ) - return - - if not subscription_url.startswith("https://"): - await message.answer( - "❌ subscription_url должен начинаться с https://. Попробуйте снова.", + text="❌ subscription_url должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return - subscription_url = subscription_url.rstrip("/") + 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) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" + "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n" ) await message.answer( - f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" - "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_inbound_id) + await state.set_state(AdminServersEditor.waiting_for_inbound_id) -@router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter()) -async def handle_inbound_id_input(message: types.Message, state: FSMContext): +@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( - "❌ inbound_id должен быть числовым значением. Попробуйте снова." + text="❌ inbound_id должен быть числовым значением. Попробуйте снова." ) return @@ -280,155 +254,62 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): ) 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(), + text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", + reply_markup=build_admin_back_kb("servers_editor"), ) 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] +@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, []) - 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=f"sync_cluster|{cluster_name}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔙 Назад в управление кластерами", callback_data="servers_editor" - ) - ) - await callback_query.message.answer( - f"🔧 Управление серверами для кластера {cluster_name}", - reply_markup=builder.as_markup(), + text=f"🔧 Управление серверами для кластера {cluster_name}", + reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), ) -@router.callback_query(F.data.startswith("sync_cluster|"), IsAdminFilter()) -async def sync_cluster_handler(callback_query: types.CallbackQuery): - """Обработчик для синхронизации ключей на всех серверах выбранного кластера.""" - cluster_name = callback_query.data.split("|")[1] - - conn = await asyncpg.connect(DATABASE_URL) - try: - query_keys = """ - SELECT tg_id, client_id, email, expiry_time - FROM keys - WHERE server_id = $1 - """ - keys_to_sync = await conn.fetch(query_keys, cluster_name) - - if not keys_to_sync: - await callback_query.message.answer( - f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", - reply_markup=InlineKeyboardBuilder() - .row( - InlineKeyboardButton( - text="🔙 Назад", callback_data="servers_editor" - ) - ) - .as_markup(), - ) - return - - tasks = [] - - for key in keys_to_sync: - tasks.append( - asyncio.create_task( - create_key_on_cluster( - cluster_name, - key["tg_id"], - key["client_id"], - key["email"], - key["expiry_time"], - ) - ) - ) - - await asyncio.gather(*tasks) - await callback_query.message.answer( - f"✅ Ключи успешно синхронизированы для кластера {cluster_name}.", - reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) - .as_markup(), - ) - except Exception as e: - logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}") - await callback_query.message.answer( - f"❌ Произошла ошибка при синхронизации: {e}", - reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) - .as_markup(), - ) - finally: - await conn.close() - - -@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] +@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.answer(f"Кластер '{cluster_name}' не содержит серверов.") + await callback_query.answer( + text=f"Кластер '{cluster_name}' не содержит серверов." + ) return - in_progress_message = await callback_query.message.answer( + text = ( f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n" "Это может занять до 1 минуты, пожалуйста, подождите..." ) - availability_message = ( + in_progress_message = await callback_query.message.answer( + text=text + ) + + text = ( f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" ) @@ -441,97 +322,84 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): await xui.login() online_users = len(await xui.client.online()) - availability_message += ( + text += ( 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}" - ) - ) + text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" await in_progress_message.edit_text( - availability_message, reply_markup=builder.as_markup() + text=text, + reply_markup=build_admin_back_kb("servers_editor") ) 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] - +@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() - 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 + 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"] - 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( + text = ( f"🔧 Информация о сервере {server_name}:\n\n" f"📡 API URL: {api_url}\n" f"🌐 Subscription URL: {subscription_url}\n" - f"🔑 Inbound ID: {inbound_id}", + f"🔑 Inbound ID: {inbound_id}" + ) + + await callback_query.message.answer( + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_manage_server_kb(server_name, cluster_name), ) 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}" - ), - ) +@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.answer( - f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", - reply_markup=builder.as_markup(), + text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", + reply_markup=build_delete_server_kb(server_name), ) -@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), + IsAdminFilter() +) async def handle_confirm_delete_server( - callback_query: types.CallbackQuery, state: FSMContext + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback ): - server_name = callback_query.data.split("|")[1] + server_name = callback_data.data conn = await asyncpg.connect(DATABASE_URL) await conn.execute( @@ -542,43 +410,49 @@ async def handle_confirm_delete_server( ) 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() + text=f"🗑️ Сервер {server_name} успешно удален.", + reply_markup=build_admin_back_kb("servers_editor") ) -@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] +@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) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-asia, server-europe" ) await callback_query.message.answer( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_server_name) + await state.set_state(AdminServersEditor.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] +@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, []) @@ -591,13 +465,14 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery): ) await create_backup_and_send_to_admins(xui) - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + text = ( + f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" + f"🔔 Бэкапы отправлены в боты панелей." + ) await callback_query.message.answer( - f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" - f"🔔 Бэкапы отправлены в боты панелей.", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) await callback_query.answer() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py new file mode 100644 index 00000000..702feed9 --- /dev/null +++ b/keyboards/admin/servers_kb.py @@ -0,0 +1,120 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn + + +class AdminServerEditorCallback(CallbackData, prefix='admin_server_editor'): + action: str + data: str + + +def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for cluster_name in servers: + builder.button( + text=f"⚙️ {cluster_name}", + callback_data=AdminServerEditorCallback( + action="clusters_manage", + data=cluster_name + ).pack() + ) + + builder.button( + text="➕ Добавить кластер", + callback_data=AdminPanelCallback(action="clusters_add").pack() + ) + builder.row( + build_admin_back_btn("admin") + ) + return builder.as_markup() + + +def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.button( + text=f"🌍 {server['server_name']}", + callback_data=AdminServerEditorCallback( + action="servers_manage", + data=server["server_name"] + ).pack() + ) + + builder.button( + text="➕ Добавить сервер", + callback_data=AdminServerEditorCallback( + action="servers_add", + data=cluster_name + ).pack() + ) + builder.button( + text="🌐 Доступность серверов", + callback_data=AdminServerEditorCallback( + action="servers_availability", + data=cluster_name + ).pack() + ) + builder.button( + text="💾 Создать бэкап кластера", + callback_data=AdminServerEditorCallback( + action="clusters_backup", + data=cluster_name + ).pack() + ) + builder.row( + build_admin_back_btn("servers_editor") + ) + return builder.as_markup() + + +def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="🗑️ Удалить", + callback_data=AdminServerEditorCallback( + action="servers_delete", + data=server_name + ).pack() + ) + builder.button( + text="🔙 Назад", + callback_data=AdminServerEditorCallback( + action="clusters_manage", + data=cluster_name + ).pack() + ) + return builder.as_markup() + + +def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="✅ Да", + callback_data=AdminServerEditorCallback( + action="servers_delete_confirm", + data=server_name + ).pack() + ) + builder.button( + text="🔙 Назад", + callback_data=AdminServerEditorCallback( + action="servers_manage", + data=server_name + ).pack() + ) + return builder.as_markup() + + +def build_cancel_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="❌ Отменить", + callback_data="servers_editor" + ) + ) + return builder.as_markup()