···11+import argparse
22+import asyncio
13from collections import Counter
44+from typing import List
2533-import requests
44-from atproto import AtUri, Client
66+from atproto import AtUri, Client, DidDocument
57from atproto_identity.did.resolver import DidResolver
88+from microcosmClient import AsyncMicrocosmClient
69710client = Client(base_url="https://bsky.social")
811resolver = DidResolver(plc_url="https://plc.directory")
91210131111-def getRecs(uri: AtUri, n=100):
1212- # get the users that liked that item
1313- # go get the n most recent liked items for each of those users
1414- # return the most common items from that list
1414+async def getRecs(uri: AtUri, n: int = 100, concurrency: int = 10) -> List[tuple]:
1515+ """Return top liked subject URIs as (uri, count) tuples for likers of the given target.
15161616- response = requests.get(
1717- f"https://constellation.microcosm.blue/links/distinct-dids?target={uri}&collection=app.bsky.feed.like&path=.subject.uri&limit={n}", headers={'Accept': 'application/json'})
1717+ - Uses Microcosm to find distinct likers of `uri`.
1818+ - For each liker (repo), resolves the DID and calls the blocking AtProto repo.list_records in a thread.
1919+ """
2020+ async with AsyncMicrocosmClient() as micro:
2121+ data = await micro.links_distinct_dids(uri, "app.bsky.feed.like", ".subject.uri")
18221919- if response.status_code == 200:
2020- data = response.json()
2121- likers = data['linking_dids']
2323+ likers = data.get("linking_dids", [])
2424+ if not likers:
2525+ return []
22262323- records = []
2424- for user in likers:
2525- didDoc = resolver.resolve(user)
2626- client.update_base_url(didDoc.service[0].service_endpoint)
2727- likes = client.com.atproto.repo.list_records(params={"collection": "app.bsky.feed.like",
2828- "repo": user,
2929- "limit": n})
3030- for record in likes.records:
3131- records.append(record.value.subject.uri)
2727+ semaphore = asyncio.Semaphore(concurrency)
32283333- c = Counter([r for r in records if r != str(uri)])
3434- for item, count in c.most_common(10):
3535- print(
3636- f"https://bsky.app/profile/{item.split('at://')[1].split('/')[0] + '/post/' + item.split('/')[-1]}: {count}")
3737- else:
3838- print("Error fetching data:", response.status_code)
2929+ async def fetch(repo: str):
3030+ async with semaphore:
3131+ try:
3232+ did: DidDocument | None = await asyncio.to_thread(resolver.resolve, repo)
3333+ if not did or not did.service:
3434+ return []
3535+ await asyncio.to_thread(client.update_base_url, did.service[0].service_endpoint)
3636+ likes = await asyncio.to_thread(lambda: client.com.atproto.repo.list_records(params={"collection": "app.bsky.feed.like", "repo": repo, "limit": n}))
3737+ return [r["value"]["subject"]["uri"] for r in likes["records"]]
3838+ except Exception:
3939+ return []
4040+4141+ tasks = [asyncio.create_task(fetch(u)) for u in likers]
4242+ records: List[str] = []
4343+ for t in asyncio.as_completed(tasks):
4444+ records.extend(await t)
4545+4646+ records = [r for r in records if r and r != str(uri)]
4747+ c = Counter(records)
4848+ return c.most_common(5)
394940504151def fromPost(post: str) -> AtUri:
4252 handle, rkey = post.split("/post/")
4353 handle = handle.split("https://bsky.app/profile/")[1]
4444- did = client.com.atproto.identity.resolve_handle(
4545- params={"handle": handle})
5454+ did = client.com.atproto.identity.resolve_handle(params={"handle": handle})
4655 return AtUri.from_str(f"at://{did.did}/app.bsky.feed.post/{rkey}")
475648574949-getRecs(fromPost("https://bsky.app/profile/beverage2000.bsky.social/post/3lzpn5gewbc2e"))
5858+def toPost(uri) -> str:
5959+ did = uri.split('//')[1].split('/')[0]
6060+ rkey = uri.split('.post/')[1]
6161+ return f"https://bsky.app/profile/{did}/post/{rkey}"
6262+6363+6464+async def main(post: str):
6565+ uri = fromPost(post)
6666+ recs = await getRecs(uri)
6767+ if not recs:
6868+ print("No recommendations found.")
6969+ return
7070+ for item, count in recs:
7171+ print(f"{toPost(item)}: {count}")
7272+7373+7474+if __name__ == "__main__":
7575+ parser = argparse.ArgumentParser()
7676+ parser.add_argument(
7777+ "post", nargs="?", default="https://bsky.app/profile/beverage2000.bsky.social/post/3lzpn5gewbc2e")
7878+ args = parser.parse_args()
7979+ asyncio.run(main(args.post))