Files

338 lines
12 KiB
Python

#!/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 <<EOF
NTFY_BASE_URL=https://ntfy.nomadro.com
NTFY_USER=admin
NTFY_PASSWORD={ntfy_pass}
NTFY_TOKEN=$TOKEN
NTFY_TOPIC=server-alerts
EOF
chmod 600 {CONF}/ntfy.env {CONF}/umami.env
cat {CONF}/ntfy.env
""",
timeout=90,
)
# Install helpers
run(
c,
f"""
cp {CONF}/observability-src/notify-ntfy.sh /usr/local/bin/notify-ntfy
cp {CONF}/observability-src/healthcheck.sh /usr/local/bin/nomadro-healthcheck
chmod +x /usr/local/bin/notify-ntfy /usr/local/bin/nomadro-healthcheck
cat >/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'<script defer src="https://umami.nomadro.com/script.js" data-website-id="{{wid}}"></script>'
if "umami.nomadro.com/script.js" in text:
text = re.sub(r'<script defer src="https://umami\\.nomadro\\.com/script\\.js" data-website-id="[^"]*"></script>', line, text)
else:
if "<html" in text and "<head" not in text:
text = re.sub(r'(<html[^>]*>)', r'\\1\\n <head>\\n '+line+r'\\n </head>', text, count=1)
elif "<head>" in text:
text = text.replace("<head>", "<head>\\n "+line, 1)
elif "<body" in text:
text = text.replace("<body", " "+line+"\\n <body", 1)
else:
text += "\\n"+line+"\\n"
path.write_text(text, encoding='utf-8')
print('patched', path)
PY
""",
)
# Also write env files for other next apps
for path, domain in [
("/opt/douyinWEB/frontend/.env.local", "dylive.nomadro.com"),
("/opt/tiktokWEB/frontend/.env.local", "nomadro.live"),
("/opt/wauto/web/.env.local", "wx.nomadro.com"),
]:
wid = id_for(domain)
run(
c,
f"""
touch {path}
grep -q '^NEXT_PUBLIC_UMAMI_WEBSITE_ID=' {path} \\
&& sed -i 's/^NEXT_PUBLIC_UMAMI_WEBSITE_ID=.*/NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid}/' {path} \\
|| echo 'NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid}' >> {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()