mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
283 lines
10 KiB
Python
283 lines
10 KiB
Python
import asyncio
|
||
import logging
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta, time
|
||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.database import AsyncSessionLocal
|
||
from app.database.crud.server_squad import sync_with_remnawave
|
||
from app.services.remnawave_service import (
|
||
RemnaWaveConfigurationError,
|
||
RemnaWaveService,
|
||
)
|
||
from app.utils.cache import cache
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RemnaWaveAutoSyncStatus:
|
||
enabled: bool
|
||
times: List[time]
|
||
next_run: Optional[datetime]
|
||
last_run_started_at: Optional[datetime]
|
||
last_run_finished_at: Optional[datetime]
|
||
last_run_success: Optional[bool]
|
||
last_run_reason: Optional[str]
|
||
last_run_error: Optional[str]
|
||
last_user_stats: Optional[Dict[str, Any]]
|
||
last_server_stats: Optional[Dict[str, Any]]
|
||
is_running: bool
|
||
|
||
|
||
class RemnaWaveAutoSyncService:
|
||
def __init__(
|
||
self,
|
||
service_factory: Callable[[], RemnaWaveService] = RemnaWaveService,
|
||
) -> None:
|
||
self._scheduler_task: Optional[asyncio.Task] = None
|
||
self._scheduler_lock = asyncio.Lock()
|
||
self._sync_lock = asyncio.Lock()
|
||
self._service_factory = service_factory
|
||
self._service = self._service_factory()
|
||
|
||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||
self._initialized = False
|
||
self._pending_refresh = False
|
||
self._pending_run_immediately = False
|
||
|
||
self._next_run: Optional[datetime] = None
|
||
self._last_run_started_at: Optional[datetime] = None
|
||
self._last_run_finished_at: Optional[datetime] = None
|
||
self._last_run_success: Optional[bool] = None
|
||
self._last_run_reason: Optional[str] = None
|
||
self._last_run_error: Optional[str] = None
|
||
self._last_user_stats: Optional[Dict[str, Any]] = None
|
||
self._last_server_stats: Optional[Dict[str, Any]] = None
|
||
|
||
async def initialize(self) -> None:
|
||
self._loop = asyncio.get_running_loop()
|
||
self._initialized = True
|
||
|
||
run_immediately = self._pending_run_immediately
|
||
if self._pending_refresh:
|
||
self._pending_refresh = False
|
||
self._pending_run_immediately = False
|
||
await self.refresh_schedule(run_immediately=run_immediately)
|
||
else:
|
||
await self.refresh_schedule()
|
||
|
||
def refresh_configuration(self) -> None:
|
||
"""Rebuild the RemnaWave service instance using current settings."""
|
||
self._refresh_service()
|
||
|
||
async def refresh_schedule(self, *, run_immediately: bool = False) -> None:
|
||
async with self._scheduler_lock:
|
||
if self._scheduler_task and not self._scheduler_task.done():
|
||
self._scheduler_task.cancel()
|
||
try:
|
||
await self._scheduler_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
finally:
|
||
self._scheduler_task = None
|
||
|
||
if not settings.REMNAWAVE_AUTO_SYNC_ENABLED:
|
||
self._next_run = None
|
||
return
|
||
|
||
times = settings.get_remnawave_auto_sync_times()
|
||
if not times:
|
||
logger.warning(
|
||
"⚠️ Автосинхронизация включена, но расписание пустое. Укажите время запуска."
|
||
)
|
||
self._next_run = None
|
||
return
|
||
|
||
self._scheduler_task = asyncio.create_task(self._run_scheduler(times))
|
||
|
||
if run_immediately:
|
||
asyncio.create_task(self.run_sync_now(reason="immediate"))
|
||
|
||
def schedule_refresh(self, *, run_immediately: bool = False) -> None:
|
||
if not self._initialized:
|
||
self._pending_refresh = True
|
||
if run_immediately:
|
||
self._pending_run_immediately = True
|
||
return
|
||
|
||
loop = self._loop or asyncio.get_running_loop()
|
||
loop.create_task(self.refresh_schedule(run_immediately=run_immediately))
|
||
|
||
async def stop(self) -> None:
|
||
async with self._scheduler_lock:
|
||
if self._scheduler_task and not self._scheduler_task.done():
|
||
self._scheduler_task.cancel()
|
||
try:
|
||
await self._scheduler_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
self._scheduler_task = None
|
||
self._next_run = None
|
||
|
||
async def run_sync_now(self, *, reason: str = "manual") -> Dict[str, Any]:
|
||
if self._sync_lock.locked():
|
||
return {"started": False, "reason": "already_running"}
|
||
|
||
async with self._sync_lock:
|
||
self._last_run_started_at = datetime.utcnow()
|
||
self._last_run_finished_at = None
|
||
self._last_run_reason = reason
|
||
self._last_run_error = None
|
||
self._last_run_success = None
|
||
|
||
try:
|
||
user_stats, server_stats = await self._perform_sync()
|
||
except RemnaWaveConfigurationError as error:
|
||
message = str(error)
|
||
self._last_run_error = message
|
||
self._last_run_success = False
|
||
self._last_user_stats = None
|
||
self._last_server_stats = None
|
||
self._last_run_finished_at = datetime.utcnow()
|
||
logger.error("❌ Автосинхронизация RemnaWave: %s", message)
|
||
return {
|
||
"started": True,
|
||
"success": False,
|
||
"error": message,
|
||
"user_stats": None,
|
||
"server_stats": None,
|
||
}
|
||
except Exception as error:
|
||
message = str(error)
|
||
self._last_run_error = message
|
||
self._last_run_success = False
|
||
self._last_user_stats = None
|
||
self._last_server_stats = None
|
||
self._last_run_finished_at = datetime.utcnow()
|
||
logger.exception("❌ Ошибка автосинхронизации RemnaWave: %s", error)
|
||
return {
|
||
"started": True,
|
||
"success": False,
|
||
"error": message,
|
||
"user_stats": None,
|
||
"server_stats": None,
|
||
}
|
||
|
||
self._last_run_success = True
|
||
self._last_run_error = None
|
||
self._last_user_stats = user_stats
|
||
self._last_server_stats = server_stats
|
||
self._last_run_finished_at = datetime.utcnow()
|
||
|
||
return {
|
||
"started": True,
|
||
"success": True,
|
||
"error": None,
|
||
"user_stats": user_stats,
|
||
"server_stats": server_stats,
|
||
}
|
||
|
||
def get_status(self) -> RemnaWaveAutoSyncStatus:
|
||
times = settings.get_remnawave_auto_sync_times()
|
||
enabled = settings.REMNAWAVE_AUTO_SYNC_ENABLED and bool(times)
|
||
|
||
return RemnaWaveAutoSyncStatus(
|
||
enabled=enabled,
|
||
times=times,
|
||
next_run=self._next_run,
|
||
last_run_started_at=self._last_run_started_at,
|
||
last_run_finished_at=self._last_run_finished_at,
|
||
last_run_success=self._last_run_success,
|
||
last_run_reason=self._last_run_reason,
|
||
last_run_error=self._last_run_error,
|
||
last_user_stats=self._last_user_stats,
|
||
last_server_stats=self._last_server_stats,
|
||
is_running=self._sync_lock.locked(),
|
||
)
|
||
|
||
async def _run_scheduler(self, times: List[time]) -> None:
|
||
try:
|
||
while True:
|
||
next_run = self._calculate_next_run(times)
|
||
self._next_run = next_run
|
||
|
||
delay = (next_run - datetime.utcnow()).total_seconds()
|
||
if delay > 0:
|
||
await asyncio.sleep(delay)
|
||
|
||
await self.run_sync_now(reason="auto")
|
||
except asyncio.CancelledError:
|
||
raise
|
||
finally:
|
||
self._next_run = None
|
||
|
||
def _refresh_service(self) -> RemnaWaveService:
|
||
self._service = self._service_factory()
|
||
return self._service
|
||
|
||
async def _perform_sync(self) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||
service = self._refresh_service()
|
||
|
||
if not service.is_configured:
|
||
raise RemnaWaveConfigurationError(
|
||
service.configuration_error or "RemnaWave API не настроен"
|
||
)
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user_stats = await service.sync_users_from_panel(session, "all")
|
||
server_stats = await self._sync_servers(session, service)
|
||
|
||
return user_stats, server_stats
|
||
|
||
async def _sync_servers(
|
||
self,
|
||
session: AsyncSession,
|
||
service: RemnaWaveService,
|
||
) -> Dict[str, Any]:
|
||
squads = await service.get_all_squads()
|
||
|
||
if not squads:
|
||
logger.warning("⚠️ Не удалось получить сквады из RemnaWave для автосинхронизации")
|
||
return {"created": 0, "updated": 0, "removed": 0, "total": 0}
|
||
|
||
created, updated, removed = await sync_with_remnawave(session, squads)
|
||
|
||
try:
|
||
await cache.delete_pattern("available_countries*")
|
||
except Exception as error:
|
||
logger.warning("⚠️ Не удалось очистить кеш стран после автосинхронизации: %s", error)
|
||
|
||
return {
|
||
"created": created,
|
||
"updated": updated,
|
||
"removed": removed,
|
||
"total": len(squads),
|
||
}
|
||
|
||
@staticmethod
|
||
def _calculate_next_run(times: List[time]) -> datetime:
|
||
now = datetime.utcnow()
|
||
today = now.date()
|
||
|
||
for scheduled in sorted(times):
|
||
candidate = datetime.combine(today, scheduled)
|
||
if candidate > now:
|
||
return candidate
|
||
|
||
first_time = sorted(times)[0]
|
||
next_day = today + timedelta(days=1)
|
||
return datetime.combine(next_day, first_time)
|
||
|
||
|
||
def _create_service() -> RemnaWaveAutoSyncService:
|
||
service = RemnaWaveAutoSyncService()
|
||
return service
|
||
|
||
|
||
remnawave_sync_service = _create_service()
|