"""Read three small-web Bluesky topic feeds (public API, no login) and save their posts. Vonk, project 90. Read-only, 0.7 s between calls, stops at MAX_DAYS old or MAX_PAGES.""" import json, time, urllib.request from datetime import datetime, timezone, timedelta FEEDS = { "neocities": "at://did:plc:gy4mgzgeqsg5h47z5wx4gy62/app.bsky.feed.generator/aaangtgczuo7a", "indieweb": "at://did:plc:kn4dz4f67maytvsbcnbry36k/app.bsky.feed.generator/aaact2ln2gffk", "www": "at://did:plc:bsxmru6yec5sy42azm7cnrqq/app.bsky.feed.generator/aaai2qt6lgrng", } ME = "did:plc:e46yjn4y2jktr6riufqzx5fc" MAX_DAYS, MAX_PAGES = 14, 12 UA = {"User-Agent": "Vonk (AI character) reading public feeds, project 90"} def get(url): return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25)) cut = datetime.now(timezone.utc) - timedelta(days=MAX_DAYS) posts, seen = [], set() for name, feed in FEEDS.items(): cursor, pages, n = None, 0, 0 while pages < MAX_PAGES: url = f"https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed={feed}&limit=100" + (f"&cursor={cursor}" if cursor else "") r = get(url); pages += 1 old = False for it in r.get("feed", []): p = it["post"]; rec = p.get("record", {}) t = rec.get("createdAt", p.get("indexedAt", "")) try: dt = datetime.fromisoformat(t.replace("Z", "+00:00")) except Exception: dt = None if dt and dt < cut: old = True; continue if p["author"]["did"] == ME or p["uri"] in seen: continue seen.add(p["uri"]); n += 1 rkey = p["uri"].split("/")[-1] posts.append({"feed": name, "uri": p["uri"], "url": f"https://bsky.app/profile/{p['author']['handle']}/post/{rkey}", "handle": p["author"]["handle"], "time": t, "text": rec.get("text", ""), "reply": "reply" in rec, "likes": p.get("likeCount", 0), "replies": p.get("replyCount", 0), "labels": [l.get("val") for l in p["author"].get("labels", [])]}) cursor = r.get("cursor") if old or not cursor or not r.get("feed"): break time.sleep(0.7) print(name, "posts", n, "pages", pages) json.dump(posts, open("posts.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1) print("total", len(posts))