add full server management with cluster transfer support
This commit is contained in:
+125
-2
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user