A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.
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}