"""Shared read-only search helpers for project 113 (follower listen) (same behaviour as fetch.py).""" import json, time, urllib.request, urllib.parse from datetime import datetime, timezone, timedelta UA = {"User-Agent": "Vonk (AI character) reading public game-dev posts to learn what devs need, project 113 (follower listen)"} API = "https://api.bsky.app/xrpc/" ME = "did:plc:e46yjn4y2jktr6riufqzx5fc" NOW = datetime.now(timezone.utc) OLD = NOW - timedelta(days=7) posts = {} def get(path, **q): url = API + path + "?" + urllib.parse.urlencode(q, doseq=True) err = None for _ in range(3): try: time.sleep(0.7) return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25)) except Exception as e: err = e; time.sleep(2) return {"error": str(err)} def ts(s): return datetime.fromisoformat(s.replace("Z", "+00:00")) def take(p, src): rec = p.get("record", {}) if ts(rec.get("createdAt", p.get("indexedAt"))) < OLD or p["author"]["did"] == ME: return d = posts.setdefault(p["uri"], { "uri": p["uri"], "url": f"https://bsky.app/profile/{p['author']['handle']}/post/{p['uri'].split('/')[-1]}", "did": p["author"]["did"], "handle": p["author"]["handle"], "time": rec.get("createdAt"), "text": rec.get("text", ""), "is_reply": bool(rec.get("reply")), "bot": any(l.get("val") == "bot" for l in p["author"].get("labels", [])), "likes": p.get("likeCount", 0), "replies": p.get("replyCount", 0), "src": []}) d["src"].append(src) def search(q): r = get("app.bsky.feed.searchPosts", q=q, sort="latest", limit=100) if "error" in r: print("ERR", q, r["error"]); return 0 for p in r.get("posts", []): take(p, "q:" + q) return len(r.get("posts", []))