"""Статус и 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"Статус серверов", f"Онлайн: {online} / {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} {n.name} — Онлайн{ping}{extra}") elif n.panel_disabled: icon = "⚫" lines.append(f"{icon} {flag} {n.name} — Выключен") else: icon = "🔴" lines.append(f"{icon} {flag} {n.name} — Оффлайн") 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)