Merge pull request #498 from Fr1ngg/bedolaga/fix-server-deletion-issue-in-subscriptions-vi0z1h
Handle RemnaWave server removal and add connected users view
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
from sqlalchemy import select, and_, func, update, delete, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import PromoGroup, ServerSquad, SubscriptionServer, Subscription
|
||||
from app.database.models import PromoGroup, ServerSquad, SubscriptionServer, Subscription, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -232,10 +233,10 @@ async def sync_with_remnawave(
|
||||
db: AsyncSession,
|
||||
remnawave_squads: List[dict]
|
||||
) -> Tuple[int, int, int]:
|
||||
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
disabled = 0
|
||||
removed = 0
|
||||
|
||||
existing_servers = {}
|
||||
result = await db.execute(select(ServerSquad))
|
||||
@@ -265,15 +266,101 @@ async def sync_with_remnawave(
|
||||
)
|
||||
created += 1
|
||||
|
||||
for uuid, server in existing_servers.items():
|
||||
if uuid not in remnawave_uuids and server.is_available:
|
||||
server.is_available = False
|
||||
disabled += 1
|
||||
|
||||
removed_servers = [
|
||||
server for uuid, server in existing_servers.items()
|
||||
if uuid not in remnawave_uuids
|
||||
]
|
||||
|
||||
if removed_servers:
|
||||
removed_ids = [server.id for server in removed_servers]
|
||||
removed_uuids = {server.squad_uuid for server in removed_servers}
|
||||
|
||||
subscription_ids_result = await db.execute(
|
||||
select(SubscriptionServer.subscription_id)
|
||||
.where(SubscriptionServer.server_squad_id.in_(removed_ids))
|
||||
)
|
||||
subscription_ids = {row[0] for row in subscription_ids_result.fetchall()}
|
||||
|
||||
for server in removed_servers:
|
||||
logger.info(
|
||||
"🗑️ Удаляется сервер %s (UUID: %s)",
|
||||
server.display_name,
|
||||
server.squad_uuid,
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(SubscriptionServer.server_squad_id.in_(removed_ids))
|
||||
)
|
||||
|
||||
subscriptions_to_update: dict[int, Subscription] = {}
|
||||
|
||||
if subscription_ids:
|
||||
subscriptions_result = await db.execute(
|
||||
select(Subscription).where(Subscription.id.in_(subscription_ids))
|
||||
)
|
||||
for subscription in subscriptions_result.scalars().unique().all():
|
||||
subscriptions_to_update[subscription.id] = subscription
|
||||
|
||||
for squad_uuid in removed_uuids:
|
||||
if not squad_uuid:
|
||||
continue
|
||||
|
||||
extra_result = await db.execute(
|
||||
select(Subscription).where(
|
||||
text("connected_squads::text LIKE :uuid_pattern")
|
||||
),
|
||||
{"uuid_pattern": f'%"{squad_uuid}"%'}
|
||||
)
|
||||
|
||||
for subscription in extra_result.scalars().unique().all():
|
||||
subscriptions_to_update[subscription.id] = subscription
|
||||
|
||||
cleaned_subscriptions = 0
|
||||
|
||||
for subscription in subscriptions_to_update.values():
|
||||
current_squads = list(subscription.connected_squads or [])
|
||||
if not current_squads:
|
||||
continue
|
||||
|
||||
filtered_squads = [
|
||||
squad_uuid for squad_uuid in current_squads if squad_uuid not in removed_uuids
|
||||
]
|
||||
|
||||
if len(filtered_squads) != len(current_squads):
|
||||
subscription.connected_squads = filtered_squads
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
cleaned_subscriptions += 1
|
||||
|
||||
await db.execute(delete(ServerSquad).where(ServerSquad.id.in_(removed_ids)))
|
||||
removed = len(removed_servers)
|
||||
|
||||
if cleaned_subscriptions:
|
||||
logger.info(
|
||||
"🧹 Обновлены подписки после удаления серверов: %s",
|
||||
cleaned_subscriptions,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"🔄 Синхронизация завершена: +{created} ~{updated} -{disabled}")
|
||||
return created, updated, disabled
|
||||
|
||||
logger.info(f"🔄 Синхронизация завершена: +{created} ~{updated} -{removed}")
|
||||
return created, updated, removed
|
||||
|
||||
|
||||
async def get_server_connected_users(
|
||||
db: AsyncSession,
|
||||
server_id: int
|
||||
) -> List[User]:
|
||||
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.join(Subscription, Subscription.user_id == User.id)
|
||||
.join(SubscriptionServer, SubscriptionServer.subscription_id == Subscription.id)
|
||||
.where(SubscriptionServer.server_squad_id == server_id)
|
||||
.options(selectinload(User.subscription))
|
||||
.order_by(User.id)
|
||||
)
|
||||
|
||||
return result.scalars().unique().all()
|
||||
|
||||
|
||||
def _generate_display_name(original_name: str) -> str:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -15,6 +16,7 @@ from app.database.crud.server_squad import (
|
||||
create_server_squad,
|
||||
get_available_server_squads,
|
||||
update_server_squad_promo_groups,
|
||||
get_server_connected_users,
|
||||
)
|
||||
from app.database.crud.promo_group import get_promo_groups_with_counts
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
@@ -73,6 +75,11 @@ def _build_server_edit_view(server):
|
||||
text="👥 Лимит", callback_data=f"admin_server_edit_limit_{server.id}"
|
||||
),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="👥 Юзеры", callback_data=f"admin_server_users_{server.id}"
|
||||
),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🎯 Промогруппы", callback_data=f"admin_server_edit_promo_{server.id}"
|
||||
@@ -276,7 +283,7 @@ async def sync_servers_with_remnawave(
|
||||
)
|
||||
return
|
||||
|
||||
created, updated, disabled = await sync_with_remnawave(db, squads)
|
||||
created, updated, removed = await sync_with_remnawave(db, squads)
|
||||
|
||||
await cache.delete_pattern("available_countries*")
|
||||
|
||||
@@ -286,7 +293,7 @@ async def sync_servers_with_remnawave(
|
||||
📊 <b>Результаты:</b>
|
||||
• Создано новых серверов: {created}
|
||||
• Обновлено существующих: {updated}
|
||||
• Отключено неактивных: {disabled}
|
||||
• Удалено отсутствующих: {removed}
|
||||
• Всего обработано: {len(squads)}
|
||||
|
||||
ℹ️ Новые серверы созданы как недоступные.
|
||||
@@ -314,7 +321,7 @@ async def sync_servers_with_remnawave(
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_servers")]
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -343,6 +350,89 @@ async def show_server_edit_menu(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_server_users(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
server_id = int(callback.data.split('_')[-1])
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
|
||||
if not server:
|
||||
await callback.answer("❌ Сервер не найден!", show_alert=True)
|
||||
return
|
||||
|
||||
users = await get_server_connected_users(db, server_id)
|
||||
|
||||
safe_name = html.escape(server.display_name or "—")
|
||||
safe_uuid = html.escape(server.squad_uuid or "—")
|
||||
|
||||
header = [
|
||||
"🌐 <b>Пользователи сервера</b>",
|
||||
"",
|
||||
f"• Сервер: {safe_name}",
|
||||
f"• UUID: <code>{safe_uuid}</code>",
|
||||
f"• Подключений: {len(users)}",
|
||||
"",
|
||||
]
|
||||
|
||||
text = "\n".join(header)
|
||||
|
||||
if users:
|
||||
lines = []
|
||||
for index, user in enumerate(users, 1):
|
||||
subscription_status = (
|
||||
user.subscription.status_display
|
||||
if user.subscription
|
||||
else "❌ Нет подписки"
|
||||
)
|
||||
safe_user_name = html.escape(user.full_name)
|
||||
safe_status = html.escape(subscription_status)
|
||||
lines.append(
|
||||
f"{index}. {safe_user_name} — {safe_status}"
|
||||
)
|
||||
|
||||
text += "\n" + "\n".join(lines)
|
||||
else:
|
||||
text += "Пользователи не найдены."
|
||||
|
||||
keyboard: list[list[types.InlineKeyboardButton]] = []
|
||||
|
||||
for user in users:
|
||||
display_name = user.full_name
|
||||
if len(display_name) > 30:
|
||||
display_name = display_name[:27] + "..."
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(
|
||||
text=f"👤 {display_name}",
|
||||
callback_data=f"admin_user_manage_{user.id}",
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️ К серверу", callback_data=f"admin_server_edit_{server_id}"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️ К списку", callback_data="admin_servers_list"
|
||||
)
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_server_availability(
|
||||
@@ -1105,6 +1195,7 @@ def register_handlers(dp: Dispatcher):
|
||||
& ~F.data.contains("promo"),
|
||||
)
|
||||
dp.callback_query.register(toggle_server_availability, F.data.startswith("admin_server_toggle_"))
|
||||
dp.callback_query.register(show_server_users, F.data.startswith("admin_server_users_"))
|
||||
|
||||
dp.callback_query.register(start_server_edit_name, F.data.startswith("admin_server_edit_name_"))
|
||||
dp.callback_query.register(start_server_edit_price, F.data.startswith("admin_server_edit_price_"))
|
||||
|
||||
Reference in New Issue
Block a user