From d6b33cc614bd7de4bcfd33ebff971e5d26f9ee9c Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Thu, 11 Jun 2026 18:26:32 +0200 Subject: [PATCH] nowe --- working_copy/conanjurer/README.md | 73 ------------ .../conan-discord-bridge/.env.example | 21 ---- .../conanjurer/conan-discord-bridge/README.md | 73 ------------ .../conan-discord-bridge/bot/__init__.py | 0 .../conan-discord-bridge/bot/cogs/__init__.py | 0 .../bot/cogs/from_conan.py | 44 ------- .../conan-discord-bridge/bot/core/__init__.py | 0 .../conan-discord-bridge/bot/core/config.py | 55 --------- .../conan-discord-bridge/bot/core/rcon.py | 55 --------- .../conan-discord-bridge/bot/main.py | 49 -------- .../conan-discord-bridge/requirements.txt | 5 - .../to_conan.py => conanjurer_commands.py} | 48 ++++++-- .../logwatch.py => conanjurer_functions.py} | 108 +++++++++++++++--- 13 files changed, 131 insertions(+), 400 deletions(-) delete mode 100644 working_copy/conanjurer/README.md delete mode 100644 working_copy/conanjurer/conan-discord-bridge/.env.example delete mode 100644 working_copy/conanjurer/conan-discord-bridge/README.md delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/__init__.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/cogs/__init__.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/cogs/from_conan.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/core/__init__.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/core/config.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/core/rcon.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/bot/main.py delete mode 100644 working_copy/conanjurer/conan-discord-bridge/requirements.txt rename working_copy/{conanjurer/conan-discord-bridge/bot/cogs/to_conan.py => conanjurer_commands.py} (69%) rename working_copy/{conanjurer/conan-discord-bridge/bot/core/logwatch.py => conanjurer_functions.py} (59%) diff --git a/working_copy/conanjurer/README.md b/working_copy/conanjurer/README.md deleted file mode 100644 index d26f477..0000000 --- a/working_copy/conanjurer/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Tovah-Zar — Most Discord ⇄ Conan Exiles - -Dwukierunkowy most między serwerem Conan Exiles (Enhanced) a serwerem Discord. -Działa w trybie podstawowym **bez moda** (natywny RCON + log serwera), a z małym modem -mostu (Chat V2 API) zyskuje komendy in-game i wiadomości RP z nadawcą. - -## Co potrafi - -**Conan → Discord** -- Relacja czatu z gry na żywo na kanał Discorda. -- Powiadomienia: logowania/wylogowania, zgony, eventy GM. -- (z modem) `/discord ` w grze → wiadomość na Discord. - -**Discord → Conan** -- `!say ` → `broadcast` w grze (widzą wszyscy) — DZIAŁA NA SAMYM RCON. -- `!players` → lista graczy online (RCON `listplayers`). -- `!kick`, `!ban` (tylko role GM). -- (z modem) `!ogłoś ` → wiadomość RP z nadawcą (np. „Król Khasar: ..."). - -## Architektura - -``` - ┌──────────────────────────────────────────┐ - │ BOT (Python, ten projekt) │ - Discord ◄───►│ discord.py ──► RCON client ──► Conan │ - │ ▲ (TCP, komendy) │ - │ │ │ - │ log watcher ◄── SFTP/lokalny log Conana ◄───┤ Conan pisze log - └──────────────────────────────────────────┘ -``` - -- **RCON** = kanał Discord→Conan (komendy z efektem w grze). -- **Log watcher** = kanał Conan→Discord (czat/zdarzenia z gry). -- Bot stoi na VPS lub Twoim NUC-u (na wynajętym Host Havoc nie ma dostępu do OS maszyny — łączy się zdalnie po RCON + SFTP). - -## Szybki start - -```bash -python -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env # uzupełnij dane -python -m bot.main -``` - -## Wymagania po stronie Conan/Host Havoc -- Włączony **RCON** (w panelu Host Havoc / `Engine.ini`): `RconEnabled=True`, `RconPassword=...`, `RconPort=...`. -- Dostęp do pliku logu serwera: lokalnie (self-host) lub przez **SFTP** (Host Havoc). - -## Struktura plików -``` -conan-discord-bridge/ -├── README.md -├── requirements.txt -├── .env.example -├── config.example.toml -└── bot/ - ├── main.py # punkt wejścia, ładuje cogs - ├── core/ - │ ├── config.py # ładowanie .env / toml - │ ├── rcon.py # klient Source RCON (async) - │ └── logwatch.py # tail logu lokalnie lub po SFTP + parser - └── cogs/ - ├── from_conan.py # Conan -> Discord (relacja czatu/eventów) - └── to_conan.py # Discord -> Conan (komendy RCON) -``` - -## Most z własnym modem (opcjonalnie, później) -Mały mod na **Tot ! Chat V2 API** definiuje: -- komendę `/discord ` → zapisuje znacznik do logu, który bot wykrywa, -- kanał RP, na który bot (przez RCON-owy hook) wysyła wiadomości „z nadawcą". -Bot jest na to gotowy — patrz `cogs/to_conan.py` (funkcja `rp_announce`). -``` -``` diff --git a/working_copy/conanjurer/conan-discord-bridge/.env.example b/working_copy/conanjurer/conan-discord-bridge/.env.example deleted file mode 100644 index 2491030..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# ---- Discord ---- -DISCORD_TOKEN=wklej_token_bota -DISCORD_GUILD_ID=000000000000000000 -CHAN_CHAT=000000000000000000 # kanał, na który leci czat z gry -CHAN_EVENTS=000000000000000000 # kanał na logowania/zgony/eventy -GM_ROLE_ID=000000000000000000 # rola uprawniona do komend Discord->Conan - -# ---- Conan RCON ---- -RCON_HOST=1.2.3.4 # IP serwera z panelu Host Havoc -RCON_PORT=25575 # RconPort -RCON_PASSWORD=twoje_haslo_rcon - -# ---- Log serwera Conan ---- -LOG_MODE=sftp # "local" (self-host) albo "sftp" (Host Havoc) -LOG_PATH=/ConanSandbox/Saved/Logs/ConanSandbox.log - -# Pola SFTP (wypełnij tylko gdy LOG_MODE=sftp) -SFTP_HOST=1.2.3.4 -SFTP_PORT=22 -SFTP_USER=ftp_user -SFTP_PASSWORD=ftp_haslo diff --git a/working_copy/conanjurer/conan-discord-bridge/README.md b/working_copy/conanjurer/conan-discord-bridge/README.md deleted file mode 100644 index d26f477..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Tovah-Zar — Most Discord ⇄ Conan Exiles - -Dwukierunkowy most między serwerem Conan Exiles (Enhanced) a serwerem Discord. -Działa w trybie podstawowym **bez moda** (natywny RCON + log serwera), a z małym modem -mostu (Chat V2 API) zyskuje komendy in-game i wiadomości RP z nadawcą. - -## Co potrafi - -**Conan → Discord** -- Relacja czatu z gry na żywo na kanał Discorda. -- Powiadomienia: logowania/wylogowania, zgony, eventy GM. -- (z modem) `/discord ` w grze → wiadomość na Discord. - -**Discord → Conan** -- `!say ` → `broadcast` w grze (widzą wszyscy) — DZIAŁA NA SAMYM RCON. -- `!players` → lista graczy online (RCON `listplayers`). -- `!kick`, `!ban` (tylko role GM). -- (z modem) `!ogłoś ` → wiadomość RP z nadawcą (np. „Król Khasar: ..."). - -## Architektura - -``` - ┌──────────────────────────────────────────┐ - │ BOT (Python, ten projekt) │ - Discord ◄───►│ discord.py ──► RCON client ──► Conan │ - │ ▲ (TCP, komendy) │ - │ │ │ - │ log watcher ◄── SFTP/lokalny log Conana ◄───┤ Conan pisze log - └──────────────────────────────────────────┘ -``` - -- **RCON** = kanał Discord→Conan (komendy z efektem w grze). -- **Log watcher** = kanał Conan→Discord (czat/zdarzenia z gry). -- Bot stoi na VPS lub Twoim NUC-u (na wynajętym Host Havoc nie ma dostępu do OS maszyny — łączy się zdalnie po RCON + SFTP). - -## Szybki start - -```bash -python -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env # uzupełnij dane -python -m bot.main -``` - -## Wymagania po stronie Conan/Host Havoc -- Włączony **RCON** (w panelu Host Havoc / `Engine.ini`): `RconEnabled=True`, `RconPassword=...`, `RconPort=...`. -- Dostęp do pliku logu serwera: lokalnie (self-host) lub przez **SFTP** (Host Havoc). - -## Struktura plików -``` -conan-discord-bridge/ -├── README.md -├── requirements.txt -├── .env.example -├── config.example.toml -└── bot/ - ├── main.py # punkt wejścia, ładuje cogs - ├── core/ - │ ├── config.py # ładowanie .env / toml - │ ├── rcon.py # klient Source RCON (async) - │ └── logwatch.py # tail logu lokalnie lub po SFTP + parser - └── cogs/ - ├── from_conan.py # Conan -> Discord (relacja czatu/eventów) - └── to_conan.py # Discord -> Conan (komendy RCON) -``` - -## Most z własnym modem (opcjonalnie, później) -Mały mod na **Tot ! Chat V2 API** definiuje: -- komendę `/discord ` → zapisuje znacznik do logu, który bot wykrywa, -- kanał RP, na który bot (przez RCON-owy hook) wysyła wiadomości „z nadawcą". -Bot jest na to gotowy — patrz `cogs/to_conan.py` (funkcja `rp_announce`). -``` -``` diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/__init__.py b/working_copy/conanjurer/conan-discord-bridge/bot/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/cogs/__init__.py b/working_copy/conanjurer/conan-discord-bridge/bot/cogs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/cogs/from_conan.py b/working_copy/conanjurer/conan-discord-bridge/bot/cogs/from_conan.py deleted file mode 100644 index 21d2633..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/bot/cogs/from_conan.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Cog: relacja Conan -> Discord. Uruchamia obserwator logu i wrzuca zdarzenia -na właściwe kanały.""" -from __future__ import annotations -import asyncio -import logging - -import discord -from discord.ext import commands - -from bot.core.logwatch import watch, Event - -log = logging.getLogger("from_conan") - - -class FromConan(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self._task: asyncio.Task | None = None - - async def cog_load(self): - self._task = asyncio.create_task(self._run()) - - async def cog_unload(self): - if self._task: - self._task.cancel() - - async def _run(self): - await self.bot.wait_until_ready() - cfg = self.bot.cfg - chat_ch = self.bot.get_channel(cfg.chan_chat) - evt_ch = self.bot.get_channel(cfg.chan_events) - - async def on_event(e: Event): - target = chat_ch if e.kind == "chat" else evt_ch - if target is not None: - # allowed_mentions: nie pinguj nikogo treścią z gry - await target.send(e.text, allowed_mentions=discord.AllowedMentions.none()) - - log.info("Start obserwacji logu (tryb=%s)", cfg.log_mode) - await watch(cfg, on_event) - - -async def setup(bot: commands.Bot): - await bot.add_cog(FromConan(bot)) diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/core/__init__.py b/working_copy/conanjurer/conan-discord-bridge/bot/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/core/config.py b/working_copy/conanjurer/conan-discord-bridge/bot/core/config.py deleted file mode 100644 index 027c6e1..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/bot/core/config.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Ładowanie konfiguracji z .env (proste i wystarczające dla jednego serwera).""" -from __future__ import annotations -import os -from dataclasses import dataclass -from dotenv import load_dotenv - -load_dotenv() - - -def _req(key: str) -> str: - val = os.getenv(key) - if not val or val.startswith("wklej") or val.startswith("000000"): - raise RuntimeError(f"Brak/placeholder w .env: {key}") - return val - - -@dataclass(frozen=True) -class Config: - # Discord - token: str - guild_id: int - chan_chat: int - chan_events: int - gm_role_id: int - # RCON - rcon_host: str - rcon_port: int - rcon_password: str - # Log - log_mode: str # "local" | "sftp" - log_path: str - sftp_host: str | None - sftp_port: int - sftp_user: str | None - sftp_password: str | None - - @staticmethod - def load() -> "Config": - mode = os.getenv("LOG_MODE", "local").lower() - return Config( - token=_req("DISCORD_TOKEN"), - guild_id=int(_req("DISCORD_GUILD_ID")), - chan_chat=int(_req("CHAN_CHAT")), - chan_events=int(_req("CHAN_EVENTS")), - gm_role_id=int(_req("GM_ROLE_ID")), - rcon_host=_req("RCON_HOST"), - rcon_port=int(os.getenv("RCON_PORT", "25575")), - rcon_password=_req("RCON_PASSWORD"), - log_mode=mode, - log_path=_req("LOG_PATH"), - sftp_host=os.getenv("SFTP_HOST"), - sftp_port=int(os.getenv("SFTP_PORT", "22")), - sftp_user=os.getenv("SFTP_USER"), - sftp_password=os.getenv("SFTP_PASSWORD"), - ) diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/core/rcon.py b/working_copy/conanjurer/conan-discord-bridge/bot/core/rcon.py deleted file mode 100644 index 51e6cba..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/bot/core/rcon.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Cienka, odporna na rozłączenia otoczka na klienta Source RCON. - -Conan Exiles wystawia standardowy Source RCON (TCP). Używamy aiomcrcon, -który implementuje ten protokół. Reconnect na wypadek restartów serwera. -""" -from __future__ import annotations -import asyncio -import logging - -from aiomcrcon import Client as _Rcon # Source RCON over TCP - -log = logging.getLogger("rcon") - - -class RconClient: - def __init__(self, host: str, port: int, password: str): - self._host, self._port, self._pw = host, port, password - self._client: _Rcon | None = None - self._lock = asyncio.Lock() - - async def _ensure(self) -> _Rcon: - if self._client is None: - c = _Rcon(self._host, self._port, self._pw) - await c.connect() - self._client = c - log.info("RCON połączony %s:%s", self._host, self._port) - return self._client - - async def command(self, cmd: str) -> str: - """Wyślij komendę do serwera Conana. Zwraca odpowiedź serwera. - - To jest KANAŁ Discord -> Conan. Np. command("broadcast Witajcie!") - wyświetli komunikat wszystkim graczom w grze. - """ - async with self._lock: - for attempt in (1, 2): - try: - client = await self._ensure() - resp, _ = await client.send_cmd(cmd) - return resp - except Exception as e: # rozłączenie/restart serwera - log.warning("RCON błąd (próba %s): %s", attempt, e) - await self.close() - if attempt == 2: - raise - await asyncio.sleep(1.0) - return "" - - async def close(self) -> None: - if self._client is not None: - try: - await self._client.close() - except Exception: - pass - self._client = None diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/main.py b/working_copy/conanjurer/conan-discord-bridge/bot/main.py deleted file mode 100644 index 0420335..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/bot/main.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Punkt wejścia bota mostu Discord <-> Conan Exiles.""" -from __future__ import annotations -import asyncio -import logging - -import discord -from discord.ext import commands - -from bot.core.config import Config -from bot.core.rcon import RconClient - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)s %(name)s: %(message)s", -) -log = logging.getLogger("main") - -COGS = ["bot.cogs.from_conan", "bot.cogs.to_conan"] - - -class BridgeBot(commands.Bot): - def __init__(self, cfg: Config): - intents = discord.Intents.default() - intents.message_content = True # potrzebne do komend prefiksowych - super().__init__(command_prefix="!", intents=intents) - self.cfg = cfg - self.rcon = RconClient(cfg.rcon_host, cfg.rcon_port, cfg.rcon_password) - - async def setup_hook(self): - for c in COGS: - await self.load_extension(c) - log.info("Załadowano cog: %s", c) - - async def close(self): - await self.rcon.close() - await super().close() - - async def on_ready(self): - log.info("Zalogowano jako %s (id=%s)", self.user, self.user.id) - - -def main(): - cfg = Config.load() - bot = BridgeBot(cfg) - bot.run(cfg.token, log_handler=None) - - -if __name__ == "__main__": - main() diff --git a/working_copy/conanjurer/conan-discord-bridge/requirements.txt b/working_copy/conanjurer/conan-discord-bridge/requirements.txt deleted file mode 100644 index f3a7a9e..0000000 --- a/working_copy/conanjurer/conan-discord-bridge/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -discord.py>=2.3,<3.0 -aiomcrcon>=0.2.0 # async Source RCON client -asyncssh>=2.14 # SFTP log pulling (Host Havoc / remote) -python-dotenv>=1.0 # .env loading -tomli>=2.0 ; python_version < "3.11" diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/cogs/to_conan.py b/working_copy/conanjurer_commands.py similarity index 69% rename from working_copy/conanjurer/conan-discord-bridge/bot/cogs/to_conan.py rename to working_copy/conanjurer_commands.py index a93b799..b33965a 100644 --- a/working_copy/conanjurer/conan-discord-bridge/bot/cogs/to_conan.py +++ b/working_copy/conanjurer_commands.py @@ -1,18 +1,14 @@ -"""Cog: komendy Discord -> Conan (przez RCON). - -To jest dowód, że most jest DWUKIERUNKOWY: komenda napisana na Discordzie -wywołuje efekt w grze (np. !say -> broadcast widoczny dla wszystkich graczy). - -Komendy modyfikujące grę są bramkowane rolą GM (GM_ROLE_ID). -""" -from __future__ import annotations +# ai command cogs import logging - import discord from discord.ext import commands +from other_functions import discord_friendly_send, discord_friendly_reply +from __future__ import annotations +import asyncio +import logging -log = logging.getLogger("to_conan") - +from conanjurer_functions import watch, Event +log = logging.getLogger("discord") def is_gm(): async def predicate(ctx: commands.Context) -> bool: @@ -24,6 +20,33 @@ def is_gm(): return commands.check(predicate) +class FromConan(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self._task: asyncio.Task | None = None + + async def cog_load(self): + self._task = asyncio.create_task(self._run()) + + async def cog_unload(self): + if self._task: + self._task.cancel() + + async def _run(self): + await self.bot.wait_until_ready() + cfg = self.bot.cfg + chat_ch = self.bot.get_channel(cfg.chan_chat) + evt_ch = self.bot.get_channel(cfg.chan_events) + + async def on_event(e: Event): + target = chat_ch if e.kind == "chat" else evt_ch + if target is not None: + # allowed_mentions: nie pinguj nikogo treścią z gry + await target.send(e.text, allowed_mentions=discord.AllowedMentions.none()) + + log.info("Start obserwacji logu (tryb=%s)", cfg.log_mode) + await watch(cfg, on_event) + class ToConan(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot @@ -74,3 +97,6 @@ class ToConan(commands.Cog): async def setup(bot: commands.Bot): await bot.add_cog(ToConan(bot)) + await bot.add_cog(FromConan(bot)) + + diff --git a/working_copy/conanjurer/conan-discord-bridge/bot/core/logwatch.py b/working_copy/conanjurer_functions.py similarity index 59% rename from working_copy/conanjurer/conan-discord-bridge/bot/core/logwatch.py rename to working_copy/conanjurer_functions.py index c96f82a..c0b2011 100644 --- a/working_copy/conanjurer/conan-discord-bridge/bot/core/logwatch.py +++ b/working_copy/conanjurer_functions.py @@ -1,23 +1,95 @@ -"""Obserwator logu serwera Conana — KANAŁ Conan -> Discord. - -Dwa tryby: - - local: plik logu jest na tej samej maszynie co bot (self-host) -> klasyczny tail. - - sftp : log pobierany zdalnie (Host Havoc) -> polling przyrostowy po SFTP. - -Parser jest CELOWO ostrożny: formaty linii logu Conana bywają różne między -wersjami i modami (np. Pippi/Tot dokładają własne wpisy). Dlatego dopasowania -są zachowawcze, a wszystko, czego nie rozpoznamy, jest ignorowane (nie zgadujemy). -WERYFIKUJ wzorce na realnym logu swojego serwera i dostrój REGEXY niżej. -""" from __future__ import annotations +import os +from dataclasses import dataclass +from dotenv import load_dotenv import asyncio import logging import re -from dataclasses import dataclass from typing import AsyncIterator, Callable, Awaitable +from aiomcrcon import Client as _Rcon # Source RCON over TCP +import asyncssh -log = logging.getLogger("logwatch") +log = logging.getLogger("discord") +@dataclass(frozen=True) +class Config: + # Discord + guild_id: int + chan_chat: int + chan_events: int + gm_role_id: int + # RCON + rcon_host: str + rcon_port: int + rcon_password: str + # Log + log_mode: str # "local" | "sftp" + log_path: str + sftp_host: str | None + sftp_port: int + sftp_user: str | None + sftp_password: str | None + @staticmethod + def load() -> "Config": + mode = os.getenv("LOG_MODE", "local").lower() + return Config( + guild_id=int(_req("DISCORD_GUILD_ID")), + chan_chat=int(_req("CHAN_CHAT")), + chan_events=int(_req("CHAN_EVENTS")), + gm_role_id=int(_req("GM_ROLE_ID")), + rcon_host=_req("RCON_HOST"), + rcon_port=int(os.getenv("RCON_PORT", "25575")), + rcon_password=_req("RCON_PASSWORD"), + log_mode=mode, + log_path=_req("LOG_PATH"), + sftp_host=os.getenv("SFTP_HOST"), + sftp_port=int(os.getenv("SFTP_PORT", "22")), + sftp_user=os.getenv("SFTP_USER"), + sftp_password=os.getenv("SFTP_PASSWORD"), + ) + + +class RconClient: + def __init__(self, host: str, port: int, password: str): + self._host, self._port, self._pw = host, port, password + self._client: _Rcon | None = None + self._lock = asyncio.Lock() + + async def _ensure(self) -> _Rcon: + if self._client is None: + c = _Rcon(self._host, self._port, self._pw) + await c.connect() + self._client = c + log.info("RCON połączony %s:%s", self._host, self._port) + return self._client + + async def command(self, cmd: str) -> str: + """Wyślij komendę do serwera Conana. Zwraca odpowiedź serwera. + + To jest KANAŁ Discord -> Conan. Np. command("broadcast Witajcie!") + wyświetli komunikat wszystkim graczom w grze. + """ + async with self._lock: + for attempt in (1, 2): + try: + client = await self._ensure() + resp, _ = await client.send_cmd(cmd) + return resp + except Exception as e: # rozłączenie/restart serwera + log.warning("RCON błąd (próba %s): %s", attempt, e) + await self.close() + if attempt == 2: + raise + await asyncio.sleep(1.0) + return "" + + async def close(self) -> None: + if self._client is not None: + try: + await self._client.close() + except Exception: + pass + self._client = None @dataclass class Event: @@ -83,7 +155,6 @@ async def _follow_local(path: str) -> AsyncIterator[str]: async def _follow_sftp(cfg) -> AsyncIterator[str]: """Polling przyrostowy po SFTP (Host Havoc). Czyta tylko nowe bajty.""" - import asyncssh offset = 0 while True: try: @@ -123,3 +194,12 @@ async def watch(cfg, on_event: Callable[[Event], Awaitable[None]]) -> None: await on_event(evt) except Exception: log.exception("Błąd obsługi zdarzenia") + +load_dotenv() + +def _req(key: str) -> str: + val = os.getenv(key) + if not val or val.startswith("wklej") or val.startswith("000000"): + raise RuntimeError(f"Brak/placeholder w .env: {key}") + return val +