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()