#!/usr/bin/env python3 """Make sure every published news post has its comment discussion in the forum. Lists the posts with `hugo list all`, sends the list to the forum, where `php flarum oc:pages` creates the missing discussions, and stores the returned map (content file -> discussion number) in data/forum_pages.json, which the layouts read. Run before building (see `make forum-sync`). --run is the command that executes a shell line where the forum lives, e.g. 'ssh -p 27 user@host' or 'docker compose -f forum/docker-compose.yml exec -T flarum sh -c' """ import argparse import csv import io import json import shlex import subprocess import sys from datetime import datetime, timezone from pathlib import Path from urllib.parse import urlparse ROOT = Path(__file__).resolve().parent.parent MAP = ROOT / "data" / "forum_pages.json" def posts(): """Published posts as the forum command expects them, oldest first.""" out = subprocess.run(["hugo", "list", "all"], cwd=ROOT, check=True, capture_output=True, text=True).stdout now, pages = datetime.now(timezone.utc), [] for row in csv.DictReader(io.StringIO(out)): if row["kind"] != "page" or row["section"] != "post" or row["draft"] == "true": continue date = datetime.fromisoformat(row["date"].replace("Z", "+00:00")) published = datetime.fromisoformat(row["publishDate"].replace("Z", "+00:00")) if published > now: continue pages.append({ "key": row["path"].removeprefix("content/"), "title": row["title"], "date": date.strftime("%Y-%m-%d %H:%M:%S") if date.year > 1 else "", "path": urlparse(row["permalink"]).path, }) return sorted(pages, key=lambda p: p["date"]) def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("--run", required=True, help="command prefix that runs a shell line on the forum host") ap.add_argument("--dir", required=True, help="forum folder on that host") ap.add_argument("--php", default="php", help="PHP command line there") ap.add_argument("--site-url", required=True, help="public address of the site, for the link in each discussion") args = ap.parse_args() pages = posts() remote = (f"cd {shlex.quote(args.dir)} && cat > storage/oc-pages.json && " f"{args.php} flarum oc:pages storage/oc-pages.json --site-url={shlex.quote(args.site_url.rstrip('/'))} " f"--map=storage/oc-pages-map.json >&2 && cat storage/oc-pages-map.json; " f"rm -f storage/oc-pages.json storage/oc-pages-map.json") done = subprocess.run([*shlex.split(args.run), remote], input=json.dumps(pages), capture_output=True, text=True) notes = [line for line in done.stderr.splitlines() if "discussions created" in line] if done.returncode != 0 or not done.stdout.strip().startswith(("{", "[")): sys.exit(f"forum sync failed:\n{done.stderr.strip()[-800:]}") mapping = json.loads(done.stdout) or {} text = json.dumps(dict(sorted(mapping.items())), indent=1) + "\n" if not MAP.exists() or MAP.read_text() != text: MAP.write_text(text) print(f"{len(pages)} posts; {notes[-1].strip() if notes else 'done'}") if __name__ == "__main__": main()