127 lines
4.0 KiB
Python
127 lines
4.0 KiB
Python
"""Статус и TCP-пинг нод Remnawave."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import socket
|
|
from dataclasses import dataclass
|
|
|
|
from remnawave import RemnawaveSDK
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class NodeStatus:
|
|
name: str
|
|
address: str
|
|
port: int | None
|
|
country_code: str
|
|
panel_connected: bool
|
|
panel_disabled: bool
|
|
tcp_ok: bool
|
|
tcp_ms: float | None
|
|
users_online: int | None
|
|
last_status_message: str | None
|
|
|
|
@property
|
|
def online(self) -> bool:
|
|
if self.panel_disabled:
|
|
return False
|
|
# Онлайн только если панель видит ноду и TCP отвечает
|
|
return self.panel_connected and self.tcp_ok
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
if self.panel_disabled:
|
|
return "Выключен"
|
|
if self.online:
|
|
return "Онлайн"
|
|
return "Оффлайн"
|
|
|
|
|
|
async def tcp_ping(host: str, port: int, timeout: float = 2.5) -> tuple[bool, float | None]:
|
|
loop = asyncio.get_running_loop()
|
|
started = loop.time()
|
|
|
|
def _connect() -> None:
|
|
with socket.create_connection((host, port), timeout=timeout):
|
|
return None
|
|
|
|
try:
|
|
await asyncio.wait_for(asyncio.to_thread(_connect), timeout=timeout + 0.5)
|
|
ms = (loop.time() - started) * 1000
|
|
return True, round(ms, 1)
|
|
except Exception: # noqa: BLE001 — любой сбой = недоступен
|
|
return False, None
|
|
|
|
|
|
async def fetch_nodes_status(sdk: RemnawaveSDK) -> list[NodeStatus]:
|
|
nodes = await sdk.nodes.get_all_nodes()
|
|
results: list[NodeStatus] = []
|
|
|
|
async def _one(node) -> NodeStatus:
|
|
port = node.port or 443
|
|
tcp_ok, tcp_ms = await tcp_ping(node.address, port)
|
|
return NodeStatus(
|
|
name=node.name,
|
|
address=node.address,
|
|
port=node.port,
|
|
country_code=node.country_code or "XX",
|
|
panel_connected=bool(node.is_connected),
|
|
panel_disabled=bool(node.is_disabled),
|
|
tcp_ok=tcp_ok,
|
|
tcp_ms=tcp_ms,
|
|
users_online=node.users_online,
|
|
last_status_message=node.last_status_message,
|
|
)
|
|
|
|
tasks = [_one(node) for node in nodes]
|
|
if tasks:
|
|
results = list(await asyncio.gather(*tasks))
|
|
# сохраняем порядок панели
|
|
return results
|
|
|
|
|
|
def format_nodes_status(nodes: list[NodeStatus], *, detailed: bool = False) -> str:
|
|
if not nodes:
|
|
return "Серверы не найдены в панели Remnawave."
|
|
|
|
online = sum(1 for n in nodes if n.online)
|
|
lines = [
|
|
f"<b>Статус серверов</b>",
|
|
f"Онлайн: <b>{online}</b> / {len(nodes)}",
|
|
"",
|
|
]
|
|
|
|
for n in nodes:
|
|
flag = _flag(n.country_code)
|
|
if n.online:
|
|
icon = "🟢"
|
|
ping = f" · {n.tcp_ms:.0f} ms" if n.tcp_ms is not None else ""
|
|
extra = f" · 👥 {n.users_online}" if n.users_online is not None else ""
|
|
lines.append(f"{icon} {flag} <b>{n.name}</b> — Онлайн{ping}{extra}")
|
|
elif n.panel_disabled:
|
|
icon = "⚫"
|
|
lines.append(f"{icon} {flag} <b>{n.name}</b> — Выключен")
|
|
else:
|
|
icon = "🔴"
|
|
lines.append(f"{icon} {flag} <b>{n.name}</b> — Оффлайн")
|
|
if detailed:
|
|
reasons: list[str] = []
|
|
if not n.panel_connected:
|
|
reasons.append("панель не видит ноду")
|
|
if not n.tcp_ok:
|
|
reasons.append(f"нет ответа {n.address}:{n.port or 443}")
|
|
if n.last_status_message:
|
|
reasons.append(n.last_status_message)
|
|
if reasons:
|
|
lines.append(" └ " + "; ".join(reasons))
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _flag(code: str) -> str:
|
|
code = (code or "XX").upper()
|
|
if len(code) != 2 or not code.isalpha():
|
|
return "🌐"
|
|
return "".join(chr(0x1F1E6 + ord(c) - ord("A")) for c in code)
|