Обратная синхронизация
This commit is contained in:
@@ -2295,6 +2295,12 @@ async def show_sync_options(
|
|||||||
callback_data="sync_all_users",
|
callback_data="sync_all_users",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="🔄 Обратная синхронизация",
|
||||||
|
callback_data="sync_to_panel",
|
||||||
|
)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
types.InlineKeyboardButton(
|
types.InlineKeyboardButton(
|
||||||
text="⚙️ Настройки автосинхронизации",
|
text="⚙️ Настройки автосинхронизации",
|
||||||
@@ -2311,6 +2317,94 @@ async def show_sync_options(
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
@error_handler
|
||||||
|
async def sync_users_to_panel(
|
||||||
|
callback: types.CallbackQuery,
|
||||||
|
db_user: User,
|
||||||
|
db: AsyncSession
|
||||||
|
):
|
||||||
|
|
||||||
|
progress_text = """
|
||||||
|
🔄 <b>Выполняется обратная синхронизация...</b>
|
||||||
|
|
||||||
|
📋 Этапы:
|
||||||
|
• Загрузка ВСЕХ пользователей из бота
|
||||||
|
• Проверка существования в панели Remnawave
|
||||||
|
• Создание новых пользователей в панели
|
||||||
|
• Обновление данных существующих пользователей
|
||||||
|
• Настройка подписок и трафика
|
||||||
|
|
||||||
|
⏳ Пожалуйста, подождите...
|
||||||
|
"""
|
||||||
|
|
||||||
|
await callback.message.edit_text(progress_text, reply_markup=None)
|
||||||
|
|
||||||
|
remnawave_service = RemnaWaveService()
|
||||||
|
stats = await remnawave_service.sync_users_to_panel(db)
|
||||||
|
|
||||||
|
total_operations = stats['created'] + stats['updated']
|
||||||
|
|
||||||
|
if stats['errors'] == 0:
|
||||||
|
status_emoji = "✅"
|
||||||
|
status_text = "успешно завершена"
|
||||||
|
elif stats['errors'] < total_operations:
|
||||||
|
status_emoji = "⚠️"
|
||||||
|
status_text = "завершена с предупреждениями"
|
||||||
|
else:
|
||||||
|
status_emoji = "❌"
|
||||||
|
status_text = "завершена с ошибками"
|
||||||
|
|
||||||
|
text = f"""
|
||||||
|
{status_emoji} <b>Обратная синхронизация {status_text}</b>
|
||||||
|
|
||||||
|
📊 <b>Результат:</b>
|
||||||
|
• 🆕 Создано в панели: {stats['created']}
|
||||||
|
• 🔄 Обновлено в панели: {stats['updated']}
|
||||||
|
• ❌ Ошибок: {stats['errors']}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if stats['errors'] > 0:
|
||||||
|
text += f"""
|
||||||
|
|
||||||
|
⚠️ <b>Внимание:</b>
|
||||||
|
Некоторые операции завершились с ошибками.
|
||||||
|
Проверьте логи для получения подробной информации.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text += f"""
|
||||||
|
|
||||||
|
💡 <b>Рекомендации:</b>
|
||||||
|
• Обратная синхронизация выполнена
|
||||||
|
• Все пользователи из бота теперь в панели Remnawave
|
||||||
|
• Подписки пользователей синхронизированы (если есть)
|
||||||
|
"""
|
||||||
|
|
||||||
|
keyboard = []
|
||||||
|
|
||||||
|
if stats['errors'] > 0:
|
||||||
|
keyboard.append([
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="🔄 Повторить синхронизацию",
|
||||||
|
callback_data="sync_to_panel"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
keyboard.extend([
|
||||||
|
[
|
||||||
|
types.InlineKeyboardButton(text="📊 Статистика системы", callback_data="admin_rw_system"),
|
||||||
|
types.InlineKeyboardButton(text="🌐 Ноды", callback_data="admin_rw_nodes")
|
||||||
|
],
|
||||||
|
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
|
||||||
|
])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@admin_required
|
@admin_required
|
||||||
@error_handler
|
@error_handler
|
||||||
async def show_auto_sync_settings(
|
async def show_auto_sync_settings(
|
||||||
@@ -3120,6 +3214,7 @@ def register_handlers(dp: Dispatcher):
|
|||||||
dp.callback_query.register(manage_node, F.data.startswith("node_restart_"))
|
dp.callback_query.register(manage_node, F.data.startswith("node_restart_"))
|
||||||
dp.callback_query.register(restart_all_nodes, F.data == "admin_restart_all_nodes")
|
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(show_sync_options, F.data == "admin_rw_sync")
|
||||||
|
dp.callback_query.register(sync_users_to_panel, F.data == "sync_to_panel")
|
||||||
dp.callback_query.register(show_auto_sync_settings, F.data == "admin_rw_auto_sync")
|
dp.callback_query.register(show_auto_sync_settings, F.data == "admin_rw_auto_sync")
|
||||||
dp.callback_query.register(toggle_auto_sync_setting, F.data == "remnawave_auto_sync_toggle")
|
dp.callback_query.register(toggle_auto_sync_setting, F.data == "remnawave_auto_sync_toggle")
|
||||||
dp.callback_query.register(prompt_auto_sync_schedule, F.data == "remnawave_auto_sync_times")
|
dp.callback_query.register(prompt_auto_sync_schedule, F.data == "remnawave_auto_sync_times")
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ class RemnaWaveService:
|
|||||||
password=auth_params.get("password")
|
password=auth_params.get("password")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _gb_to_bytes(self, gb: int) -> int:
|
||||||
|
"""Конвертирует гигабайты в байты"""
|
||||||
|
if gb == 0:
|
||||||
|
return 0
|
||||||
|
return gb * 1024 * 1024 * 1024
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
return self._config_error is None
|
return self._config_error is None
|
||||||
@@ -2500,5 +2506,132 @@ class RemnaWaveService:
|
|||||||
"api_url": settings.REMNAWAVE_API_URL,
|
"api_url": settings.REMNAWAVE_API_URL,
|
||||||
"attempts_used": attempts,
|
"attempts_used": attempts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def sync_users_to_panel(self, db: AsyncSession, limit: Optional[int] = None) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Синхронизирует пользователей бота в RemnaWave панель (бот → RemnaWave)
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from app.database.models import User, Subscription
|
||||||
|
from app.external.remnawave_api import UserStatus, TrafficLimitStrategy
|
||||||
|
|
||||||
|
stats = {"created": 0, "updated": 0, "errors": 0, "total_processed": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем всех пользователей с их подписками
|
||||||
|
users_query = select(User).options(selectinload(User.subscription))
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
users_query = users_query.limit(limit)
|
||||||
|
|
||||||
|
result = await db.execute(users_query)
|
||||||
|
users = result.scalars().unique().all()
|
||||||
|
|
||||||
|
logger.info(f"🚀 Начинаем синхронизацию {len(users)} пользователей в RemnaWave")
|
||||||
|
|
||||||
|
async with self.get_api_client() as api:
|
||||||
|
for user in users:
|
||||||
|
stats["total_processed"] += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверяем, есть ли уже пользователь в RemnaWave
|
||||||
|
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
|
||||||
|
|
||||||
|
if existing_users:
|
||||||
|
# Пользователь уже существует - обновляем его данные
|
||||||
|
remnawave_user = existing_users[0]
|
||||||
|
update_kwargs = {
|
||||||
|
'uuid': remnawave_user.uuid,
|
||||||
|
'description': settings.format_remnawave_user_description(
|
||||||
|
full_name=user.full_name,
|
||||||
|
username=user.username,
|
||||||
|
telegram_id=user.telegram_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Если у пользователя есть активная подписка, обновляем и её параметры
|
||||||
|
if hasattr(user, 'subscription') and user.subscription:
|
||||||
|
from app.services.subscription_service import get_traffic_reset_strategy
|
||||||
|
|
||||||
|
update_kwargs.update({
|
||||||
|
'status': UserStatus.ACTIVE if user.subscription.end_date > datetime.utcnow() else UserStatus.DISABLED,
|
||||||
|
'expire_at': user.subscription.end_date,
|
||||||
|
'traffic_limit_bytes': self._gb_to_bytes(user.subscription.traffic_limit_gb),
|
||||||
|
'traffic_limit_strategy': get_traffic_reset_strategy(),
|
||||||
|
'active_internal_squads': user.subscription.connected_squads
|
||||||
|
})
|
||||||
|
|
||||||
|
# Устанавливаем лимит устройств
|
||||||
|
from app.utils.subscription_utils import resolve_hwid_device_limit_for_payload
|
||||||
|
hwid_limit = resolve_hwid_device_limit_for_payload(user.subscription)
|
||||||
|
if hwid_limit is not None:
|
||||||
|
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||||
|
|
||||||
|
await api.update_user(**update_kwargs)
|
||||||
|
stats["updated"] += 1
|
||||||
|
|
||||||
|
logger.info(f"🔄 Пользователь {user.telegram_id} обновлен в RemnaWave")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Создаем нового пользователя в RemnaWave
|
||||||
|
username = settings.format_remnawave_username(
|
||||||
|
full_name=user.full_name,
|
||||||
|
username=user.username,
|
||||||
|
telegram_id=user.telegram_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подготовим параметры для создания
|
||||||
|
create_kwargs = {
|
||||||
|
'username': username,
|
||||||
|
'telegram_id': user.telegram_id,
|
||||||
|
'description': settings.format_remnawave_user_description(
|
||||||
|
full_name=user.full_name,
|
||||||
|
username=user.username,
|
||||||
|
telegram_id=user.telegram_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Если у пользователя есть активная подписка, используем её данные
|
||||||
|
if hasattr(user, 'subscription') and user.subscription:
|
||||||
|
from app.services.subscription_service import get_traffic_reset_strategy
|
||||||
|
|
||||||
|
create_kwargs.update({
|
||||||
|
'status': UserStatus.ACTIVE if user.subscription.end_date > datetime.utcnow() else UserStatus.DISABLED,
|
||||||
|
'expire_at': user.subscription.end_date,
|
||||||
|
'traffic_limit_bytes': self._gb_to_bytes(user.subscription.traffic_limit_gb),
|
||||||
|
'traffic_limit_strategy': get_traffic_reset_strategy(),
|
||||||
|
'active_internal_squads': user.subscription.connected_squads
|
||||||
|
})
|
||||||
|
|
||||||
|
# Устанавливаем лимит устройств
|
||||||
|
from app.utils.subscription_utils import resolve_hwid_device_limit_for_payload
|
||||||
|
hwid_limit = resolve_hwid_device_limit_for_payload(user.subscription)
|
||||||
|
if hwid_limit is not None:
|
||||||
|
create_kwargs['hwid_device_limit'] = hwid_limit
|
||||||
|
else:
|
||||||
|
# Для пользователей без подписки - создаем с базовыми параметрами
|
||||||
|
create_kwargs.update({
|
||||||
|
'status': UserStatus.DISABLED, # По умолчанию отключаем
|
||||||
|
'expire_at': datetime.utcnow() + timedelta(days=30), # 30 дней по умолчанию
|
||||||
|
'traffic_limit_bytes': 0, # 0 трафика по умолчанию
|
||||||
|
'traffic_limit_strategy': TrafficLimitStrategy.NO_RESET
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.create_user(**create_kwargs)
|
||||||
|
stats["created"] += 1
|
||||||
|
|
||||||
|
logger.info(f"✅ Пользователь {user.telegram_id} создан в RemnaWave")
|
||||||
|
|
||||||
|
except Exception as user_error:
|
||||||
|
logger.error(f"❌ Ошибка синхронизации пользователя {user.telegram_id}: {user_error}")
|
||||||
|
stats["errors"] += 1
|
||||||
|
|
||||||
|
logger.info(f"✅ Синхронизация завершена. Создано: {stats['created']}, Обновлено: {stats['updated']}, Ошибок: {stats['errors']}")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Ошибка синхронизации пользователей в RemnaWave: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|||||||
@@ -428,12 +428,13 @@ async def sync_from_panel(
|
|||||||
async def sync_to_panel(
|
async def sync_to_panel(
|
||||||
_: Any = Security(require_api_token),
|
_: Any = Security(require_api_token),
|
||||||
db: AsyncSession = Depends(get_db_session),
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
limit: int = Query(None, ge=1, le=10000, description="Ограничение на количество пользователей для синхронизации")
|
||||||
) -> RemnaWaveGenericSyncResponse:
|
) -> RemnaWaveGenericSyncResponse:
|
||||||
service = _get_service()
|
service = _get_service()
|
||||||
_ensure_service_configured(service)
|
_ensure_service_configured(service)
|
||||||
|
|
||||||
stats = await service.sync_users_to_panel(db)
|
stats = await service.sync_users_to_panel(db, limit=limit)
|
||||||
detail = "Синхронизация в панель выполнена"
|
detail = f"Синхронизация в панель выполнена (лимит: {limit if limit else 'все'})"
|
||||||
return RemnaWaveGenericSyncResponse(success=True, detail=detail, data=stats)
|
return RemnaWaveGenericSyncResponse(success=True, detail=detail, data=stats)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-8
@@ -11,7 +11,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- bot_network
|
- remnawave-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
- bot_network
|
- remnawave-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -74,7 +74,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${WEB_API_PORT:-8080}:8080"
|
- "${WEB_API_PORT:-8080}:8080"
|
||||||
networks:
|
networks:
|
||||||
- bot_network
|
- remnawave-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8080/health\", timeout=5)' || exit 1"]
|
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8080/health\", timeout=5)' || exit 1"]
|
||||||
interval: 60s
|
interval: 60s
|
||||||
@@ -89,9 +89,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
bot_network:
|
remnawave-network:
|
||||||
|
name: remnawave-network
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ipam:
|
external: true
|
||||||
config:
|
|
||||||
- subnet: 172.20.0.0/16
|
|
||||||
gateway: 172.20.0.1
|
|
||||||
|
|||||||
Reference in New Issue
Block a user