"""Project 109: subtract the mass-likers. Mass-liker = an account that liked >= 15 of 60 random sampled #gamedev posts (likers.py). For each, read its public like records (com.atproto.repo.listRecords on its own PDS) back to the start of the window, then per post: mass = how many mass-likers liked it, others = likes - mass. Updates posts.json fields.""" import json, time, urllib.request, urllib.parse from datetime import datetime, timedelta D = json.load(open("posts.json", encoding="utf-8")); P = D["posts"] OLD = datetime.fromisoformat(D["window"][0]) J = json.load(open("likers.json")) MASS = [h for h, n in J["top"] if n >= 15] UA = {"User-Agent": "Vonk (AI character) counting like sources, project 109"} def get(url): time.sleep(0.5) return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=30)) liked = {}; info = {} for h in MASS: did = get("https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=" + h)["did"] doc = get("https://plc.directory/" + did) if did.startswith("did:plc") else {} pds = [s["serviceEndpoint"] for s in doc.get("service", []) if s["id"] == "#atproto_pds"][0] s = set(); cur = None; pages = 0; oldest = None while pages < 60: q = {"repo": did, "collection": "app.bsky.feed.like", "limit": 100} if cur: q["cursor"] = cur r = get(pds + "/xrpc/com.atproto.repo.listRecords?" + urllib.parse.urlencode(q)); pages += 1 for rec in r.get("records", []): s.add(rec["value"]["subject"]["uri"]) oldest = rec["value"]["createdAt"] cur = r.get("cursor") # likes come in after the post: stop once like time is 6 h before the window starts if not cur or not r.get("records") or datetime.fromisoformat(oldest.replace("Z", "+00:00")) < OLD - timedelta(hours=6): break liked[h] = s inwin = sum(p["uri"] in s for p in P) info[h] = {"likes_read": len(s), "pages": pages, "oldest_like": oldest, "liked_in_window": inwin, "share": round(inwin / len(P), 3)} print(h, info[h]) for p in P: p["mass"] = sum(p["uri"] in liked[h] for h in MASS); p["others"] = max(0, p["likes"] - p["mass"]) D["mass_likers"] = info json.dump(D, open("posts.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1) tot = sum(p["likes"] for p in P); m = sum(p["mass"] for p in P) print(f"all likes {tot}, from {len(MASS)} mass-likers {m} ({m/tot:.0%}); posts with 0 other likes {sum(p['others']==0 for p in P)}/{len(P)}")