From 4120d516b4e8b8bc50c35f4efc791fa5e122e7fe Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 10 Oct 2025 02:09:35 +0300 Subject: [PATCH] Revert "Add Remnawave squad migration admin workflow" --- app/database/crud/server_squad.py | 32 +- app/handlers/admin/remnawave.py | 782 +----------------------------- app/keyboards/admin.py | 6 - app/services/remnawave_service.py | 245 +--------- app/states.py | 7 - locales/en.json | 35 -- locales/ru.json | 35 -- 7 files changed, 13 insertions(+), 1129 deletions(-) diff --git a/app/database/crud/server_squad.py b/app/database/crud/server_squad.py index 88b52008..f1dccd7f 100644 --- a/app/database/crud/server_squad.py +++ b/app/database/crud/server_squad.py @@ -17,14 +17,7 @@ from sqlalchemy import ( from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.database.models import ( - PromoGroup, - ServerSquad, - SubscriptionServer, - Subscription, - SubscriptionStatus, - User, -) +from app.database.models import PromoGroup, ServerSquad, SubscriptionServer, Subscription, User logger = logging.getLogger(__name__) @@ -502,10 +495,10 @@ def _extract_country_code(original_name: str) -> Optional[str]: async def get_server_statistics(db: AsyncSession) -> dict: - + total_result = await db.execute(select(func.count(ServerSquad.id))) total_servers = total_result.scalar() - + available_result = await db.execute( select(func.count(ServerSquad.id)) .where(ServerSquad.is_available == True) @@ -544,25 +537,6 @@ async def get_server_statistics(db: AsyncSession) -> dict: 'total_revenue_rubles': total_revenue_kopeks / 100 } - -async def count_active_users_for_squad(db: AsyncSession, squad_uuid: str) -> int: - """Возвращает количество активных подписок, подключенных к указанному скваду.""" - - result = await db.execute( - select(func.count(Subscription.id)).where( - Subscription.status.in_( - [ - SubscriptionStatus.ACTIVE.value, - SubscriptionStatus.TRIAL.value, - ] - ), - cast(Subscription.connected_squads, String).like(f'%"{squad_uuid}"%'), - ) - ) - - return result.scalar() or 0 - - async def add_user_to_servers( db: AsyncSession, server_squad_ids: List[int] diff --git a/app/handlers/admin/remnawave.py b/app/handlers/admin/remnawave.py index d6167a27..ec5b4629 100644 --- a/app/handlers/admin/remnawave.py +++ b/app/handlers/admin/remnawave.py @@ -1,24 +1,18 @@ import logging -import math from aiogram import Dispatcher, types, F from aiogram.fsm.context import FSMContext -from app.states import SquadRenameStates, SquadCreateStates, SquadMigrationStates +from app.states import SquadRenameStates, SquadCreateStates from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import User -from app.database.crud.server_squad import ( - count_active_users_for_squad, - get_all_server_squads, - get_server_squad_by_uuid, -) from app.keyboards.admin import ( get_admin_remnawave_keyboard, get_sync_options_keyboard, get_node_management_keyboard, get_confirmation_keyboard, get_squad_management_keyboard, get_squad_edit_keyboard ) from app.localization.texts import get_texts -from app.services.remnawave_service import RemnaWaveService, RemnaWaveConfigurationError +from app.services.remnawave_service import RemnaWaveService from app.utils.decorators import admin_required, error_handler from app.utils.formatters import format_bytes, format_datetime @@ -27,769 +21,6 @@ logger = logging.getLogger(__name__) squad_inbound_selections = {} squad_create_data = {} -MIGRATION_PAGE_SIZE = 8 - - -def _format_migration_server_label(texts, server) -> str: - status = ( - texts.t("ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE", "✅ Доступен") - if getattr(server, "is_available", True) - else texts.t("ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE", "🚫 Недоступен") - ) - return texts.t( - "ADMIN_SQUAD_MIGRATION_SERVER_LABEL", - "{name} — 👥 {users} ({status})", - ).format(name=server.display_name, users=server.current_users, status=status) - - -def _build_migration_keyboard( - texts, - squads, - page: int, - total_pages: int, - stage: str, - *, - exclude_uuid: str = None, -): - prefix = "admin_migration_source" if stage == "source" else "admin_migration_target" - rows = [] - has_items = False - - button_template = texts.t( - "ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON", - "🌍 {name} — 👥 {users} ({status})", - ) - - for squad in squads: - if exclude_uuid and squad.squad_uuid == exclude_uuid: - continue - - has_items = True - status = ( - texts.t("ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT", "✅") - if getattr(squad, "is_available", True) - else texts.t("ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT", "🚫") - ) - rows.append( - [ - types.InlineKeyboardButton( - text=button_template.format( - name=squad.display_name, - users=squad.current_users, - status=status, - ), - callback_data=f"{prefix}_{squad.squad_uuid}", - ) - ] - ) - - if total_pages > 1: - nav_buttons = [] - if page > 1: - nav_buttons.append( - types.InlineKeyboardButton( - text="⬅️", - callback_data=f"{prefix}_page_{page - 1}", - ) - ) - nav_buttons.append( - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_PAGE", - "Стр. {page}/{pages}", - ).format(page=page, pages=total_pages), - callback_data="admin_migration_page_info", - ) - ) - if page < total_pages: - nav_buttons.append( - types.InlineKeyboardButton( - text="➡️", - callback_data=f"{prefix}_page_{page + 1}", - ) - ) - rows.append(nav_buttons) - - rows.append( - [ - types.InlineKeyboardButton( - text=texts.CANCEL, - callback_data="admin_migration_cancel", - ) - ] - ) - - return types.InlineKeyboardMarkup(inline_keyboard=rows), has_items - - -async def _fetch_migration_page( - db: AsyncSession, - page: int, -): - squads, total = await get_all_server_squads( - db, - page=max(1, page), - limit=MIGRATION_PAGE_SIZE, - ) - total_pages = max(1, math.ceil(total / MIGRATION_PAGE_SIZE)) - - if page < 1: - page = 1 - if page > total_pages: - page = total_pages - squads, total = await get_all_server_squads( - db, - page=page, - limit=MIGRATION_PAGE_SIZE, - ) - total_pages = max(1, math.ceil(total / MIGRATION_PAGE_SIZE)) - - return squads, page, total_pages - - -@admin_required -@error_handler -async def show_squad_migration_menu( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - texts = get_texts(db_user.language) - - await state.clear() - - squads, page, total_pages = await _fetch_migration_page(db, page=1) - keyboard, has_items = _build_migration_keyboard( - texts, - squads, - page, - total_pages, - "source", - ) - - message = ( - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов") - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECT_SOURCE", - "Выберите сквад, из которого нужно переехать:", - ) - ) - - if not has_items: - message += ( - "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_NO_OPTIONS", - "Нет доступных сквадов. Добавьте новые или отмените операцию.", - ) - ) - - await state.set_state(SquadMigrationStates.selecting_source) - - await callback.message.edit_text( - message, - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def paginate_migration_source( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - if await state.get_state() != SquadMigrationStates.selecting_source: - await callback.answer() - return - - try: - page = int(callback.data.split("_page_")[-1]) - except (ValueError, IndexError): - await callback.answer() - return - - squads, page, total_pages = await _fetch_migration_page(db, page=page) - texts = get_texts(db_user.language) - keyboard, has_items = _build_migration_keyboard( - texts, - squads, - page, - total_pages, - "source", - ) - - message = ( - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов") - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECT_SOURCE", - "Выберите сквад, из которого нужно переехать:", - ) - ) - - if not has_items: - message += ( - "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_NO_OPTIONS", - "Нет доступных сквадов. Добавьте новые или отмените операцию.", - ) - ) - - await callback.message.edit_text( - message, - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def handle_migration_source_selection( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - if await state.get_state() != SquadMigrationStates.selecting_source: - await callback.answer() - return - - if "_page_" in callback.data: - await callback.answer() - return - - source_uuid = callback.data.replace("admin_migration_source_", "", 1) - - texts = get_texts(db_user.language) - server = await get_server_squad_by_uuid(db, source_uuid) - - if not server: - await callback.answer( - texts.t( - "ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND", - "Сквад не найден или недоступен.", - ), - show_alert=True, - ) - return - - await state.update_data( - source_uuid=server.squad_uuid, - source_display=_format_migration_server_label(texts, server), - ) - - squads, page, total_pages = await _fetch_migration_page(db, page=1) - keyboard, has_items = _build_migration_keyboard( - texts, - squads, - page, - total_pages, - "target", - exclude_uuid=server.squad_uuid, - ) - - message = ( - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов") - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE", - "Источник: {source}", - ).format(source=_format_migration_server_label(texts, server)) - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECT_TARGET", - "Выберите сквад, в который нужно переехать:", - ) - ) - - if not has_items: - message += ( - "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_TARGET_EMPTY", - "Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.", - ) - ) - - await state.set_state(SquadMigrationStates.selecting_target) - - await callback.message.edit_text( - message, - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def paginate_migration_target( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - if await state.get_state() != SquadMigrationStates.selecting_target: - await callback.answer() - return - - try: - page = int(callback.data.split("_page_")[-1]) - except (ValueError, IndexError): - await callback.answer() - return - - data = await state.get_data() - source_uuid = data.get("source_uuid") - if not source_uuid: - await callback.answer() - return - - texts = get_texts(db_user.language) - - squads, page, total_pages = await _fetch_migration_page(db, page=page) - keyboard, has_items = _build_migration_keyboard( - texts, - squads, - page, - total_pages, - "target", - exclude_uuid=source_uuid, - ) - - source_display = data.get("source_display") or source_uuid - - message = ( - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов") - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE", - "Источник: {source}", - ).format(source=source_display) - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECT_TARGET", - "Выберите сквад, в который нужно переехать:", - ) - ) - - if not has_items: - message += ( - "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_TARGET_EMPTY", - "Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.", - ) - ) - - await callback.message.edit_text( - message, - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def handle_migration_target_selection( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - current_state = await state.get_state() - if current_state != SquadMigrationStates.selecting_target: - await callback.answer() - return - - if "_page_" in callback.data: - await callback.answer() - return - - data = await state.get_data() - source_uuid = data.get("source_uuid") - - if not source_uuid: - await callback.answer() - return - - target_uuid = callback.data.replace("admin_migration_target_", "", 1) - - texts = get_texts(db_user.language) - - if target_uuid == source_uuid: - await callback.answer( - texts.t( - "ADMIN_SQUAD_MIGRATION_SAME_SQUAD", - "Нельзя выбрать тот же сквад.", - ), - show_alert=True, - ) - return - - target_server = await get_server_squad_by_uuid(db, target_uuid) - if not target_server: - await callback.answer( - texts.t( - "ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND", - "Сквад не найден или недоступен.", - ), - show_alert=True, - ) - return - - source_display = data.get("source_display") or source_uuid - - users_to_move = await count_active_users_for_squad(db, source_uuid) - - await state.update_data( - target_uuid=target_server.squad_uuid, - target_display=_format_migration_server_label(texts, target_server), - migration_count=users_to_move, - ) - - await state.set_state(SquadMigrationStates.confirming) - - message_lines = [ - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов"), - "", - texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS", - "Проверьте параметры переезда:", - ), - texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE", - "• Из: {source}", - ).format(source=source_display), - texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET", - "• В: {target}", - ).format(target=_format_migration_server_label(texts, target_server)), - texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT", - "• Пользователей к переносу: {count}", - ).format(count=users_to_move), - "", - texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT", - "Подтвердите выполнение операции.", - ), - ] - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON", - "✅ Подтвердить", - ), - callback_data="admin_migration_confirm", - ) - ], - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_CHANGE_TARGET", - "🔄 Изменить сервер назначения", - ), - callback_data="admin_migration_change_target", - ) - ], - [ - types.InlineKeyboardButton( - text=texts.CANCEL, - callback_data="admin_migration_cancel", - ) - ], - ] - ) - - await callback.message.edit_text( - "\n".join(message_lines), - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def change_migration_target( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - data = await state.get_data() - source_uuid = data.get("source_uuid") - - if not source_uuid: - await callback.answer() - return - - await state.set_state(SquadMigrationStates.selecting_target) - - texts = get_texts(db_user.language) - squads, page, total_pages = await _fetch_migration_page(db, page=1) - keyboard, has_items = _build_migration_keyboard( - texts, - squads, - page, - total_pages, - "target", - exclude_uuid=source_uuid, - ) - - source_display = data.get("source_display") or source_uuid - - message = ( - texts.t("ADMIN_SQUAD_MIGRATION_TITLE", "🚚 Переезд сквадов") - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE", - "Источник: {source}", - ).format(source=source_display) - + "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_SELECT_TARGET", - "Выберите сквад, в который нужно переехать:", - ) - ) - - if not has_items: - message += ( - "\n\n" - + texts.t( - "ADMIN_SQUAD_MIGRATION_TARGET_EMPTY", - "Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.", - ) - ) - - await callback.message.edit_text( - message, - reply_markup=keyboard, - disable_web_page_preview=True, - ) - await callback.answer() - - -@admin_required -@error_handler -async def confirm_squad_migration( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - current_state = await state.get_state() - if current_state != SquadMigrationStates.confirming: - await callback.answer() - return - - data = await state.get_data() - source_uuid = data.get("source_uuid") - target_uuid = data.get("target_uuid") - - if not source_uuid or not target_uuid: - await callback.answer() - return - - texts = get_texts(db_user.language) - remnawave_service = RemnaWaveService() - - await callback.answer(texts.t("ADMIN_SQUAD_MIGRATION_IN_PROGRESS", "Запускаю переезд...")) - - try: - result = await remnawave_service.migrate_squad_users( - db, - source_uuid=source_uuid, - target_uuid=target_uuid, - ) - except RemnaWaveConfigurationError as error: - message = texts.t( - "ADMIN_SQUAD_MIGRATION_API_ERROR", - "❌ RemnaWave API не настроен: {error}", - ).format(error=str(error)) - reply_markup = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON", - "⬅️ В Remnawave", - ), - callback_data="admin_remnawave", - ) - ] - ] - ) - await callback.message.edit_text(message, reply_markup=reply_markup) - await state.clear() - return - - source_display = data.get("source_display") or source_uuid - target_display = data.get("target_display") or target_uuid - - if not result.get("success"): - error_message = result.get("message") or "" - error_code = result.get("error") or "unexpected" - message = texts.t( - "ADMIN_SQUAD_MIGRATION_ERROR", - "❌ Не удалось выполнить переезд (код: {code}). {details}", - ).format(code=error_code, details=error_message) - reply_markup = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON", - "⬅️ В Remnawave", - ), - callback_data="admin_remnawave", - ) - ], - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_NEW_BUTTON", - "🔁 Новый переезд", - ), - callback_data="admin_rw_migration", - ) - ], - ] - ) - await callback.message.edit_text(message, reply_markup=reply_markup) - await state.clear() - return - - message_lines = [ - texts.t("ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE", "✅ Переезд завершен"), - "", - texts.t("ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE", "• Из: {source}").format( - source=source_display - ), - texts.t("ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET", "• В: {target}").format( - target=target_display - ), - "", - texts.t( - "ADMIN_SQUAD_MIGRATION_RESULT_TOTAL", - "Найдено подписок: {count}", - ).format(count=result.get("total", 0)), - texts.t( - "ADMIN_SQUAD_MIGRATION_RESULT_UPDATED", - "Перенесено: {count}", - ).format(count=result.get("updated", 0)), - ] - - panel_updated = result.get("panel_updated", 0) - panel_failed = result.get("panel_failed", 0) - - if panel_updated: - message_lines.append( - texts.t( - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED", - "Обновлено в панели: {count}", - ).format(count=panel_updated) - ) - if panel_failed: - message_lines.append( - texts.t( - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED", - "Не удалось обновить в панели: {count}", - ).format(count=panel_failed) - ) - - reply_markup = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_NEW_BUTTON", - "🔁 Новый переезд", - ), - callback_data="admin_rw_migration", - ) - ], - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON", - "⬅️ В Remnawave", - ), - callback_data="admin_remnawave", - ) - ], - ] - ) - - await callback.message.edit_text( - "\n".join(message_lines), - reply_markup=reply_markup, - disable_web_page_preview=True, - ) - await state.clear() - - -@admin_required -@error_handler -async def cancel_squad_migration( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - texts = get_texts(db_user.language) - await state.clear() - - message = texts.t( - "ADMIN_SQUAD_MIGRATION_CANCELLED", - "❌ Переезд отменен.", - ) - - reply_markup = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=texts.t( - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON", - "⬅️ В Remnawave", - ), - callback_data="admin_remnawave", - ) - ] - ] - ) - - await callback.message.edit_text(message, reply_markup=reply_markup) - await callback.answer() - - -@admin_required -@error_handler -async def handle_migration_page_info( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("ADMIN_SQUAD_MIGRATION_PAGE_HINT", "Это текущая страница."), - show_alert=False, - ) - @admin_required @error_handler async def show_remnawave_menu( @@ -2689,15 +1920,6 @@ def register_handlers(dp: Dispatcher): dp.callback_query.register(restart_all_nodes, F.data == "admin_restart_all_nodes") dp.callback_query.register(show_sync_options, F.data == "admin_rw_sync") dp.callback_query.register(sync_all_users, F.data == "sync_all_users") - dp.callback_query.register(show_squad_migration_menu, F.data == "admin_rw_migration") - dp.callback_query.register(paginate_migration_source, F.data.startswith("admin_migration_source_page_")) - dp.callback_query.register(handle_migration_source_selection, F.data.startswith("admin_migration_source_")) - dp.callback_query.register(paginate_migration_target, F.data.startswith("admin_migration_target_page_")) - dp.callback_query.register(handle_migration_target_selection, F.data.startswith("admin_migration_target_")) - dp.callback_query.register(change_migration_target, F.data == "admin_migration_change_target") - dp.callback_query.register(confirm_squad_migration, F.data == "admin_migration_confirm") - dp.callback_query.register(cancel_squad_migration, F.data == "admin_migration_cancel") - dp.callback_query.register(handle_migration_page_info, F.data == "admin_migration_page_info") dp.callback_query.register(show_squads_management, F.data == "admin_rw_squads") dp.callback_query.register(show_squad_details, F.data.startswith("admin_squad_manage_")) dp.callback_query.register(manage_squad_action, F.data.startswith("squad_add_users_")) diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index a1d9e3a8..076fe30e 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -674,12 +674,6 @@ def get_admin_remnawave_keyboard(language: str = "ru") -> InlineKeyboardMarkup: callback_data="admin_rw_squads" ) ], - [ - InlineKeyboardButton( - text=_t(texts, "ADMIN_REMNAWAVE_MIGRATION", "🚚 Переезд"), - callback_data="admin_rw_migration" - ) - ], [ InlineKeyboardButton( text=_t(texts, "ADMIN_REMNAWAVE_TRAFFIC", "📈 Трафик"), diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index 84e90c6f..6ec15eea 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -1,7 +1,7 @@ import logging import os import re -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import asynccontextmanager from datetime import datetime, timedelta from typing import Any, Dict, List, Optional @@ -12,8 +12,7 @@ from app.external.remnawave_api import ( RemnaWaveAPI, RemnaWaveUser, RemnaWaveInternalSquad, RemnaWaveNode, UserStatus, TrafficLimitStrategy, RemnaWaveAPIError ) -from sqlalchemy import and_, cast, delete, func, select, update, String -from sqlalchemy.orm import selectinload +from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession from app.database.crud.user import get_users_list, get_user_by_telegram_id, update_user from app.database.crud.subscription import ( @@ -21,16 +20,9 @@ from app.database.crud.subscription import ( update_subscription_usage, decrement_subscription_server_counts, ) -from app.database.crud.server_squad import get_server_squad_by_uuid from app.database.models import ( - User, - Subscription, - SubscriptionServer, - Transaction, - ReferralEarning, - PromoCodeUse, - SubscriptionStatus, - ServerSquad, + User, SubscriptionServer, Transaction, ReferralEarning, + PromoCodeUse, SubscriptionStatus ) logger = logging.getLogger(__name__) @@ -489,237 +481,16 @@ class RemnaWaveService: try: async with self.get_api_client() as api: result = await api.delete_internal_squad(uuid) - + if result: logger.info(f"✅ Удален сквад {uuid}") - + return result - + except Exception as e: logger.error(f"Ошибка удаления сквада {uuid}: {e}") return False - - async def migrate_squad_users( - self, - db: AsyncSession, - source_uuid: str, - target_uuid: str, - ) -> Dict[str, Any]: - """Переносит активных подписок с одного сквада на другой.""" - - if source_uuid == target_uuid: - return { - "success": False, - "error": "same_squad", - "message": "Источник и назначение совпадают", - } - - source_uuid = source_uuid.strip() - target_uuid = target_uuid.strip() - - source_server = await get_server_squad_by_uuid(db, source_uuid) - target_server = await get_server_squad_by_uuid(db, target_uuid) - - if not source_server or not target_server: - return { - "success": False, - "error": "not_found", - "message": "Сквады не найдены", - } - - subscription_query = ( - select(Subscription) - .options(selectinload(Subscription.user)) - .where( - Subscription.status.in_( - [ - SubscriptionStatus.ACTIVE.value, - SubscriptionStatus.TRIAL.value, - ] - ), - cast(Subscription.connected_squads, String).like( - f'%"{source_uuid}"%' - ), - ) - ) - - result = await db.execute(subscription_query) - subscriptions = result.scalars().unique().all() - - total_candidates = len(subscriptions) - if not subscriptions: - logger.info( - "🚚 Переезд сквада %s → %s: подходящих подписок не найдено", - source_uuid, - target_uuid, - ) - return { - "success": True, - "total": 0, - "updated": 0, - "panel_updated": 0, - "panel_failed": 0, - } - - exit_stack = AsyncExitStack() - panel_updated = 0 - panel_failed = 0 - updated_subscriptions = 0 - source_decrement = 0 - target_increment = 0 - - try: - needs_panel_update = any( - subscription.user and subscription.user.remnawave_uuid - for subscription in subscriptions - ) - - api = None - if needs_panel_update: - api = await exit_stack.enter_async_context(self.get_api_client()) - - for subscription in subscriptions: - current_squads = list(subscription.connected_squads or []) - if source_uuid not in current_squads: - continue - - had_target_before = target_uuid in current_squads - new_squads = [ - squad_uuid for squad_uuid in current_squads if squad_uuid != source_uuid - ] - if not had_target_before: - new_squads.append(target_uuid) - - if subscription.user and subscription.user.remnawave_uuid: - if api is None: - panel_failed += 1 - logger.error( - "❌ RemnaWave API недоступен для обновления пользователя %s", - subscription.user.telegram_id, - ) - continue - - try: - await api.update_user( - uuid=subscription.user.remnawave_uuid, - active_internal_squads=new_squads, - ) - panel_updated += 1 - except Exception as error: - panel_failed += 1 - logger.error( - "❌ Ошибка обновления сквадов пользователя %s: %s", - subscription.user.telegram_id, - error, - ) - continue - - subscription.connected_squads = new_squads - subscription.updated_at = datetime.utcnow() - - source_decrement += 1 - if not had_target_before: - target_increment += 1 - - updated_subscriptions += 1 - - link_result = await db.execute( - select(SubscriptionServer) - .where( - and_( - SubscriptionServer.subscription_id == subscription.id, - SubscriptionServer.server_squad_id == source_server.id, - ) - ) - .limit(1) - ) - link = link_result.scalars().first() - - if link: - if had_target_before: - await db.execute( - delete(SubscriptionServer).where( - and_( - SubscriptionServer.subscription_id - == subscription.id, - SubscriptionServer.server_squad_id - == source_server.id, - ) - ) - ) - else: - link.server_squad_id = target_server.id - elif not had_target_before: - db.add( - SubscriptionServer( - subscription_id=subscription.id, - server_squad_id=target_server.id, - paid_price_kopeks=0, - ) - ) - - if updated_subscriptions: - if source_decrement: - await db.execute( - update(ServerSquad) - .where(ServerSquad.id == source_server.id) - .values( - current_users=func.greatest( - ServerSquad.current_users - source_decrement, - 0, - ) - ) - ) - if target_increment: - await db.execute( - update(ServerSquad) - .where(ServerSquad.id == target_server.id) - .values( - current_users=ServerSquad.current_users + target_increment - ) - ) - - await db.commit() - else: - await db.rollback() - - logger.info( - "🚚 Завершен переезд сквада %s → %s: обновлено %s подписок (%s не обновлены в панели)", - source_uuid, - target_uuid, - updated_subscriptions, - panel_failed, - ) - - return { - "success": True, - "total": total_candidates, - "updated": updated_subscriptions, - "panel_updated": panel_updated, - "panel_failed": panel_failed, - "source_removed": source_decrement, - "target_added": target_increment, - } - - except RemnaWaveConfigurationError: - await db.rollback() - raise - except Exception as error: - await db.rollback() - logger.error( - "❌ Ошибка переезда сквада %s → %s: %s", - source_uuid, - target_uuid, - error, - ) - return { - "success": False, - "error": "unexpected", - "message": str(error), - } - finally: - await exit_stack.aclose() - + async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]: try: stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0} diff --git a/app/states.py b/app/states.py index 0d10a629..b7eb5765 100644 --- a/app/states.py +++ b/app/states.py @@ -166,13 +166,6 @@ class SquadCreateStates(StatesGroup): class SquadRenameStates(StatesGroup): waiting_for_new_name = State() - -class SquadMigrationStates(StatesGroup): - selecting_source = State() - selecting_target = State() - confirming = State() - - class AdminSubmenuStates(StatesGroup): in_users_submenu = State() in_promo_submenu = State() diff --git a/locales/en.json b/locales/en.json index 217035a6..3444c456 100644 --- a/locales/en.json +++ b/locales/en.json @@ -870,42 +870,7 @@ "ADMIN_REMNAWAVE_MANAGE_NODES": "🖥️ Manage nodes", "ADMIN_REMNAWAVE_SYNC": "🔄 Synchronization", "ADMIN_REMNAWAVE_MANAGE_SQUADS": "🌐 Manage squads", - "ADMIN_REMNAWAVE_MIGRATION": "🚚 Migration", "ADMIN_REMNAWAVE_TRAFFIC": "📈 Traffic", - "ADMIN_SQUAD_MIGRATION_TITLE": "🚚 Squad migration", - "ADMIN_SQUAD_MIGRATION_SELECT_SOURCE": "Choose the squad you want to migrate from:", - "ADMIN_SQUAD_MIGRATION_NO_OPTIONS": "No squads available. Add new ones or cancel the operation.", - "ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE": "✅ Available", - "ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE": "🚫 Unavailable", - "ADMIN_SQUAD_MIGRATION_SERVER_LABEL": "{name} — 👥 {users} ({status})", - "ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON": "🌍 {name} — 👥 {users} ({status})", - "ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT": "✅", - "ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT": "🚫", - "ADMIN_SQUAD_MIGRATION_PAGE": "Page {page}/{pages}", - "ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE": "Source: {source}", - "ADMIN_SQUAD_MIGRATION_SELECT_TARGET": "Choose the destination squad:", - "ADMIN_SQUAD_MIGRATION_TARGET_EMPTY": "No other squads available. Cancel or create new squads.", - "ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND": "Squad not found or unavailable.", - "ADMIN_SQUAD_MIGRATION_SAME_SQUAD": "You can't choose the same squad.", - "ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS": "Review the migration parameters:", - "ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE": "• From: {source}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET": "• To: {target}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT": "• Users to migrate: {count}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT": "Confirm the operation.", - "ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON": "✅ Confirm", - "ADMIN_SQUAD_MIGRATION_CHANGE_TARGET": "🔄 Change destination", - "ADMIN_SQUAD_MIGRATION_IN_PROGRESS": "Starting migration...", - "ADMIN_SQUAD_MIGRATION_API_ERROR": "❌ Remnawave API is not configured: {error}", - "ADMIN_SQUAD_MIGRATION_ERROR": "❌ Failed to migrate (code: {code}). {details}", - "ADMIN_SQUAD_MIGRATION_NEW_BUTTON": "🔁 New migration", - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON": "⬅️ Back to Remnawave", - "ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE": "✅ Migration completed", - "ADMIN_SQUAD_MIGRATION_RESULT_TOTAL": "Subscriptions matched: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_UPDATED": "Migrated: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED": "Updated in panel: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED": "Panel update failed: {count}", - "ADMIN_SQUAD_MIGRATION_CANCELLED": "❌ Migration cancelled.", - "ADMIN_SQUAD_MIGRATION_PAGE_HINT": "This is the current page.", "ADMIN_STATS_USERS": "👥 Users", "ADMIN_STATS_SUBSCRIPTIONS": "📱 Subscriptions", "ADMIN_STATS_REVENUE": "💰 Revenue", diff --git a/locales/ru.json b/locales/ru.json index 2348dd4b..47242a4d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -872,42 +872,7 @@ "ADMIN_REMNAWAVE_MANAGE_NODES": "🖥️ Управление нодами", "ADMIN_REMNAWAVE_SYNC": "🔄 Синхронизация", "ADMIN_REMNAWAVE_MANAGE_SQUADS": "🌐 Управление сквадами", - "ADMIN_REMNAWAVE_MIGRATION": "🚚 Переезд", "ADMIN_REMNAWAVE_TRAFFIC": "📈 Трафик", - "ADMIN_SQUAD_MIGRATION_TITLE": "🚚 Переезд сквадов", - "ADMIN_SQUAD_MIGRATION_SELECT_SOURCE": "Выберите сквад, из которого нужно переехать:", - "ADMIN_SQUAD_MIGRATION_NO_OPTIONS": "Нет доступных сквадов. Добавьте новые или отмените операцию.", - "ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE": "✅ Доступен", - "ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE": "🚫 Недоступен", - "ADMIN_SQUAD_MIGRATION_SERVER_LABEL": "{name} — 👥 {users} ({status})", - "ADMIN_SQUAD_MIGRATION_SQUAD_BUTTON": "🌍 {name} — 👥 {users} ({status})", - "ADMIN_SQUAD_MIGRATION_STATUS_AVAILABLE_SHORT": "✅", - "ADMIN_SQUAD_MIGRATION_STATUS_UNAVAILABLE_SHORT": "🚫", - "ADMIN_SQUAD_MIGRATION_PAGE": "Стр. {page}/{pages}", - "ADMIN_SQUAD_MIGRATION_SELECTED_SOURCE": "Источник: {source}", - "ADMIN_SQUAD_MIGRATION_SELECT_TARGET": "Выберите сквад, в который нужно переехать:", - "ADMIN_SQUAD_MIGRATION_TARGET_EMPTY": "Нет других сквадов для переезда. Отмените операцию или создайте новые сквады.", - "ADMIN_SQUAD_MIGRATION_SQUAD_NOT_FOUND": "Сквад не найден или недоступен.", - "ADMIN_SQUAD_MIGRATION_SAME_SQUAD": "Нельзя выбрать тот же сквад.", - "ADMIN_SQUAD_MIGRATION_CONFIRM_DETAILS": "Проверьте параметры переезда:", - "ADMIN_SQUAD_MIGRATION_CONFIRM_SOURCE": "• Из: {source}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_TARGET": "• В: {target}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_COUNT": "• Пользователей к переносу: {count}", - "ADMIN_SQUAD_MIGRATION_CONFIRM_PROMPT": "Подтвердите выполнение операции.", - "ADMIN_SQUAD_MIGRATION_CONFIRM_BUTTON": "✅ Подтвердить", - "ADMIN_SQUAD_MIGRATION_CHANGE_TARGET": "🔄 Изменить сервер назначения", - "ADMIN_SQUAD_MIGRATION_IN_PROGRESS": "Запускаю переезд...", - "ADMIN_SQUAD_MIGRATION_API_ERROR": "❌ Remnawave API не настроен: {error}", - "ADMIN_SQUAD_MIGRATION_ERROR": "❌ Не удалось выполнить переезд (код: {code}). {details}", - "ADMIN_SQUAD_MIGRATION_NEW_BUTTON": "🔁 Новый переезд", - "ADMIN_SQUAD_MIGRATION_BACK_BUTTON": "⬅️ В Remnawave", - "ADMIN_SQUAD_MIGRATION_SUCCESS_TITLE": "✅ Переезд завершен", - "ADMIN_SQUAD_MIGRATION_RESULT_TOTAL": "Найдено подписок: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_UPDATED": "Перенесено: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_UPDATED": "Обновлено в панели: {count}", - "ADMIN_SQUAD_MIGRATION_RESULT_PANEL_FAILED": "Не удалось обновить в панели: {count}", - "ADMIN_SQUAD_MIGRATION_CANCELLED": "❌ Переезд отменен.", - "ADMIN_SQUAD_MIGRATION_PAGE_HINT": "Это текущая страница.", "ADMIN_STATS_USERS": "👥 Пользователи", "ADMIN_STATS_SUBSCRIPTIONS": "📱 Подписки", "ADMIN_STATS_REVENUE": "💰 Доходы",