1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
"""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"))
|