"""Sync Telegram credentials for Layer-3 host patrol (health_monitor.sh).""" from __future__ import annotations import logging import os import shutil from pathlib import Path from server.infrastructure.env_file import read_env_var, upsert_env_vars logger = logging.getLogger("nexus.ops_patrol") NEXUS_ROOT = Path(__file__).resolve().parents[3] ENV_FILE = NEXUS_ROOT / ".env" _TELEGRAM_ENV_KEYS = ( "NEXUS_TELEGRAM_BOT_TOKEN", "NEXUS_TELEGRAM_CHAT_ID", ) def _persist_env_path() -> Path: raw = os.environ.get("NEXUS_PERSIST_DIR", "/var/lib/nexus").strip() or "/var/lib/nexus" return Path(raw) / ".env" def _host_env_prod_path() -> Path | None: host_root = os.environ.get("NEXUS_HOST_ROOT", "").strip() if not host_root: return None return Path(host_root) / "docker" / ".env.prod" def _build_updates(token: str | None, chat_id: str | None) -> dict[str, str]: updates: dict[str, str] = {} if token and token.strip(): updates["NEXUS_TELEGRAM_BOT_TOKEN"] = token.strip() if chat_id and str(chat_id).strip(): updates["NEXUS_TELEGRAM_CHAT_ID"] = str(chat_id).strip() return updates def _sync_to_persist_volume() -> bool: if not ENV_FILE.is_file(): return False dest = _persist_env_path() try: dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(ENV_FILE, dest) dest.chmod(0o600) return True except OSError as exc: logger.warning("Failed to sync .env to persist volume %s: %s", dest, exc) return False def read_telegram_env_status() -> dict[str, bool]: """Report whether Layer-3 readable env files contain Telegram credentials.""" app_token = bool(read_env_var(ENV_FILE, "NEXUS_TELEGRAM_BOT_TOKEN")) app_chat = bool(read_env_var(ENV_FILE, "NEXUS_TELEGRAM_CHAT_ID")) host_token = False host_chat = False host_path = _host_env_prod_path() if host_path is not None: host_token = bool(read_env_var(host_path, "NEXUS_TELEGRAM_BOT_TOKEN")) host_chat = bool(read_env_var(host_path, "NEXUS_TELEGRAM_CHAT_ID")) env_synced = (app_token and app_chat) or (host_token and host_chat) return { "app_env_token": app_token, "app_env_chat_id": app_chat, "host_env_prod_token": host_token, "host_env_prod_chat_id": host_chat, "env_synced": env_synced, } def sync_telegram_to_env(token: str | None, chat_id: str | None) -> dict[str, bool | str]: """Write Telegram credentials to /app/.env, persist volume, and host .env.prod.""" updates = _build_updates(token, chat_id) if not updates: return { "app_env": False, "persist_env": False, "host_env_prod": False, "skipped": "telegram_not_configured", } result: dict[str, bool | str] = { "app_env": False, "persist_env": False, "host_env_prod": False, } try: if upsert_env_vars(ENV_FILE, updates): ENV_FILE.chmod(0o600) result["app_env"] = True result["persist_env"] = _sync_to_persist_volume() except OSError as exc: logger.warning("Failed to update %s: %s", ENV_FILE, exc) host_path = _host_env_prod_path() if host_path is not None: try: if host_path.is_file() and upsert_env_vars(host_path, updates): host_path.chmod(0o600) result["host_env_prod"] = True except OSError as exc: logger.warning("Failed to update host .env.prod %s: %s", host_path, exc) return result