#!/usr/bin/env python3 import argparse import json from html import escape from bscon import connect def get_posts(client, handle, limit=100): """Download all public posts of a profile with images.""" posts = [] cursor = None while True: resp = client.app.bsky.feed.get_author_feed({ "actor": handle, "limit": limit, "cursor": cursor, }) for item in resp.feed: post = item.post record = post.record # Extract images if any images = [] embed = getattr(record, "embed", None) if embed and hasattr(embed, "images"): images = [{"thumb": i.thumb, "full": i.fullsize} for i in embed.images] posts.append({ "uri": post.uri, "cid": post.cid, "createdAt": getattr(record, "createdAt", None) or getattr(record, "created_at", None), "text": getattr(record, "text", ""), "images": images }) cursor = getattr(resp, "cursor", None) if not cursor: break return posts def save_posts(posts, handle): """Save posts to JSON.""" out_file = f"{handle.replace('.', '_')}_posts.json" with open(out_file, "w", encoding="utf-8") as f: json.dump(posts, f, indent=2, ensure_ascii=False) print(f"Saved {len(posts)} posts to {out_file}") def uri_to_url(uri): """Convert at://did/... URIs to clickable Bluesky URLs.""" parts = uri.split("/") if len(parts) >= 5 and parts[3] == "app.bsky.feed.post": did = parts[2] rkey = parts[4] return f"https://bsky.app/profile/{did}/post/{rkey}" return uri def posts_to_html(posts, out_html): """Generate HTML with text and image thumbnails.""" with open(out_html, "w", encoding="utf-8") as f: f.write("
\n") for post in posts: text = escape(post.get("text", "")) url = uri_to_url(post.get("uri", "")) imgs_html = "".join( f'