"""CRUD + connection tests for linked 3x-ui panels.""" from __future__ import annotations import json from datetime import datetime, timezone from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models import XuiPanel from app.services.xui import ( SUPPORTED_CLIENT_PROTOCOLS, XuiApiError, XuiClient, normalize_base_url, ) def _client_for(panel: XuiPanel) -> XuiClient: return XuiClient( panel.base_url, username=panel.username, password=panel.password, api_token=panel.api_token, verify_ssl=panel.verify_ssl, ) async def list_panels(session: AsyncSession) -> list[XuiPanel]: result = await session.execute(select(XuiPanel).order_by(XuiPanel.id.desc())) return list(result.scalars().all()) async def create_panel( session: AsyncSession, *, name: str, base_url: str, username: str = "", password: str = "", api_token: str = "", verify_ssl: bool = True, ) -> XuiPanel: token = api_token.strip() or None user = username.strip() or None pwd = password.strip() or None if not token and not (user and pwd): raise ValueError("Укажите API Token или логин и пароль") url = normalize_base_url(base_url) panel = XuiPanel( name=(name.strip() or url), base_url=url, username=user, password=pwd, api_token=token, verify_ssl=verify_ssl, is_enabled=True, ) session.add(panel) await session.commit() await session.refresh(panel) await check_panel(session, panel.id) await session.refresh(panel) if not panel.is_reachable: raise ValueError(panel.last_error or "Не удалось подключиться к 3x-ui") return panel async def check_panel(session: AsyncSession, panel_id: int) -> XuiPanel: panel = await session.get(XuiPanel, panel_id) if not panel: raise ValueError("Панель не найдена") now = datetime.now(timezone.utc) try: async with _client_for(panel) as client: info = await client.test_connection() panel.is_reachable = True panel.last_error = None panel.panel_info = json.dumps(info, ensure_ascii=False, default=str)[:8000] panel.last_checked = now except (XuiApiError, ValueError, OSError) as exc: panel.is_reachable = False panel.last_error = str(exc)[:2000] panel.last_checked = now await session.commit() await session.refresh(panel) raise ValueError(str(exc)) from exc await session.commit() await session.refresh(panel) return panel async def delete_panel(session: AsyncSession, panel_id: int) -> None: panel = await session.get(XuiPanel, panel_id) if panel: await session.delete(panel) await session.commit() async def fetch_panel_data(panel: XuiPanel) -> dict[str, Any]: async with _client_for(panel) as client: status = await client.get_status() all_inbounds = await client.list_inbounds() available = await client.list_available_inbounds( protocols=SUPPORTED_CLIENT_PROTOCOLS, enabled_only=False ) vless_inbounds = [i for i in available if str(i.get("protocol")).lower() == "vless"] wg_inbounds = [i for i in available if str(i.get("protocol")).lower() == "wireguard"] try: clients = await client.list_clients() except XuiApiError: clients = [] return { "status": status, "inbounds": all_inbounds, "available_inbounds": available, "vless_inbounds": vless_inbounds, "wireguard_inbounds": wg_inbounds, "clients": clients, } async def load_available_inbounds( panel: XuiPanel, *, protocol: str | None = None, ) -> list[dict]: protocols: tuple[str, ...] if protocol: protocols = (protocol.lower(),) else: protocols = SUPPORTED_CLIENT_PROTOCOLS async with _client_for(panel) as client: return await client.list_available_inbounds(protocols=protocols, enabled_only=True) def compute_expiry_time(*, days: int, start_after_first_use: bool) -> int: """ 3x-ui expiryTime encoding: 0 — unlimited >0 — absolute deadline in unix ms <0 — duration ms; countdown starts after first use """ days = int(days or 0) if days <= 0: return 0 duration_ms = days * 24 * 60 * 60 * 1000 if start_after_first_use: return -duration_ms import time return int(time.time() * 1000) + duration_ms async def add_xui_client( panel: XuiPanel, *, protocol: str, email: str, inbound_id: int, total_gb: int = 0, limit_ip: int = 0, expiry_days: int = 0, start_after_first_use: bool = False, flow: str = "", comment: str = "", ) -> dict: proto = protocol.strip().lower() bytes_limit = total_gb * 1024 * 1024 * 1024 if total_gb > 0 else 0 expiry_time = compute_expiry_time(days=expiry_days, start_after_first_use=start_after_first_use) async with _client_for(panel) as client: if proto == "vless": result = await client.add_vless_client( email=email, inbound_id=inbound_id, total_gb=bytes_limit, expiry_time=expiry_time, limit_ip=limit_ip, flow=flow, comment=comment, ) elif proto == "wireguard": result = await client.add_wireguard_client( email=email, inbound_id=inbound_id, total_gb=bytes_limit, expiry_time=expiry_time, limit_ip=limit_ip, comment=comment, ) else: raise ValueError("Поддерживаются только vless и wireguard") result["expiry_days"] = expiry_days result["start_after_first_use"] = start_after_first_use return result