diff --git a/database/servers.py b/database/servers.py index 2176fd1e..f9296d35 100644 --- a/database/servers.py +++ b/database/servers.py @@ -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 diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 5af60d93..c1871286 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -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}" + ) + ] + ] + ) + diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index c3fd509c..69e34ba4 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -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"🔧 Информация о сервере {server_name}:\n\n" + f"🗂 Кластер: {cluster_name}\n" f"📡 API URL: {api_url}\n" - f"🌐 Subscription URL: {subscription_url}\n" + ) + + if subscription_url: + text += f"🌐 Subscription URL: {subscription_url}\n" + + text += ( f"🔑 Inbound ID: {inbound_id}\n" + f"⚙️ Тип панели: {panel_type}\n" f"📈 Лимит ключей: {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"✏️ Редактирование сервера: {server_name}\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"🗂 Выберите кластер для сервера {server_name}:", + 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"✏️ Введите новое значение для {field_names.get(field, field)}:", + 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"⚙️ Выберите тип панели для сервера {server_name}:", + 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()