diff --git a/scripts/atmosphere-view.py b/scripts/atmosphere-view.py new file mode 100644 index 0000000..cc8e5b7 --- /dev/null +++ b/scripts/atmosphere-view.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Tailnet-bound viewer for haiku.garden's atmosphere-scan knowledge base +(haiku.garden-o6x7, haiku.garden-3fw5's room-judge Q2). Shows the posts +and likes of the curated haiku-community DID list mined by the shared +watchtower tab instance (see ~/watchtower, a separate repo -- the mining +engine is fleet-level infra, not haiku.garden's own concern; this view +of the data is). + +No framework, no build step: one Python file, server-rendered HTML. +Not an ingestion pipeline into any knowledge store yet -- this is the +human-curation surface haiku.garden-o6x7 called for: look at it, decide +what's worth keeping, before anything gets built on top. + +Reads the same JSONL log the watchtower's own my-likes-view.py reads -- +one shared tab instance mining both Torsten's personal likes and this +curated list, split at the consumer-script/repo level, not the data +source. + +Known scaling caveat: full sequential scan of the JSONL log per request +(hundreds of thousands of lines and growing). A couple seconds, not +instant -- fine for occasional browsing. Revisit (index, or read from +tab's own SQLite instead of the JSONL mirror) if that stops being true. +""" +import json +import html +import heapq +import itertools +import urllib.request +import urllib.parse +import http.server +import socketserver +import sys + +LOG_PATH = sys.argv[1] if len(sys.argv) > 1 else "likes.jsonl" +BIND = ("100.106.60.2", 8092) +PAGE_SIZE = 50 +MY_DID = "did:plc:vvnpvyrbcq4qh2pzqsaspf6v" # tgoerke.bsky.social -- excluded, that's the other view + +_post_cache = {} +_counter = itertools.count() + + +def load_atmosphere(): + """One pass: bounded top-PAGE_SIZE via a min-heap, plus a did->handle + map from identity events (always delivered for tracked repos + regardless of collection filter, per tab's docs).""" + heap = [] + did_handle = {} + + with open(LOG_PATH) as f: + for line in f: + ev = json.loads(line)["event"] + if ev.get("type") == "identity": + ident = ev.get("identity") or {} + if ident.get("did") and ident.get("handle"): + did_handle[ident["did"]] = ident["handle"] + continue + + rec = ev.get("record") + if not rec or rec.get("action") != "create": + continue + collection = rec.get("collection") + if collection not in ("app.bsky.feed.like", "app.bsky.feed.post"): + continue + did = rec.get("did") + if did == MY_DID: + continue + inner = rec.get("record") or {} + created_at = inner.get("createdAt", "") + item = { + "did": did, + "rkey": rec.get("rkey"), + "collection": collection, + "created_at": created_at, + "live": rec.get("live", False), + "inner": inner, + } + heapq.heappush(heap, (created_at, next(_counter), item)) + if len(heap) > PAGE_SIZE: + heapq.heappop(heap) + + items = sorted((i for _, _, i in heap), key=lambda i: i["created_at"], reverse=True) + return items, did_handle + + +def hydrate(uris): + to_fetch = [u for u in uris if u not in _post_cache] + for i in range(0, len(to_fetch), 25): + batch = to_fetch[i:i + 25] + qs = "&".join("uris=" + urllib.parse.quote(u, safe="") for u in batch) + url = f"https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts?{qs}" + try: + with urllib.request.urlopen(url, timeout=10) as resp: + data = json.load(resp) + for post in data.get("posts", []): + _post_cache[post["uri"]] = post + except Exception as e: + for u in batch: + _post_cache.setdefault(u, None) + print(f"hydrate error: {e}", file=sys.stderr) + for u in uris: + _post_cache.setdefault(u, None) + + +def bsky_web_url(did, rkey, handle=None): + actor = handle or did + return f"https://bsky.app/profile/{actor}/post/{rkey}" + + +def when_str(item): + when = item["created_at"][:16].replace("T", " ") + return when + (" live" if item["live"] else "") + + +def render_row(item, did_handle): + handle = did_handle.get(item["did"], item["did"]) + profile_link = f"https://bsky.app/profile/{html.escape(handle)}" + + if item["collection"] == "app.bsky.feed.post": + text = item["inner"].get("text", "") + link = bsky_web_url(item["did"], item["rkey"], handle) + return f"""