Files
vpn/app/services/xui_panels.py
T

202 lines
6.0 KiB
Python

"""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