#!/usr/bin/env python3 """vanguard-stream post-install configuration. Stateless: the config files ARE the state. ./configure.py --full first-time setup: asks every question (current values as defaults) ./configure.py manage: skips what is already set; add/remove/reset streamers & editors ./configure.py --show print current streamers, editors, domain and the OBS settings ./configure.py --hosts AUTH WATCH [GIT [BOT [LIVE]]] set the Authentik/Jellyfin/Gitea/bot/live hostnames and apply (non-interactive; used by addCommunityMediaServer.sh) Reads : Caddyfile (hostnames), mediamtx.yml (streamers), copyparty.conf (editors + admin), .env Writes : Caddyfile, mediamtx.yml, copyparty.conf, dump-users.txt -> /etc/vanguard, .env (generated secrets + Discord bot token/guild), folders, restarts Set VG_DRY=1 to write files only (no sudo / docker / folders). """ import getpass, hashlib, os, re, secrets, shutil, subprocess, sys from pathlib import Path HERE = Path(__file__).resolve().parent os.chdir(HERE) DRY = os.environ.get("VG_DRY") == "1" NAME_RE = re.compile(r"^[a-z0-9_-]{2,32}$") BAD_PW = set(':&?#/ "\'\\\t') # would break the RTMP query string / SRT streamid / YAML # ------------------------------------------------------------------ read current def read_hosts(): """{'main': host, 'auth': host, 'watch': host} from '# vanguard:' markers; first block = main.""" hosts, role = {}, None try: for line in Path("Caddyfile").read_text().splitlines(): if line[:1].isspace(): continue # site blocks start at column 0 t = line.strip() m = re.match(r"#\s*vanguard:(\w+)", t) if m: role = m.group(1); continue if t and not t.startswith(("#", "}", "{")) and t.endswith("{") and " " in t: host = t.split()[0] if "example.com" in host: role = None; continue hosts[role or ("main" if "main" not in hosts else host)] = host role = None except FileNotFoundError: pass return hosts def read_domain(): return read_hosts().get("main", "") def read_env(): env = {} try: for line in Path(".env").read_text().splitlines(): if "=" in line and not line.startswith("#"): k, v = line.split("=", 1); env[k.strip()] = v.strip() except FileNotFoundError: pass return env def default_sub(main, sub): labels = main.split(".") base = ".".join(labels[1:]) if len(labels) >= 3 else main return f"{sub}.{base}" if main else "" def read_streamers(): out, cur = {}, None try: in_users = False for line in Path("mediamtx.yml").read_text().splitlines(): if line.startswith("authInternalUsers:"): in_users = True; continue if in_users and line and not line.startswith(" "): break if in_users: m = re.match(r"\s*-\s*user:\s*(\S+)", line) if m: cur = m.group(1); continue m = re.match(r"\s*pass:\s*(\S+)", line) if m and cur: if not m.group(1).startswith("CHANGEME"): out[cur] = m.group(1) cur = None except FileNotFoundError: pass return out def read_accounts(): out = {} try: in_acc = False for line in Path("copyparty.conf").read_text().splitlines(): if line.strip() == "[accounts]": in_acc = True; continue if in_acc and line.startswith("["): break if in_acc: m = re.match(r"\s*([A-Za-z0-9_-]+):\s*(\S+)", line) if m and not m.group(2).startswith("CHANGEME"): out[m.group(1)] = m.group(2) except FileNotFoundError: pass return out # ------------------------------------------------------------------ prompts def ask(q, default=""): s = input(f"{q}{f' [{default}]' if default else ''}: ").strip() return s or default def yes(q, default=True): d = "Y/n" if default else "y/N" s = input(f"{q} [{d}]: ").strip().lower() return default if not s else s.startswith("y") def gen_pw(): return "".join(c for c in secrets.token_urlsafe(24) if c.isalnum())[:20] def ask_pw(label, current=None): """keep / generate / type. Returns the password.""" opts = "(enter = generate" + (", k = keep current" if current else "") + ", or type one)" while True: prompt = f" password for {label} {opts}: " s = (getpass.getpass(prompt) if sys.stdin.isatty() else input(prompt)).strip() if not s: p = gen_pw(); print(f" generated: {p}"); return p if s == "k" and current: return current if BAD_PW & set(s): print(" avoid : & ? # / \\ quotes and spaces (they break stream keys)"); continue if len(s) < 8: print(" at least 8 characters"); continue return s def ask_name(kind, taken): while True: n = ask(f" {kind} name (blank to stop)").lower() if not n: return None if not NAME_RE.match(n): print(" lowercase letters, digits, _ - (2-32 chars)"); continue if n in taken: print(" already exists"); continue return n # ------------------------------------------------------------------ writers AUTHENTIK_FORWARD_AUTH = """ # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the # whole domain (recommended) or single-application for this host — assigned to the embedded outpost. # Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups. reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000 forward_auth 127.0.0.1:9000 { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version trusted_proxies private_ranges } """ def gate(required_group=""): """Caddy lines: Authentik forward-auth, then deny unless X-Authentik-Groups contains required_group.""" block = AUTHENTIK_FORWARD_AUTH if required_group: g = re.escape(required_group) block += f""" @lacks_group not header_regexp X-Authentik-Groups (^|\\|){g}(\\||$) handle @lacks_group {{ respond "This page is for members of the '{required_group}' group." 403 }} """ return block def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): env = read_env() extra = "" if live_host: extra += f""" # vanguard:live {live_host} {{ encode zstd gzip {gate(env.get("LIVE_REQUIRE_GROUP", ""))} handle /me/* {{ reverse_proxy 127.0.0.1:8080 # dumpd: /me/stream-key for the logged-in streamer (Go Live) }} handle_path /api/* {{ reverse_proxy 127.0.0.1:9997 }} handle_path /webrtc/* {{ reverse_proxy 127.0.0.1:8889 }} handle_path /hls/* {{ reverse_proxy 127.0.0.1:8888 }} handle {{ root * /srv/live try_files {{path}} /index.html file_server }} }} """ if bot_host: extra += f""" # vanguard:bot {bot_host} {{ encode zstd gzip {gate(env.get("BOT_REQUIRE_GROUP", "bot-users"))} reverse_proxy 127.0.0.1:8000 }} """ if git_host: extra += f""" # vanguard:git {git_host} {{ encode zstd gzip reverse_proxy 127.0.0.1:3000 }} """ if auth_host: extra += f""" # vanguard:auth {auth_host} {{ encode zstd gzip reverse_proxy 127.0.0.1:9000 }} """ if watch_host: extra += f""" # vanguard:watch {watch_host} {{ encode zstd gzip reverse_proxy 127.0.0.1:8096 }} """ Path("Caddyfile").write_text(f"""# vanguard:main {domain} {{ encode zstd gzip # dump endpoint (python service on the host, does its own auth) handle /dump* {{ reverse_proxy 127.0.0.1:8080 }} # Syncthing admin UI (its own login; strip the prefix, present a local Host to pass its host check) redir /sync /sync/ 308 handle_path /sync/* {{ reverse_proxy 127.0.0.1:8384 {{ header_up Host {{upstream_hostport}} }} }} # OBS browser dock (static) handle_path /dock* {{ root * /srv/dock file_server }} # everything else is copyparty (web UI + WebDAV) handle {{ reverse_proxy 127.0.0.1:3923 {{ flush_interval -1 }} }} }} """ + extra) def write_env(watch_host, discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", main_host="", live_webhook=None): env = read_env() env["LIVE_HOST"] = live_host or env.get("LIVE_HOST", "live.example.com") if main_host: env["MAIN_HOST"] = main_host if live_webhook is not None: env["LIVE_WEBHOOK_URL"] = live_webhook env.setdefault("LIVE_WEBHOOK_URL", "") env.setdefault("BOT_REQUIRE_GROUP", "bot-users") # Caddy 403s anyone not in this Authentik group; empty = any logged-in member env.setdefault("LIVE_REQUIRE_GROUP", "") env["GIT_HOST"] = git_host or env.get("GIT_HOST", "git.example.com") env["BOT_HOST"] = bot_host or env.get("BOT_HOST", "bot.example.com") env.setdefault("GITEA_DB_PASSWORD", "".join(c for c in secrets.token_urlsafe(40) if c.isalnum())[:32]) env.setdefault("GITEA_RUNNER_TOKEN", "") # filled by ./registerGiteaRunner.sh if discord_token is not None: env["DISCORD_BOT_TOKEN"] = discord_token if discord_guild is not None: env["DISCORD_GUILD_ID"] = discord_guild env.setdefault("AUTHENTIK_SECRET_KEY", secrets.token_urlsafe(48)) env.setdefault("AUTHENTIK_PG_PASS", "".join(c for c in secrets.token_urlsafe(40) if c.isalnum())[:32]) env.setdefault("AUTHENTIK_TAG", "2025.8") env.setdefault("MARIADB_ROOT_PASSWORD", "".join(c for c in secrets.token_urlsafe(40) if c.isalnum())[:32]) env.setdefault("BOT_DB_NAME", "vanguardbot") env.setdefault("BOT_DB_USER", "bot") env.setdefault("BOT_DB_PASSWORD", "".join(c for c in secrets.token_urlsafe(40) if c.isalnum())[:32]) env.setdefault("DISCORD_BOT_TOKEN", "") # fill in: Discord dev portal -> the Authentik app -> Bot env.setdefault("DISCORD_GUILD_ID", "") env["WATCH_HOST"] = watch_host or "watch.example.com" Path(".env").write_text("".join(f"{k}={v}\n" for k, v in env.items())) os.chmod(".env", 0o600) def write_mediamtx(streamers): users = "".join(f""" - user: {u} pass: {p} permissions: - action: publish path: ~^{u}/[A-Za-z0-9_.-]+$ """ for u, p in streamers.items()) or " []\n" alt = "|".join(streamers) or "nobody" Path("mediamtx.yml").write_text(f"""# MediaMTX — ingest only. No playback endpoints exposed. (generated by configure.py) logLevel: info rtsp: no metrics: no pprof: no # live portal (served to the browser only via Caddy + Authentik on live.HOST; these ports are published on 127.0.0.1) api: yes apiAddress: :9997 hls: yes hlsAddress: :8888 hlsVariant: lowLatency hlsAlwaysRemux: no webrtc: yes webrtcAddress: :8889 webrtcLocalUDPAddress: :8189 webrtcLocalTCPAddress: :8189 webrtcIPsFromInterfaces: no # webrtcAdditionalHosts is set via env MTX_WEBRTCADDITIONALHOSTS (= the public hostname) in docker-compose.yml rtmp: yes rtmpAddress: :1935 srt: yes srtAddress: :8890 # ---- auth ------------------------------------------------------------- # Each streamer may publish only under their own prefix. # RTMP key: /Title?user=&pass= # SRT: srt://HOST:8890?streamid=publish:/Title:: authMethod: internal authInternalUsers: - user: any permissions: - action: read - action: playback - action: api {users} # ---- recording -------------------------------------------------------- pathDefaults: record: yes recordPath: /recordings/%path/.live/%Y-%m-%d_%H-%M-%S # hidden while recording; the sweep publishes the finished file one level up recordFormat: fmp4 recordPartDuration: 1s recordSegmentDuration: 12h # one file per stream unless it runs >12h recordDeleteAfter: 0s # never auto-delete; the archive job handles it # optional Discord "is live" notice (LIVE_WEBHOOK_URL set in .env). busybox wget in the -ffmpeg image. runOnReady: > sh -c '[ -n "$LIVE_WEBHOOK_URL" ] || exit 0; u="${{MTX_PATH%%/*}}"; t="${{MTX_PATH#*/}}"; wget -q -O /dev/null --header="Content-Type: application/json" --post-data="{{\\"content\\":\\"🔴 **$u** is live: $t — https://$LIVE_HOST/\\"}}" "$LIVE_WEBHOOK_URL"' runOnReadyRestart: no paths: # any name under a streamer's prefix; inherits pathDefaults ~^({alt})/[A-Za-z0-9_.-]+$: {{}} """) def write_copyparty(admin_pw, editors): acc = f" admin: {admin_pw}\n" + "".join(f" {u}: {p}\n" for u, p in editors.items()) eds = ", ".join(editors) r_line = f" r: {eds}\n" if eds else "" rw_line = f" rw: {eds}\n" if eds else "" Path("copyparty.conf").write_text(f"""# copyparty — editors browse/preview/download; finals are writable. (generated by configure.py) # Docs: https://github.com/9001/copyparty#server-config [global] e2dsa # index files for search e2ts # read media tags th-ff-jpg # video thumbnails via ffmpeg (cached under /cfg/hist) hist: /cfg/hist rproxy: 1 # trust first X-Forwarded-For hop (Caddy) xff-src: 172.16.0.0/12, 10.0.0.0/8, 192.168.0.0/16, 127.0.0.0/8 no-robots name: vanguard usernames # login needs username + password (default is password-only) # dark skin for the login page (served by Caddy from ./dock) html-head: [accounts] {acc} # streams: read for editors, full for admin [/streams] /w/streams accs: {r_line} rwmd: admin # gamer clips (Syncthing drops them here) [/clips] /w/clips accs: {r_line} rwmd: admin # editors upload finished work here [/finals] /w/finals accs: {rw_line} rwmd: admin # dumped nights, purged after 3 days — admin only [/trash] /w/trash accs: rwmd: admin """) def write_streamers_json(streamers): import json as _json Path("streamers.json").write_text(_json.dumps({u.lower(): p for u, p in streamers.items()}, indent=2) + "\n") def write_dump_users(admin_pw, streamers): h = lambda p: hashlib.sha256(p.encode()).hexdigest() body = "# user:sha256hex(password) (generated by configure.py)\n" body += "".join(f"{u}:{h(p)}\n" for u, p in streamers.items()) body += f"admin:{h(admin_pw)}\n" Path("dump-users.txt").write_text(body) def run(cmd, **kw): print(" $", " ".join(cmd)) if DRY: return 0 return subprocess.call(cmd, **kw) def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", discord_token=None, discord_guild=None, git_host="", bot_host="", live_host="", live_webhook=None): print("\n== writing configs") write_env(watch_host, discord_token, discord_guild, git_host, bot_host, live_host, domain, live_webhook) write_caddy(domain, auth_host, watch_host, git_host, bot_host, live_host); write_mediamtx(streamers); write_copyparty(admin_pw, editors); write_dump_users(admin_pw, streamers); write_streamers_json(streamers) run(["sudo", "install", "-d", "-o", os.environ.get("SUDO_USER") or os.environ.get("USER") or "debian", "/srv/live"]) print("== app data folders") run(["sudo", "install", "-d", "-o", "1000", "-g", "1000", "/srv/appdata/jellyfin/config", "/srv/appdata/jellyfin/cache", "/srv/appdata/authentik/media", "/srv/appdata/authentik/templates", "/srv/appdata/authentik/certs"]) run(["sudo", "install", "-d", "/srv/appdata/authentik/db", "/srv/appdata/authentik/redis", "/srv/appdata/mariadb"]) run(["sudo", "install", "-d", "-o", "1000", "-g", "1000", "/srv/appdata/discord-bot", "/srv/appdata/gitea", "/srv/appdata/act_runner"]) print("== folders for streamers") for u in streamers: run(["sudo", "install", "-d", "-o", "1000", "-g", "1000", f"/srv/media/streams/{u}", f"/srv/media/clips/{u}"]) if not DRY and not Path(f"/srv/media/clips/{u}/.stignore").exists(): run(["sudo", "install", "-m", "0644", "-o", "1000", "-g", "1000", "syncthing/stignore", f"/srv/media/clips/{u}/.stignore"]) print("== dump users + restart dumpd") run(["sudo", "install", "-d", "-m", "0750", "/etc/vanguard"]) run(["sudo", "install", "-m", "0640", "dump-users.txt", "/etc/vanguard/dump-users.txt"]) run(["sudo", "install", "-m", "0600", "streamers.json", "/etc/vanguard/streamers.json"]) run(["sudo", "systemctl", "restart", "dumpd"]) print("== containers") dc = ["docker", "compose"] if (DRY or subprocess.call(["docker", "info"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0) else ["sudo", "docker", "compose"] run(dc + ["up", "-d", "--build"]) run(dc + ["restart", "mediamtx", "copyparty", "caddy"]) def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): print(f"\n== domain : {domain or '(not set)'}") print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)") print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)") print(f"== gitea : https://{git_host}/ ssh port 2222 (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)") print(f"== authentik: https://{auth_host}/ (first run: https://{auth_host}/if/flow/initial-setup/)" if auth_host else "== authentik: (not set — run --full)") print(f"== jellyfin : https://{watch_host}/" if watch_host else "== jellyfin : (not set — run --full)") env = read_env() tok = env.get("DISCORD_BOT_TOKEN", "") print(f"== discord : guild {env.get('DISCORD_GUILD_ID') or '(not set)'}; bot token {'set (' + tok[:6] + '…)' if tok else '(not set)'}") print(f"== mariadb : {domain or 'HOST'}:3306 (public, root local-only) root pass {env.get('MARIADB_ROOT_PASSWORD','(run apply)')}") print(f" bot : db {env.get('BOT_DB_NAME','vanguardbot')} user {env.get('BOT_DB_USER','bot')} pass {env.get('BOT_DB_PASSWORD','(run apply)')}") print(f" gitea : db gitea user gitea pass {env.get('GITEA_DB_PASSWORD','(run apply)')} (created on MariaDB first init, else ./createGiteaDb.sh)") print(f"== admin : admin / {admin_pw or '(not set)'} (copyparty + dump)") print("== editors :", ", ".join(f"{u} / {p}" for u, p in editors.items()) or "(none)") print("== streamers:") for u, p in streamers.items(): print(f"\n {u} password {p}") print(f" OBS server : rtmp://{domain or 'HOST'}:1935/") print(f" OBS key : {u}/TITLE?user={u}&pass={p} (the dock sets this for them)") print(f" SRT : srt://{domain or 'HOST'}:8890?streamid=publish:{u}/TITLE:{u}:{p}") print(f" Dock URL : https://{domain or 'HOST'}/dock/ (settings: host={domain or 'HOST'}, user={u}, pass={p})") if not streamers: print(" (none)") print(f"\n editors log in at https://{domain or 'HOST'}/ — WebDAV at the same URL") print(f" verify: curl -sI https://{domain or 'HOST'}/dock/ | head -1 (expect 200)") print(f" curl -s -o /dev/null -w '%{{http_code}}\\n' https://{domain or 'HOST'}/dump (expect 401)\n") # ------------------------------------------------------------------ modes def manage_users(kind, users): """add / remove / reset loop for one kind of user; mutates dict.""" while True: print(f"\n {kind}s: {', '.join(users) or '(none)'}") c = ask(" [a]dd [r]emove [p]assword reset [d]one", "d").lower()[:1] if c == "d": return if c == "a": n = ask_name(kind, users) if n: users[n] = ask_pw(n) elif c in "rp": n = ask(f" which {kind}").lower() if n not in users: print(" no such user"); continue if c == "r": if yes(f" remove {n}? (their files stay on disk)", False): del users[n] else: users[n] = ask_pw(n, users[n]) def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): print("== FULL SETUP (enter keeps the shown default)\n") while True: domain = ask("public hostname (DNS A record must point here)", domain) if domain and "." in domain and " " not in domain: break print(" need a hostname like media.example.com") auth_host = ask("Authentik hostname (CNAME to the above)", auth_host or default_sub(domain, "auth")) watch_host = ask("Jellyfin hostname (CNAME to the above)", watch_host or default_sub(domain, "watch")) git_host = ask("Gitea hostname (CNAME to the above)", git_host or default_sub(domain, "git")) bot_host = ask("Bot web UI hostname (CNAME to the above)", bot_host or default_sub(domain, "bot")) live_host = ask("Live portal hostname (CNAME to the above)", live_host or default_sub(domain, "live")) print("\n-- Discord bot (optional; the Bot added to the Authentik Discord application). Enter keeps the current value.") discord_token, discord_guild = ask_discord() live_webhook = ask(" Discord webhook URL for 'is live' notices (optional)", read_env().get("LIVE_WEBHOOK_URL", "")) print("\n-- admin (copyparty admin + may dump anyone's night)") admin_pw = ask_pw("admin", admin_pw or None) print("\n-- streamers (OBS publishers; each gets streams/ and clips/)") for u in list(streamers): k = ask(f" {u}: [k]eep [r]emove [p]assword reset", "k").lower()[:1] if k == "r": del streamers[u] elif k == "p": streamers[u] = ask_pw(u, streamers[u]) print(" add streamers:") while True: n = ask_name("streamer", streamers) if not n: break streamers[n] = ask_pw(n) print("\n-- editors (copyparty logins: read streams/clips, write finals)") for u in list(editors): k = ask(f" {u}: [k]eep [r]emove [p]assword reset", "k").lower()[:1] if k == "r": del editors[u] elif k == "p": editors[u] = ask_pw(u, editors[u]) print(" add editors:") while True: n = ask_name("editor", editors) if not n: break editors[n] = ask_pw(n) return domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook def ask_discord(): env = read_env() cur_tok, cur_guild = env.get("DISCORD_BOT_TOKEN", ""), env.get("DISCORD_GUILD_ID", "") prompt = f" bot token{' [keep ' + cur_tok[:6] + '…]' if cur_tok else ' [none]'}: " tok = (getpass.getpass(prompt) if sys.stdin.isatty() else input(prompt)).strip() or cur_tok guild = ask(" server (guild) ID", cur_guild) return tok, guild def normal(domain, admin_pw, streamers, editors): missing = [x for x, v in (("domain", domain), ("admin password", admin_pw)) if not v] if missing: print(f"not set yet: {', '.join(missing)} — run ./configure.py --full"); sys.exit(1) print(f"== MANAGE (domain {domain}; run --full to change hostnames/admin)") discord = (None, None) while True: c = ask("\n[s]treamers [e]ditors [d]iscord bot token/guild [v]iew [a]pply & quit [q]uit without applying", "a").lower()[:1] if c == "s": manage_users("streamer", streamers) elif c == "e": manage_users("editor", editors) elif c == "d": discord = ask_discord() elif c == "v": show(domain, admin_pw, streamers, editors) elif c == "a": return discord elif c == "q": return False def main(): mode = sys.argv[1] if len(sys.argv) > 1 else "" hosts = read_hosts() domain, auth_host, watch_host, git_host, bot_host, live_host = hosts.get("main", ""), hosts.get("auth", ""), hosts.get("watch", ""), hosts.get("git", ""), hosts.get("bot", ""), hosts.get("live", "") streamers, accounts = read_streamers(), read_accounts() admin_pw = accounts.pop("admin", "") editors = accounts if mode == "--show": show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host); return if mode == "--hosts": if len(sys.argv) not in (4, 5, 6, 7): print("usage: configure.py --hosts AUTH_HOST WATCH_HOST [GIT_HOST [BOT_HOST [LIVE_HOST]]]"); sys.exit(2) if not domain or not admin_pw: print("domain/admin not set yet — run ./configure.py --full first"); sys.exit(1) auth_host, watch_host = sys.argv[2].strip(), sys.argv[3].strip() if len(sys.argv) >= 5: git_host = sys.argv[4].strip() if len(sys.argv) >= 6: bot_host = sys.argv[5].strip() if len(sys.argv) == 7: live_host = sys.argv[6].strip() apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host, live_host=live_host) show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host); return discord_token = discord_guild = live_webhook = None if mode == "--full": domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook = full(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host) if not yes("\napply now (write configs, restart services)?"): print("nothing written"); return elif mode == "": r = normal(domain, admin_pw, streamers, editors) if r is False: print("nothing written"); return discord_token, discord_guild = r else: print(__doc__); sys.exit(2) apply(domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook) show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host, live_host) if __name__ == "__main__": try: main() except (KeyboardInterrupt, EOFError): print("\naborted, nothing written")