#!/usr/bin/env python3 """Finish ntfy auth, Umami sites, tracker injection, rebuilds.""" from __future__ import annotations import json import os import secrets import string import time from pathlib import Path import paramiko HOST = "107.173.30.245" PASS = os.environ.get("NOMADRO_SSH_PASS", "Xiao4669805") REPO = Path(__file__).resolve().parents[2] CONF = "/etc/nomadro" NTFY_DIR = "/opt/ntfy" SITES = { "nomadro.com": "Nomadro (blog + app)", "portal.nomadro.com": "Nomadro portal", "dylive.nomadro.com": "DouyinWEB / dylive", "nomadro.live": "TikTok Promo / nomadro.live", "wx.nomadro.com": "HackRobot / wx", "x.nomadro.com": "XAuto / x", } NEXT_LAYOUTS = [ ("/var/www/nomadro.com/app/layout.tsx", "nomadro.com"), ("/opt/douyinWEB/frontend/src/app/layout.tsx", "dylive.nomadro.com"), ("/opt/tiktokWEB/frontend/src/app/layout.tsx", "nomadro.live"), ("/opt/wauto/web/app/layout.tsx", "wx.nomadro.com"), ] def gen(n=20): return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(n)) def run(c, cmd, timeout=300): print("$", cmd[:140]) _i, o, e = c.exec_command(cmd, timeout=timeout) # channel timeout chan = _i.channel if hasattr(_i, "channel") else o.channel chan.settimeout(timeout) out = (o.read() + e.read()).decode("utf-8", errors="replace") if out.strip(): print(out[-3000:] if len(out) > 3000 else out) return out def main(): c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(HOST, username="root", password=PASS, timeout=30, allow_agent=False, look_for_keys=False) sftp = c.open_sftp() # Load or create umami admin password try: with sftp.file(f"{CONF}/umami.env", "r") as f: umami_env = f.read().decode() except OSError: umami_env = "" umami_pass = None for line in umami_env.splitlines(): if line.startswith("UMAMI_PASSWORD="): umami_pass = line.split("=", 1)[1].strip() if not umami_pass: umami_pass = gen() with sftp.file(f"{CONF}/umami.env", "w") as f: f.write( f"UMAMI_URL=https://umami.nomadro.com\nUMAMI_USER=admin\nUMAMI_PASSWORD={umami_pass}\n" ) ntfy_pass = gen() # ntfy user + token — use expect-like via script file to avoid hang run( c, f""" set -e cd {NTFY_DIR} # Remove incomplete user if any docker compose exec -T ntfy ntfy user del admin 2>/dev/null || true # Non-interactive add: ntfy reads password from NTFY_PASSWORD env in recent versions docker compose exec -T -e NTFY_PASSWORD='{ntfy_pass}' ntfy ntfy user add --role=admin admin 2>/tmp/ntfy_user_err || true if ! docker compose exec -T ntfy ntfy user list 2>/dev/null | grep -q admin; then # fallback: hash + config — use `ntfy user hash` then add with password hash if available HASH=$(docker compose exec -T ntfy ntfy hash '{ntfy_pass}' 2>/dev/null | tr -d '\\r' || true) echo "hash=$HASH" # try password flag variants docker compose exec -T ntfy ntfy user add admin --role=admin --password '{ntfy_pass}' 2>/tmp/ntfy_err2 || true fi docker compose exec -T ntfy ntfy user list || true docker compose exec -T ntfy ntfy access admin server-alerts rw || docker compose exec -T ntfy ntfy access admin server-alerts read-write || true docker compose exec -T ntfy ntfy access admin deploy rw || docker compose exec -T ntfy ntfy access admin deploy read-write || true # Create token TOKEN_OUT=$(docker compose exec -T ntfy ntfy token add admin 2>&1 | tr -d '\\r') echo "TOKEN_OUT=$TOKEN_OUT" TOKEN=$(echo "$TOKEN_OUT" | grep -Eo 'tk_[A-Za-z0-9]+' | tail -1) if [ -z "$TOKEN" ]; then TOKEN=$(echo "$TOKEN_OUT" | awk 'NF{{print $NF}}' | tail -1) fi echo "TOKEN=$TOKEN" cat > {CONF}/ntfy.env </etc/cron.d/nomadro-health <<'EOF' */5 * * * * root /usr/local/bin/nomadro-healthcheck >/dev/null 2>&1 EOF """, ) # Wait public umami for _ in range(40): out = run(c, "curl -fsS -o /dev/null -w '%{http_code}' https://umami.nomadro.com/api/heartbeat || echo x") if "200" in out: break time.sleep(3) sites_json = json.dumps(SITES) out = run( c, f""" python3 - <<'PY' import json, urllib.request, urllib.error admin_pass = {umami_pass!r} base = "https://umami.nomadro.com" sites = json.loads({sites_json!r}) def req(url, method="GET", data=None, token=None): body = None if data is None else json.dumps(data).encode() headers = {{"Content-Type":"application/json","Accept":"application/json"}} if token: headers["Authorization"] = f"Bearer {{token}}" r = urllib.request.Request(url, data=body, headers=headers, method=method) try: with urllib.request.urlopen(r, timeout=45) as resp: raw = resp.read().decode() return resp.status, json.loads(raw) if raw else {{}} except urllib.error.HTTPError as e: raw = e.read().decode(errors="replace") try: return e.code, json.loads(raw) if raw else {{}} except Exception: return e.code, {{"raw": raw}} code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":admin_pass}}) if code != 200: code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":"umami"}}) if code != 200: raise SystemExit(f"login failed {{code}} {{login}}") token = login["token"]; uid = login["user"]["id"] code2, upd = req(base+f"/api/users/{{uid}}","POST",{{"password":admin_pass}}, token=token) print("password update", code2, upd) code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":admin_pass}}) if code != 200: raise SystemExit(f"re-login failed {{code}} {{login}}") token = login["token"] code, listing = req(base+"/api/websites", token=token) items = listing if isinstance(listing, list) else (listing.get("data") or listing.get("websites") or []) existing = {{w["domain"]: w["id"] for w in items if isinstance(w, dict) and w.get("domain")}} result = {{}} for domain, name in sites.items(): if domain in existing: result[domain] = existing[domain] continue code, created = req(base+"/api/websites","POST",{{"name":name,"domain":domain}}, token=token) print("create", domain, code, created) if code not in (200,201) or "id" not in created: raise SystemExit(f"create failed {{domain}}") result[domain] = created["id"] with open("{CONF}/umami-sites.env","w") as f: for d, wid in result.items(): key = d.replace(".","_").replace("-","_").upper() f.write(f"UMAMI_ID_{{key}}={{wid}}\\n") f.write("UMAMI_SCRIPT=https://umami.nomadro.com/script.js\\n") print("SITE_IDS="+json.dumps(result)) PY """, timeout=120, ) env_map = {} for line in run(c, f"cat {CONF}/umami-sites.env").splitlines(): if line.startswith("UMAMI_ID_") and "=" in line: k, v = line.split("=", 1) env_map[k] = v.strip() def id_for(domain: str) -> str: key = "UMAMI_ID_" + domain.replace(".", "_").replace("-", "_").upper() return env_map[key] wid_nomadro = id_for("nomadro.com") mu = (REPO / "deploy/wordpress/mu-plugins/nomadro-adsense.php").read_text(encoding="utf-8") # Only replace the const assignment so comparison/placeholder logic stays intact mu = mu.replace( "const NOMADRO_UMAMI_WEBSITE_ID = 'REPLACE_UMAMI_WEBSITE_ID';", f"const NOMADRO_UMAMI_WEBSITE_ID = '{wid_nomadro}';", 1, ) with sftp.file("/tmp/nomadro-adsense.php", "w") as f: f.write(mu) run( c, """ docker compose -f /var/www/nomadro-wp/docker-compose.yml cp /tmp/nomadro-adsense.php wordpress:/var/www/html/wp-content/mu-plugins/nomadro-adsense.php docker compose -f /var/www/nomadro-wp/docker-compose.yml exec -T wordpress chown www-data:www-data /var/www/html/wp-content/mu-plugins/nomadro-adsense.php mkdir -p /var/www/nomadro-wp/mu-plugins && cp /tmp/nomadro-adsense.php /var/www/nomadro-wp/mu-plugins/nomadro-adsense.php """, ) run( c, f""" touch /var/www/nomadro.com/.env.local grep -q '^NEXT_PUBLIC_UMAMI_WEBSITE_ID=' /var/www/nomadro.com/.env.local \\ && sed -i 's/^NEXT_PUBLIC_UMAMI_WEBSITE_ID=.*/NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid_nomadro}/' /var/www/nomadro.com/.env.local \\ || echo 'NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid_nomadro}' >> /var/www/nomadro.com/.env.local """, ) sftp.put(str(REPO / "components/umami-analytics.tsx"), "/var/www/nomadro.com/components/umami-analytics.tsx") sftp.put(str(REPO / "app/layout.tsx"), "/var/www/nomadro.com/app/layout.tsx") for layout, domain in NEXT_LAYOUTS: wid = id_for(domain) run( c, f""" python3 - <<'PY' from pathlib import Path import re path = Path({layout!r}) wid = {wid!r} text = path.read_text(encoding='utf-8') line = f'' if "umami.nomadro.com/script.js" in text: text = re.sub(r'', line, text) else: if "]*>)', r'\\1\\n \\n '+line+r'\\n ', text, count=1) elif "" in text: text = text.replace("", "\\n "+line, 1) elif "> {path} """, ) rebuilds = [ ("cd /var/www/nomadro.com && pnpm build && systemctl restart nomadro", 900), ("cd /opt/douyinWEB/frontend && npm run build && systemctl restart douyinweb-web", 900), ( "cd /opt/tiktokWEB/frontend && npm run build && " "mkdir -p .next/standalone/.next && cp -a .next/static .next/standalone/.next/static 2>/dev/null || true; " "systemctl restart tiktok-promo-frontend", 900, ), ( "cd /opt/wauto/web && npm run build && " "mkdir -p .next/standalone/.next && cp -a public .next/standalone/ 2>/dev/null || true; " "cp -a .next/static .next/standalone/.next/static 2>/dev/null || true; " "systemctl restart hackrobot-web", 900, ), ] for cmd, t in rebuilds: try: run(c, cmd, timeout=t) except Exception as ex: print("rebuild warn", ex) # Test notify (may fail if token empty) run(c, "/usr/local/bin/notify-ntfy 'Observability online' 'Umami + ntfy ready' high 'tada' || true") run( c, """ echo '=== verify ===' curl -fsS https://umami.nomadro.com/api/heartbeat; echo curl -fsS https://ntfy.nomadro.com/v1/health; echo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'umami|ntfy|NAMES' echo '--- umami sites ---'; cat /etc/nomadro/umami-sites.env echo '--- sample homepage umami ---' curl -fsS -A 'Mozilla/5.0' https://nomadro.com/ | grep -o 'umami.nomadro.com/script.js' | head -3 curl -fsS -A 'Googlebot' https://nomadro.com/ | grep -o 'data-website-id="[^"]*"' | head -3 """, ) sftp.close() c.close() print("DONE") if __name__ == "__main__": main()