Implement admin servers handler
This commit is contained in:
+237
-362
@@ -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 = (
|
||||
"<b>🔧 Управление кластерами</b>\n\n"
|
||||
"<i>📌 Здесь вы можете добавить новый кластер.</i>\n\n"
|
||||
"<i>🌐 <b>Кластеры</b> — это пространство серверов, в пределах которого создается подписка.</i>\n"
|
||||
"💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n"
|
||||
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\n\n",
|
||||
parse_mode="HTML",
|
||||
reply_markup=builder.as_markup(),
|
||||
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\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 = (
|
||||
"🔧 <b>Введите имя нового кластера:</b>\n\n"
|
||||
"<b>Имя кластера должно быть уникальным и на английском языке.</b>\n"
|
||||
"<i>Пример:</i> <code>cluster2</code> или <code>us_east_1</code>",
|
||||
"<b>Имя кластера должно быть уникальным!</b>\n"
|
||||
"<i>Пример:</i> <code>cluster1</code> или <code>us_east_1</code>"
|
||||
)
|
||||
|
||||
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"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||||
"<i>Пример:</i> <code>server-frankfurt1</code>, <code>fra1</code>"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||||
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
|
||||
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"<b>Введите API URL для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||||
"API URL должен быть в следующем формате:\n\n"
|
||||
"<code>https://your_domain:port/panel_path</code>\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 должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||||
text="❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||||
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"<b>Введите subscription_url для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||||
"Subscription URL должен быть в следующем формате:\n\n"
|
||||
"<code>https://your_domain:port_sub/sub_path</code>\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 должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||||
text="❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||||
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"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||||
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||||
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\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"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
|
||||
f"<b>📡 API URL:</b> {api_url}\n"
|
||||
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
|
||||
f"<b>🔑 Inbound ID:</b> {inbound_id}",
|
||||
f"<b>🔑 Inbound ID:</b> {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"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||||
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>"
|
||||
)
|
||||
|
||||
await callback_query.message.answer(
|
||||
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||||
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
|
||||
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"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
|
||||
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>"
|
||||
)
|
||||
|
||||
await callback_query.message.answer(
|
||||
f"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
|
||||
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>",
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=builder.as_markup(),
|
||||
reply_markup=build_admin_back_kb("servers_editor"),
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user