#!/usr/bin/env python3 """seed-capture-threads.py — create each room's capture-thread root, print the event IDs. Posts one "capture thread" root message into every room in config.toml and prints the resulting event_id per room. Paste each into its [[room]] block as `capture_thread = "$..."` — the bot then treats any reply in that thread as a capture (no /capture prefix needed in-thread). Idempotent-ish: by default it skips rooms that already have `capture_thread` set in config, so a re-run after adding a project only seeds the new room. Use --force to reseed everything, pass room labels to seed only those, or --dry-run to preview without posting. Reads ../.env (MATRIX_ACCESS_TOKEN) and ../config.toml (homeserver URL + rooms). Talks to the homeserver over plain HTTPS, so run it anywhere the homeserver is reachable (Mac or Spark). python3 scripts/seed-capture-threads.py [--force] [--dry-run] [label ...] """ import json import os import sys import tomllib import urllib.request import urllib.error import uuid HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) def load_env(path): env = {} with open(path) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) env[k] = v.strip().strip('"') return env def root_body(project): who = "any project" if project == "?" else f"({project})" tag = "?" if project == "?" else project return ( f"📥 Capture thread — reply in this thread to log an idea, feature, or bug for {who}. " f"It goes straight to the cross-project inbox tagged ({tag}) (no /capture prefix needed " f"in-thread); run /triage in the repo to land it." ) def send_root(url, token, room_id, project): txn = uuid.uuid4().hex endpoint = ( f"{url.rstrip('/')}/_matrix/client/v3/rooms/" f"{urllib.parse.quote(room_id, safe='')}/send/m.room.message/{txn}" ) body = json.dumps({"msgtype": "m.text", "body": root_body(project)}).encode() req = urllib.request.Request( endpoint, data=body, method="PUT", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=30) as resp: return json.load(resp)["event_id"] def main(): args = [a for a in sys.argv[1:]] force = "--force" in args dry_run = "--dry-run" in args only = [a for a in args if not a.startswith("--")] env = load_env(os.path.join(ROOT, ".env")) with open(os.path.join(ROOT, "config.toml"), "rb") as f: cfg = tomllib.load(f) url = cfg["homeserver"]["url"] token = env.get("MATRIX_ACCESS_TOKEN") or env.get("MATRIX_ACCESS_TOKEN".lower()) if not token: sys.exit("no MATRIX_ACCESS_TOKEN in .env") # Build the seed list: every project room, plus the all-projects fan-out room as a `?` catch-all. targets = [] for r in cfg.get("room", []): targets.append((r["label"], r["room_id"], r.get("capture_project", r["label"]), r.get("capture_thread"))) ap = cfg.get("all_projects", {}) if ap.get("room_id"): targets.append(("all-projects", ap["room_id"], ap.get("capture_project", "?"), ap.get("capture_thread"))) if only: targets = [t for t in targets if t[0] in only] seeded = [] for label, room_id, project, existing in targets: if existing and not force: print(f"SKIP {label:<20} already has capture_thread={existing}") continue if dry_run: print(f"DRYRUN {label:<20} {room_id} (project {project})") continue try: event_id = send_root(url, token, room_id, project) seeded.append((label, room_id, project, event_id)) print(f"SEEDED {label:<20} {room_id} -> {event_id}") except urllib.error.HTTPError as e: print(f"ERROR {label:<20} {room_id} HTTP {e.code}: {e.read().decode()[:200]}") except Exception as e: # noqa: BLE001 — surface any transport failure per room, keep going print(f"ERROR {label:<20} {room_id} {e}") if seeded: print("\n# Paste each line into the matching [[room]] block (or [all_projects]):") for label, _room_id, _project, event_id in seeded: print(f'{label}: capture_thread = "{event_id}"') if __name__ == "__main__": main()