175 lines
5.1 KiB
Python
175 lines
5.1 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)
|
|
|
|
|
|
async def add_xui_client(
|
|
panel: XuiPanel,
|
|
*,
|
|
protocol: str,
|
|
email: str,
|
|
inbound_id: int,
|
|
total_gb: int = 0,
|
|
limit_ip: int = 0,
|
|
flow: str = "",
|
|
comment: str = "",
|
|
) -> dict:
|
|
proto = protocol.strip().lower()
|
|
bytes_limit = total_gb * 1024 * 1024 * 1024 if total_gb > 0 else 0
|
|
async with _client_for(panel) as client:
|
|
if proto == "vless":
|
|
return await client.add_vless_client(
|
|
email=email,
|
|
inbound_id=inbound_id,
|
|
total_gb=bytes_limit,
|
|
limit_ip=limit_ip,
|
|
flow=flow,
|
|
comment=comment,
|
|
)
|
|
if proto == "wireguard":
|
|
return await client.add_wireguard_client(
|
|
email=email,
|
|
inbound_id=inbound_id,
|
|
total_gb=bytes_limit,
|
|
limit_ip=limit_ip,
|
|
comment=comment,
|
|
)
|
|
raise ValueError("Поддерживаются только vless и wireguard")
|