mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-21 20:01:47 +00:00
Handle optional RemnaWave dependencies in web API
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.remnawave_service import RemnaWaveConfigurationError, RemnaWaveService
|
||||
|
||||
from ..dependencies import get_db_session, require_api_token
|
||||
from ..schemas.remnawave import (
|
||||
RemnaWaveConnectionStatus,
|
||||
@@ -31,11 +29,41 @@ from ..schemas.remnawave import (
|
||||
RemnaWaveUserTrafficResponse,
|
||||
)
|
||||
|
||||
try: # pragma: no cover - импорт может не работать без optional-зависимостей
|
||||
from app.services.remnawave_service import ( # type: ignore
|
||||
RemnaWaveConfigurationError,
|
||||
RemnaWaveService,
|
||||
)
|
||||
except Exception: # pragma: no cover - при ошибке импорта скрываем функционал
|
||||
RemnaWaveConfigurationError = None # type: ignore[assignment]
|
||||
RemnaWaveService = None # type: ignore[assignment]
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - только для типов в IDE
|
||||
from app.services.remnawave_service import RemnaWaveService as RemnaWaveServiceType
|
||||
else:
|
||||
RemnaWaveServiceType = Any
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _ensure_service_configured(service: RemnaWaveService) -> None:
|
||||
def _get_service() -> "RemnaWaveServiceType":
|
||||
if RemnaWaveService is None: # pragma: no cover - зависимость не доступна
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="RemnaWave сервис недоступен",
|
||||
)
|
||||
|
||||
return RemnaWaveService()
|
||||
|
||||
|
||||
def _ensure_service_configured(service: "RemnaWaveServiceType") -> None:
|
||||
if RemnaWaveService is None: # pragma: no cover - зависимость не доступна
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="RemnaWave сервис недоступен",
|
||||
)
|
||||
|
||||
if not service.is_configured:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
@@ -74,7 +102,7 @@ def _parse_last_updated(value: Any) -> Optional[datetime]:
|
||||
async def get_remnawave_status(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveStatusResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
|
||||
connection_info: Optional[RemnaWaveConnectionStatus] = None
|
||||
connection_result = await service.test_api_connection()
|
||||
@@ -93,7 +121,7 @@ async def get_remnawave_status(
|
||||
async def get_system_statistics(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveSystemStatsResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.get_system_statistics()
|
||||
@@ -108,7 +136,7 @@ async def get_system_statistics(
|
||||
async def list_nodes(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNodeListResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
nodes = await service.get_all_nodes()
|
||||
@@ -120,7 +148,7 @@ async def list_nodes(
|
||||
async def get_nodes_realtime_usage(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> List[Dict[str, Any]]:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
return await service.get_nodes_realtime_usage()
|
||||
|
||||
@@ -130,7 +158,7 @@ async def get_node_details(
|
||||
node_uuid: str,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNode:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
node = await service.get_node_details(node_uuid)
|
||||
@@ -144,7 +172,7 @@ async def get_node_statistics(
|
||||
node_uuid: str,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNodeStatisticsResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.get_node_statistics(node_uuid)
|
||||
@@ -171,7 +199,7 @@ async def get_node_usage_range(
|
||||
end: Optional[datetime] = Query(default=None),
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNodeUsageResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
end_dt = end or datetime.utcnow()
|
||||
@@ -190,7 +218,7 @@ async def manage_node(
|
||||
payload: RemnaWaveNodeActionRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNodeActionResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
success = await service.manage_node(node_uuid, payload.action)
|
||||
@@ -212,7 +240,7 @@ async def manage_node(
|
||||
async def restart_all_nodes(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveNodeActionResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
success = await service.restart_all_nodes()
|
||||
@@ -224,7 +252,7 @@ async def restart_all_nodes(
|
||||
async def list_squads(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveSquadListResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
squads = await service.get_all_squads()
|
||||
@@ -237,7 +265,7 @@ async def get_squad_details(
|
||||
squad_uuid: str,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveSquad:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
squad = await service.get_squad_details(squad_uuid)
|
||||
@@ -251,7 +279,7 @@ async def create_squad(
|
||||
payload: RemnaWaveSquadCreateRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveOperationResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
success = await service.create_squad(payload.name, payload.inbound_uuids)
|
||||
@@ -265,7 +293,7 @@ async def update_squad(
|
||||
payload: RemnaWaveSquadUpdateRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveOperationResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
success = False
|
||||
@@ -288,7 +316,7 @@ async def squad_actions(
|
||||
payload: RemnaWaveSquadActionRequest,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveOperationResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
action = payload.action
|
||||
@@ -322,7 +350,7 @@ async def squad_actions(
|
||||
async def list_inbounds(
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveInboundsResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
inbounds = await service.get_all_inbounds()
|
||||
@@ -334,7 +362,7 @@ async def get_user_traffic(
|
||||
telegram_id: int,
|
||||
_: Any = Security(require_api_token),
|
||||
) -> RemnaWaveUserTrafficResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.get_user_traffic_stats(telegram_id)
|
||||
@@ -350,15 +378,17 @@ async def sync_from_panel(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
try:
|
||||
stats = await service.sync_users_from_panel(db, payload.mode)
|
||||
detail = "Синхронизация из панели выполнена"
|
||||
return RemnaWaveGenericSyncResponse(success=True, detail=detail, data=stats)
|
||||
except RemnaWaveConfigurationError as exc:
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, str(exc)) from exc
|
||||
except Exception as exc: # pragma: no cover - точный тип зависит от импорта
|
||||
if RemnaWaveConfigurationError and isinstance(exc, RemnaWaveConfigurationError):
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, str(exc)) from exc
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/sync/to-panel", response_model=RemnaWaveGenericSyncResponse)
|
||||
@@ -366,7 +396,7 @@ async def sync_to_panel(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.sync_users_to_panel(db)
|
||||
@@ -379,7 +409,7 @@ async def validate_and_fix_subscriptions(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.validate_and_fix_subscriptions(db)
|
||||
@@ -392,7 +422,7 @@ async def cleanup_orphaned_subscriptions(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.cleanup_orphaned_subscriptions(db)
|
||||
@@ -405,7 +435,7 @@ async def sync_subscription_statuses(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
stats = await service.sync_subscription_statuses(db)
|
||||
@@ -418,7 +448,7 @@ async def get_sync_recommendations(
|
||||
_: Any = Security(require_api_token),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> RemnaWaveGenericSyncResponse:
|
||||
service = RemnaWaveService()
|
||||
service = _get_service()
|
||||
_ensure_service_configured(service)
|
||||
|
||||
data = await service.get_sync_recommendations(db)
|
||||
|
||||
@@ -137,13 +137,25 @@ curl -X POST "http://127.0.0.1:8080/tokens" \
|
||||
| `GET` | `/remnawave/status` | Проверка конфигурации и доступности RemnaWave API. |
|
||||
| `GET` | `/remnawave/system` | Агрегированная статистика по пользователям, нодам и трафику. |
|
||||
| `GET` | `/remnawave/nodes` | Список нод и их текущее состояние. |
|
||||
| `GET` | `/remnawave/nodes/realtime` | Текущая загрузка нод (realtime-метрики RemnaWave). |
|
||||
| `GET` | `/remnawave/nodes/{uuid}` | Детальная информация по конкретной ноде. |
|
||||
| `GET` | `/remnawave/nodes/{uuid}/statistics` | Агрегированная статистика и история нагрузок по ноде. |
|
||||
| `GET` | `/remnawave/nodes/{uuid}/usage` | История использования ноды пользователями за выбранный период. |
|
||||
| `POST` | `/remnawave/nodes/{uuid}/actions` | Включение, отключение или перезапуск ноды. |
|
||||
| `GET` | `/remnawave/squads` | Управление сквадами и их инбаундами. |
|
||||
| `POST` | `/remnawave/nodes/restart` | Массовый перезапуск всех нод в RemnaWave. |
|
||||
| `GET` | `/remnawave/squads` | Список внутренних сквадов с составом и статистикой. |
|
||||
| `GET` | `/remnawave/squads/{uuid}` | Детали выбранного сквада. |
|
||||
| `POST` | `/remnawave/squads` | Создание нового сквада и привязка inbounds. |
|
||||
| `PATCH` | `/remnawave/squads/{uuid}` | Обновление имени или состава inbounds сквада. |
|
||||
| `POST` | `/remnawave/squads/{uuid}/actions` | Массовые операции: добавить/удалить всех, переименовать, обновить inbounds, удалить. |
|
||||
| `GET` | `/remnawave/inbounds` | Список доступных inbounds в панели RemnaWave. |
|
||||
| `GET` | `/remnawave/users/{telegram_id}/traffic` | Использование трафика конкретного пользователя RemnaWave. |
|
||||
| `POST` | `/remnawave/sync/from-panel` | Синхронизация пользователей и подписок из панели в бота. |
|
||||
| `POST` | `/remnawave/sync/to-panel` | Обратная синхронизация данных бота в панель. |
|
||||
| `POST` | `/remnawave/sync/subscriptions/validate` | Проверка и восстановление подписок в RemnaWave. |
|
||||
|
||||
> Остальные операции (история трафика, рекомендации по синхронизации, очистка подписок и т. д.) также доступны в этом разделе Swagger UI.
|
||||
| `POST` | `/remnawave/sync/subscriptions/cleanup` | Очистка «осиротевших» подписок и пользователей в RemnaWave. |
|
||||
| `POST` | `/remnawave/sync/subscriptions/statuses` | Приведение статусов подписок в боте и панели к единому виду. |
|
||||
| `GET` | `/remnawave/sync/recommendations` | Рекомендации по синхронизации: что добавить, обновить или удалить. |
|
||||
|
||||
|
||||
> Все списковые эндпоинты поддерживают пагинацию (`limit`, `offset`) и фильтры, описанные в OpenAPI спецификации. Если `WEB_API_DOCS_ENABLED=true`, документация доступна по `/docs`. В ответах `/settings` поле `choices` всегда массив: пустой список означает отсутствие предопределённых значений.
|
||||
|
||||
Reference in New Issue
Block a user