Files
yann64andClaude Opus 5.5 785c4967be Comments on news posts: one forum discussion per post
oc:pages command in the forum extension, tools/forum_sync.py and
'make forum-sync' (run by deploy) keep data/forum_pages.json up to date;
post pages end with their discussion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDRjtmAiNpo5mK5N3d7QwZ
2026-10-11 22:22:20 +02:00

72 lines
3.2 KiB
Python
Executable File

#!/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()