Files

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)