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