Vanguard media stack: ingest, copyparty, Syncthing, normalize sweep, Authentik, Jellyfin + Vanguard Metadata plugin, MariaDB, Gitea, bot runtime
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
487
configure.py
Executable file
487
configure.py
Executable file
@@ -0,0 +1,487 @@
|
||||
#!/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]] set the Authentik/Jellyfin/Gitea/bot 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:<role>' 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 (forward auth, single application)
|
||||
# for this external host in Authentik, assigned to the embedded outpost. Until then: 4xx, never open.
|
||||
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 write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host=""):
|
||||
extra = ""
|
||||
if bot_host:
|
||||
extra += f"""
|
||||
# vanguard:bot
|
||||
{bot_host} {{
|
||||
encode zstd gzip
|
||||
{AUTHENTIK_FORWARD_AUTH} 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=""):
|
||||
env = read_env()
|
||||
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
|
||||
webrtc: no
|
||||
hls: no
|
||||
api: no
|
||||
metrics: no
|
||||
pprof: no
|
||||
|
||||
rtmp: yes
|
||||
rtmpAddress: :1935
|
||||
|
||||
srt: yes
|
||||
srtAddress: :8890
|
||||
|
||||
# ---- auth -------------------------------------------------------------
|
||||
# Each streamer may publish only under their own prefix.
|
||||
# RTMP key: <user>/Title?user=<user>&pass=<pass>
|
||||
# SRT: srt://HOST:8890?streamid=publish:<user>/Title:<user>:<pass>
|
||||
authMethod: internal
|
||||
authInternalUsers:
|
||||
{users}
|
||||
# ---- recording --------------------------------------------------------
|
||||
pathDefaults:
|
||||
record: yes
|
||||
recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S
|
||||
recordFormat: fmp4
|
||||
recordPartDuration: 1s
|
||||
recordSegmentDuration: 12h # one file per stream unless it runs >12h
|
||||
recordDeleteAfter: 0s # never auto-delete; the archive job handles it
|
||||
|
||||
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: <link rel="stylesheet" href="/dock/vanguard.css">
|
||||
|
||||
[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_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=""):
|
||||
print("\n== writing configs")
|
||||
write_caddy(domain, auth_host, watch_host, git_host, bot_host); write_mediamtx(streamers); write_copyparty(admin_pw, editors); write_dump_users(admin_pw, streamers)
|
||||
write_env(watch_host, discord_token, discord_guild, git_host, bot_host)
|
||||
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", "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=""):
|
||||
print(f"\n== domain : {domain or '(not set)'}")
|
||||
print(f"== bot web : https://{bot_host}/ (behind Authentik forward-auth; create the Proxy Provider for it)" 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=""):
|
||||
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"))
|
||||
print("\n-- Discord bot (optional; the Bot added to the Authentik Discord application). Enter keeps the current value.")
|
||||
discord_token, discord_guild = ask_discord()
|
||||
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/<name> and clips/<name>)")
|
||||
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
|
||||
|
||||
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 = hosts.get("main", ""), hosts.get("auth", ""), hosts.get("watch", ""), hosts.get("git", ""), hosts.get("bot", "")
|
||||
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); return
|
||||
if mode == "--hosts":
|
||||
if len(sys.argv) not in (4, 5, 6): print("usage: configure.py --hosts AUTH_HOST WATCH_HOST [GIT_HOST [BOT_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()
|
||||
apply(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host=git_host, bot_host=bot_host)
|
||||
show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host); return
|
||||
discord_token = discord_guild = None
|
||||
if mode == "--full":
|
||||
domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host = full(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_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)
|
||||
show(domain, admin_pw, streamers, editors, auth_host, watch_host, git_host, bot_host)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try: main()
|
||||
except (KeyboardInterrupt, EOFError): print("\naborted, nothing written")
|
||||
Reference in New Issue
Block a user