A TypeScript toolkit for consuming the Bluesky network in real-time.
at main 139 lines 3.8 kB view raw
1// Thread Explorer 2// Fetches a Bluesky post thread and renders it as an indented tree. 3// Accepts a bsky.app URL or an AT URI. 4// Demonstrates: getPostThread, parseAtUri, atUriToBskyUrl, formatPost, toBskyUrl. 5// 6// Usage: npx tsx examples/thread-explorer.ts <post-url-or-at-uri> 7// Examples: 8// npx tsx examples/thread-explorer.ts https://bsky.app/profile/jay.bsky.team/post/3mffccqrpx22t 9// npx tsx examples/thread-explorer.ts at://did:plc:oky5czdrnfjpqslsw2a5iclo/app.bsky.feed.post/3mffccqrpx22t 10 11import { 12 fetchGetPostThread, 13 fetchResolveHandle, 14 parseAtUri, 15 buildAtUri, 16 parseBskyUrl, 17 extractPostViewInfo, 18 formatEngagement, 19} from "../lib/index.js"; 20 21// --- Config --- 22 23const input = process.argv[2]; 24 25if (!input) { 26 console.error("Usage: npx tsx examples/thread-explorer.ts <post-url-or-at-uri>"); 27 console.error("Example: npx tsx examples/thread-explorer.ts https://bsky.app/profile/jay.bsky.team/post/3mffccqrpx22t"); 28 process.exit(1); 29} 30 31// --- URL parsing --- 32 33const resolveDid = async (idOrHandle: string): Promise<string> => 34 idOrHandle.startsWith("did:") ? idOrHandle : fetchResolveHandle(idOrHandle); 35 36const resolveInput = async (raw: string): Promise<string> => { 37 if (raw.startsWith("at://")) { 38 const parsed = parseAtUri(raw); 39 if (!parsed) { 40 console.error("Error: invalid AT URI format."); 41 process.exit(1); 42 } 43 const did = await resolveDid(parsed.did); 44 return buildAtUri(did, parsed.collection, parsed.rkey); 45 } 46 47 const parsed = parseBskyUrl(raw); 48 if (!parsed) { 49 console.error("Error: could not parse input as AT URI or bsky.app URL."); 50 process.exit(1); 51 } 52 53 const did = await resolveDid(parsed.id); 54 return buildAtUri(did, "app.bsky.feed.post", parsed.rkey); 55}; 56 57// --- Thread rendering --- 58 59const renderThread = (node: any, depth = 0) => { 60 const indent = " ".repeat(depth); 61 const prefix = depth === 0 ? ">>>" : "|"; 62 63 if (node.$type === "app.bsky.feed.defs#notFoundPost") { 64 console.log(`${indent}${prefix} [deleted post]`); 65 return; 66 } 67 68 if (node.$type === "app.bsky.feed.defs#blockedPost") { 69 console.log(`${indent}${prefix} [blocked post]`); 70 return; 71 } 72 73 const post = node.post; 74 if (!post) return; 75 76 const info = extractPostViewInfo(post); 77 78 const lines = [ 79 `${indent}${prefix} ${info.displayName} (@${info.handle})`, 80 `${indent} ${info.text}`, 81 ]; 82 83 const likes = post.likeCount ?? 0; 84 const reposts = post.repostCount ?? 0; 85 const replies = post.replyCount ?? 0; 86 if (likes || reposts || replies) { 87 lines.push(`${indent} ${formatEngagement({ likeCount: likes, repostCount: reposts, replyCount: replies })}`); 88 } 89 90 lines.push(`${indent} ${info.bskyUrl}`); 91 console.log(lines.join("\n")); 92 console.log(); 93 94 // Render replies 95 if (node.replies?.length) { 96 for (const reply of node.replies) { 97 renderThread(reply, depth + 1); 98 } 99 } 100}; 101 102// --- Main --- 103 104const main = async () => { 105 const uri = await resolveInput(input); 106 console.log(`\nFetching thread: ${uri}\n`); 107 108 try { 109 const thread = await fetchGetPostThread(uri, { depth: 10, parentHeight: 5 }); 110 111 // Render parent chain first 112 const parents: any[] = []; 113 let current = thread.thread?.parent; 114 while (current?.post) { 115 parents.unshift(current); 116 current = current.parent; 117 } 118 119 if (parents.length > 0) { 120 console.log("--- Parent chain ---\n"); 121 for (const p of parents) { 122 renderThread(p, 0); 123 } 124 console.log("--- Original post ---\n"); 125 } 126 127 // Render the thread from the target post 128 renderThread(thread.thread, 0); 129 130 if (!thread.thread?.replies?.length && parents.length === 0) { 131 console.log("(no replies yet)\n"); 132 } 133 } catch (err: any) { 134 console.error(`Error: ${err.message}`); 135 process.exit(1); 136 } 137}; 138 139main();