"""TeamSpeak Web Explorer configuration. Settings live in ``config.toml`` (shared with the Node voice gateway in ``gateway/``) and secrets live in ``.env``. Environment variables override the TOML values, using the same names the gateway reads (see ``ENV_OVERRIDES``). """ import os import tomllib from pathlib import Path from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent CONFIG_FILE = Path(os.environ.get("TS6_CONFIG_FILE") or BASE_DIR / "config.toml") load_dotenv(BASE_DIR / ".env") # (section, key, ENV_VAR, type). Keep in sync with gateway/src/config.js. ENV_OVERRIDES = [ ("web", "host", "WEB_HOST", str), ("web", "port", "WEB_PORT", int), ("web", "dashboard_port", "DASHBOARD_PORT", int), ("server", "host", "TS_HOST", str), ("server", "webquery_port", "TS_WEBQUERY_PORT", int), ("server", "server_id", "TS_SERVER_ID", str), ("server", "voice_port", "TS_VOICE_PORT", int), ("server", "voice_port", "TS_PORT", int), # legacy name from the old .env.template ("server", "ts_domain", "TS_DOMAIN", str), ("voice", "enabled", "VOICE_ENABLED", bool), ("voice", "trust_proxy", "VOICE_TRUST_PROXY", bool), ] def _env_value(raw, kind): if kind is bool: return raw.strip().lower() in {"1", "true", "yes", "on"} return kind(raw.strip()) def _load_toml(): try: with CONFIG_FILE.open("rb") as fh: return tomllib.load(fh) except FileNotFoundError: print(f"[config] {CONFIG_FILE} not found, using defaults") return {} def _build_config(): raw = _load_toml() sections = { name: dict(raw.get(name) or {}) for name in ( "web", "server", "voice", "ui", "buttons", "external_links", "footer", "channel_banners", "channel_icons", "generic_images", "role_icons", ) } applied = set() for section, key, env_name, kind in ENV_OVERRIDES: value = os.environ.get(env_name) if value is None or value.strip() == "" or (section, key) in applied: continue try: sections[section][key] = _env_value(value, kind) applied.add((section, key)) except ValueError: print(f"[config] ignoring invalid {env_name}={value!r}") web = sections["web"] web.setdefault("host", "0.0.0.0") web.setdefault("port", 5000) web.setdefault("dashboard_port", 5001) server = sections["server"] server.setdefault("host", "") server["port"] = server.get("webquery_port", 8443) # name used by the WebQuery fetcher server["server_id"] = str(server.get("server_id", 1)) server.setdefault("voice_port", 9987) server["ts_domain"] = server.get("ts_domain") or server["host"] server.setdefault("cache_time", 30) server["cache_file"] = BASE_DIR / server.get("cache_file", "cache.json") server.setdefault("afk_status_max_clients", 3) server["api_key"] = os.environ.get("TS_API_KEY", "") voice = sections["voice"] voice.setdefault("enabled", True) ui = sections["ui"] for key, default in { "title": "", "subtitle": "", "background_color": "#07111f", "panel_color": "#101b2d", "accent_color": "#2589ff", "text_color": "#e8f2ff", "banner_url": "", "enable_stars": False, }.items(): ui.setdefault(key, default) buttons = sections["buttons"] for key, default in { "join": "Connect in TeamSpeak", "web_join": "Join from browser", "download_text": "Teamspeak is not installed yet?", "download_link": "Download Teamspeak", "download_url": "https://www.teamspeak.com/en/downloads/", }.items(): buttons.setdefault(key, default) footer = sections["footer"] for key in ("text", "author", "author_url"): footer.setdefault(key, "") generic = sections["generic_images"] generic.setdefault("background", "") generic.setdefault("icon", "") sections["role_icons"] = {str(k): v for k, v in sections["role_icons"].items()} return sections CONFIG = _build_config() # Web voice is only offered when enabled AND a server password is configured: # the gateway refuses every join without one, so nobody can join without it. # The password itself is intentionally kept out of CONFIG (which templates see). VOICE_AVAILABLE = bool(CONFIG["voice"].get("enabled")) and bool(os.environ.get("TS_SERVER_PASSWORD"))