A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 126 lines 3.0 kB view raw
1import { 2 EmbedImage, 3 Post, 4 PostEmbed, 5 RecordEmbed, 6 RecordWithMediaEmbed, 7} from "@skyware/bot"; 8import * as c from "../core"; 9import * as yaml from "js-yaml"; 10import type { ParsedPost } from "../types"; 11import { postCache } from "../utils/cache"; 12 13export async function parsePost( 14 post: Post, 15 includeThread: boolean, 16 seenUris: Set<string> = new Set(), 17): Promise<ParsedPost | undefined> { 18 if (seenUris.has(post.uri)) { 19 return undefined; 20 } 21 seenUris.add(post.uri); 22 23 const [images, quotePost, ancestorPosts] = await Promise.all([ 24 parsePostImages(post), 25 parseQuote(post, seenUris), 26 includeThread ? traverseThread(post) : Promise.resolve(null), 27 ]); 28 29 return { 30 author: post.author.displayName 31 ? `${post.author.displayName} (${post.author.handle})` 32 : `Handle: ${post.author.handle}`, 33 text: post.text, 34 ...(images && { images }), 35 ...(quotePost && { quotePost }), 36 ...(ancestorPosts && { 37 thread: { 38 ancestors: (await Promise.all( 39 ancestorPosts.map((ancestor) => parsePost(ancestor, false, seenUris)), 40 )).filter((post): post is ParsedPost => post !== undefined), 41 }, 42 }), 43 }; 44} 45 46async function parseQuote(post: Post, seenUris: Set<string>) { 47 if ( 48 !post.embed || (!post.embed.isRecord() && !post.embed.isRecordWithMedia()) 49 ) return undefined; 50 51 const record = (post.embed as RecordEmbed || RecordWithMediaEmbed).record; 52 if (seenUris.has(record.uri)) { 53 return undefined; 54 } 55 56 let embedPost = postCache.get(record.uri); 57 if (!embedPost) { 58 embedPost = await c.bot.getPost(record.uri); 59 postCache.set(record.uri, embedPost); 60 } 61 62 return await parsePost(embedPost, false, seenUris); 63} 64 65export function parsePostImages(post: Post) { 66 if (!post.embed) return []; 67 68 let images: EmbedImage[] = []; 69 70 if (post.embed.isImages()) { 71 images = post.embed.images; 72 } else if (post.embed.isRecordWithMedia()) { 73 const media = post.embed.media; 74 if (media && media.isImages()) { 75 images = media.images; 76 } 77 } 78 79 return images.map((image, idx) => parseImage(image, idx + 1)).filter((img) => 80 img.alt.length > 0 81 ); 82} 83 84function parseImage(image: EmbedImage, index: number) { 85 return { 86 index: index, 87 alt: image.alt, 88 }; 89} 90 91/* 92 Traversal 93*/ 94export async function traverseThread(post: Post): Promise<Post[]> { 95 const thread: Post[] = [ 96 post, 97 ]; 98 let currentPost: Post | undefined = post; 99 let parentCount = 0; 100 101 while ( 102 currentPost && parentCount < c.MAX_THREAD_DEPTH 103 ) { 104 const parentPost = await currentPost.fetchParent(); 105 106 if (parentPost) { 107 thread.push(parentPost); 108 currentPost = parentPost; 109 } else { 110 break; 111 } 112 parentCount++; 113 } 114 115 return thread.reverse(); 116} 117 118export function parseThread(thread: Post[]) { 119 return yaml.dump({ 120 uri: thread[0]!.uri, 121 posts: thread.map((post) => ({ 122 author: `${post.author.displayName} (${post.author.handle})`, 123 text: post.text, 124 })), 125 }); 126}