362 lines
16 KiB
Python
362 lines
16 KiB
Python
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
|
||
import asyncpg
|
||
from typing import Any
|
||
|
||
from config import DATABASE_URL
|
||
from database import get_servers_from_db, check_unique_server_name
|
||
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(
|
||
"<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(),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == "add_cluster", IsAdminFilter())
|
||
async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext):
|
||
await callback_query.message.answer(
|
||
"🔧 <b>Введите имя нового кластера:</b>\n\n"
|
||
"<b>Имя кластера должно быть уникальным и на английском языке.</b>\n"
|
||
"<i>Пример:</i> <code>cluster2</code> или <code>us_east_1</code>",
|
||
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"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
|
||
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"<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(),
|
||
)
|
||
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 должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
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"<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(),
|
||
)
|
||
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 должен начинаться с <code>https://</code>. Попробуйте снова.",
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
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"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\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="servers_editor"))
|
||
|
||
await callback_query.message.answer(
|
||
f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=builder.as_markup()
|
||
)
|
||
|
||
|
||
@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"<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}",
|
||
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"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
|
||
"Рекомендуется указать локацию сервера в имени.\n\n"
|
||
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
|
||
parse_mode="HTML",
|
||
reply_markup=builder.as_markup(),
|
||
)
|
||
|
||
await state.set_state(UserEditorState.waiting_for_server_name)
|