add full server management with cluster transfer support

This commit is contained in:
Capybara-z
2025-06-12 23:47:18 +03:00
parent 1fb7f8d412
commit 9ade45347c
3 changed files with 389 additions and 7 deletions
+125 -2
View File
@@ -1,8 +1,8 @@
from sqlalchemy import delete, insert, select
from sqlalchemy import delete, insert, select, update, func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Server
from database.models import Server, Key
from logger import logger
@@ -118,3 +118,126 @@ async def get_cluster_name_by_server(
result = await session.execute(stmt)
row = result.scalar_one_or_none()
return row
async def get_server_by_name(session: AsyncSession, server_name: str) -> dict | None:
try:
stmt = select(Server).where(Server.server_name == server_name)
result = await session.execute(stmt)
server = result.scalar_one_or_none()
if server:
return {
"id": server.id,
"cluster_name": server.cluster_name,
"server_name": server.server_name,
"api_url": server.api_url,
"subscription_url": server.subscription_url,
"inbound_id": server.inbound_id,
"panel_type": server.panel_type,
"enabled": server.enabled,
"max_keys": server.max_keys,
"tariff_group": server.tariff_group,
}
return None
except SQLAlchemyError as e:
logger.error(f"Ошибка при получении сервера {server_name}: {e}")
return None
async def update_server_field(
session: AsyncSession, server_name: str, field: str, value: any
) -> bool:
try:
stmt = update(Server).where(Server.server_name == server_name).values(**{field: value})
await session.execute(stmt)
await session.commit()
logger.info(f"✅ Поле {field} сервера {server_name} обновлено на {value}")
return True
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при обновлении поля {field} сервера {server_name}: {e}")
await session.rollback()
return False
async def update_server_name_with_keys(
session: AsyncSession, old_name: str, new_name: str
) -> bool:
try:
from sqlalchemy import update
from database.models import Key
if not await check_unique_server_name(session, new_name):
logger.error(f"❌ Сервер с именем {new_name} уже существует")
return False
stmt_server = update(Server).where(Server.server_name == old_name).values(server_name=new_name)
await session.execute(stmt_server)
stmt_keys = update(Key).where(Key.server_id == old_name).values(server_id=new_name)
await session.execute(stmt_keys)
await session.commit()
logger.info(f"✅ Сервер переименован с {old_name} на {new_name}")
return True
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при переименовании сервера {old_name}: {e}")
await session.rollback()
return False
async def get_available_clusters(session: AsyncSession) -> list[str]:
try:
stmt = select(Server.cluster_name).distinct().order_by(Server.cluster_name)
result = await session.execute(stmt)
return [row[0] for row in result.all()]
except SQLAlchemyError as e:
logger.error(f"Ошибка при получении списка кластеров: {e}")
return []
async def update_server_cluster(
session: AsyncSession,
server_name: str,
new_cluster: str
) -> bool:
try:
server_data = await get_server_by_name(session, server_name)
if not server_data:
return False
old_cluster = server_data["cluster_name"]
stmt_remaining = select(func.count()).where(
(Server.cluster_name == old_cluster) & (Server.server_name != server_name)
)
result = await session.execute(stmt_remaining)
remaining_servers = result.scalar_one()
if remaining_servers == 0:
stmt_update_keys = update(Key).where(
Key.server_id == old_cluster
).values(server_id=new_cluster)
await session.execute(stmt_update_keys)
stmt_new_cluster = select(Server.tariff_group).where(
Server.cluster_name == new_cluster
).limit(1)
result = await session.execute(stmt_new_cluster)
new_tariff_group = result.scalar_one_or_none()
stmt_update = update(Server).where(
Server.server_name == server_name
).values(
cluster_name=new_cluster,
tariff_group=new_tariff_group
)
await session.execute(stmt_update)
await session.commit()
logger.info(f"✅ Сервер {server_name} перемещен в кластер {new_cluster} с обновлением тарифной группы")
return True
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при обновлении кластера сервера {server_name}: {e}")
await session.rollback()
return False
+107 -3
View File
@@ -1,5 +1,5 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.buttons import BACK
@@ -37,8 +37,8 @@ def build_manage_server_kb(
)
builder.button(
text="✏️ Сменить название",
callback_data=AdminServerCallback(action="rename", data=server_name).pack(),
text="✏️ Редактировать",
callback_data=f"edit_server|{server_name}",
)
builder.button(
@@ -48,3 +48,107 @@ def build_manage_server_kb(
builder.adjust(1)
return builder.as_markup()
def build_edit_server_fields_kb(server_name: str, server_data: dict) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📝 Имя сервера",
callback_data=f"edit_server_field|{server_name}|server_name"
)
)
builder.row(
InlineKeyboardButton(
text="🗂 Кластер",
callback_data=f"edit_server_field|{server_name}|cluster_name"
)
)
builder.row(
InlineKeyboardButton(
text="🌐 API URL",
callback_data=f"edit_server_field|{server_name}|api_url"
)
)
if server_data.get("subscription_url"):
builder.row(
InlineKeyboardButton(
text="📡 Subscription URL",
callback_data=f"edit_server_field|{server_name}|subscription_url"
)
)
builder.row(
InlineKeyboardButton(
text="🔑 Inbound ID",
callback_data=f"edit_server_field|{server_name}|inbound_id"
)
)
builder.row(
InlineKeyboardButton(
text="⚙️ Тип панели",
callback_data=f"select_panel_type|{server_name}"
)
)
builder.row(
InlineKeyboardButton(
text="⬅️ Назад",
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
)
)
return builder.as_markup()
def build_panel_type_selection_kb(server_name: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="🌐 3x-ui", callback_data=f"set_panel_type|{server_name}|3x-ui")],
[InlineKeyboardButton(text="🌀 remnawave", callback_data=f"set_panel_type|{server_name}|remnawave")],
[InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"edit_server|{server_name}"
)]
]
)
def build_cluster_selection_kb(server_name: str, clusters: list[str]) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster in clusters:
builder.row(
InlineKeyboardButton(
text=cluster,
callback_data=f"set_cluster|{server_name}|{cluster}"
)
)
builder.row(
InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"edit_server|{server_name}"
)
)
return builder.as_markup()
def build_cancel_edit_kb(server_name: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="❌ Отменить",
callback_data=f"edit_server|{server_name}"
)
]
]
)
+157 -2
View File
@@ -8,11 +8,25 @@ from sqlalchemy.ext.asyncio import AsyncSession
from database import get_servers
from database.models import Key, Server
from database.servers import (
get_server_by_name,
update_server_field,
update_server_name_with_keys,
get_available_clusters,
update_server_cluster,
)
from filters.admin import IsAdminFilter
from handlers.buttons import BACK
from ..panel.keyboard import build_admin_back_kb
from .keyboard import AdminServerCallback, build_manage_server_kb
from .keyboard import (
AdminServerCallback,
build_manage_server_kb,
build_edit_server_fields_kb,
build_panel_type_selection_kb,
build_cluster_selection_kb,
build_cancel_edit_kb,
)
router = Router()
@@ -21,6 +35,11 @@ class ServerLimitState(StatesGroup):
waiting_for_limit = State()
class ServerEditState(StatesGroup):
choosing_field = State()
editing_value = State()
@router.callback_query(
AdminServerCallback.filter(F.action == "manage"), IsAdminFilter()
)
@@ -46,14 +65,22 @@ async def handle_server_manage(
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
panel_type = server.get("panel_type") or "не указан"
max_keys = server.get("max_keys")
limit_display = f"{max_keys}" if max_keys else "не задан"
text = (
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>🗂 Кластер:</b> {cluster_name}\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
)
if subscription_url:
text += f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
text += (
f"<b>🔑 Inbound ID:</b> {inbound_id}\n"
f"<b>⚙️ Тип панели:</b> {panel_type}\n"
f"<b>📈 Лимит ключей:</b> {limit_display}"
)
@@ -353,3 +380,131 @@ async def save_server_limit(
except ValueError:
await message.answer("❌ Введите корректное целое число (0 = без лимита)")
@router.callback_query(F.data.startswith("edit_server|"), IsAdminFilter())
async def start_edit_server(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
server_name = callback.data.split("|")[1]
await state.clear()
server_data = await get_server_by_name(session, server_name)
if not server_data:
await callback.message.edit_text("❌ Сервер не найден.")
return
await callback.message.edit_text(
f"<b>✏️ Редактирование сервера: {server_name}</b>\n\n"
"Выберите поле для редактирования:",
reply_markup=build_edit_server_fields_kb(server_name, server_data)
)
@router.callback_query(F.data.startswith("edit_server_field|"), IsAdminFilter())
async def ask_new_field_value(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
_, server_name, field = callback.data.split("|")
if field == "cluster_name":
clusters = await get_available_clusters(session)
await callback.message.edit_text(
f"<b>🗂 Выберите кластер для сервера {server_name}:</b>",
reply_markup=build_cluster_selection_kb(server_name, clusters)
)
return
await state.update_data(server_name=server_name, field=field)
await state.set_state(ServerEditState.editing_value)
field_names = {
"server_name": "имя сервера",
"api_url": "API URL",
"subscription_url": "Subscription URL",
"inbound_id": "Inbound ID",
}
await callback.message.edit_text(
f"✏️ Введите новое значение для <b>{field_names.get(field, field)}</b>:",
reply_markup=build_cancel_edit_kb(server_name)
)
@router.callback_query(F.data.startswith("select_panel_type|"), IsAdminFilter())
async def select_panel_type(callback: CallbackQuery):
server_name = callback.data.split("|")[1]
await callback.message.edit_text(
f"<b>⚙️ Выберите тип панели для сервера {server_name}:</b>",
reply_markup=build_panel_type_selection_kb(server_name)
)
@router.callback_query(F.data.startswith("set_panel_type|"), IsAdminFilter())
async def set_panel_type(callback: CallbackQuery, session: AsyncSession):
_, server_name, panel_type = callback.data.split("|")
success = await update_server_field(session, server_name, "panel_type", panel_type)
if success:
await callback.message.edit_text(
f"✅ Тип панели сервера {server_name} изменен на {panel_type}",
reply_markup=InlineKeyboardBuilder().button(
text="⬅️ Назад к серверу",
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
).as_markup()
)
else:
await callback.message.edit_text("❌ Ошибка при изменении типа панели")
@router.callback_query(F.data.startswith("set_cluster|"), IsAdminFilter())
async def set_cluster(callback: CallbackQuery, session: AsyncSession):
_, server_name, new_cluster = callback.data.split("|")
success = await update_server_cluster(session, server_name, new_cluster)
if success:
await callback.message.edit_text(
f"✅ Кластер сервера {server_name} изменен на {new_cluster}",
reply_markup=InlineKeyboardBuilder().button(
text="⬅️ Назад к серверу",
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
).as_markup()
)
else:
await callback.message.edit_text("❌ Ошибка при изменении кластера")
@router.message(ServerEditState.editing_value, IsAdminFilter())
async def apply_field_edit(message: types.Message, state: FSMContext, session: AsyncSession):
data = await state.get_data()
server_name = data["server_name"]
field = data["field"]
value = message.text.strip()
if field == "server_name":
success = await update_server_name_with_keys(session, server_name, value)
if success:
server_name = value
else:
await message.answer("❌ Ошибка при изменении имени сервера. Возможно, такое имя уже существует.")
return
else:
success = await update_server_field(session, server_name, field, value)
if success:
field_names = {
"server_name": "имя сервера",
"api_url": "API URL",
"subscription_url": "Subscription URL",
"inbound_id": "Inbound ID",
}
await message.answer(
f"{field_names.get(field, field).capitalize()} изменено",
reply_markup=InlineKeyboardBuilder().button(
text="⬅️ Назад к серверу",
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
).as_markup()
)
else:
await message.answer("❌ Ошибка при изменении поля")
await state.clear()