A chill Bluesky bot, with responses powered by Gemini.
1import { graphemeLength, Post, PostReference } from "@skyware/bot";
2import { muted_threads } from "../db/schema";
3import * as c from "../constants";
4import { eq } from "drizzle-orm";
5import * as yaml from "js-yaml";
6import bot from "../bot";
7import db from "../db";
8
9/*
10 Traversal
11*/
12export async function traverseThread(post: Post): Promise<Post[]> {
13 const thread: Post[] = [
14 post,
15 ];
16 let currentPost: Post | undefined = post;
17 let parentCount = 0;
18
19 while (
20 currentPost && parentCount < c.MAX_THREAD_DEPTH
21 ) {
22 const parentPost = await currentPost.fetchParent();
23
24 if (parentPost) {
25 thread.push(parentPost);
26 currentPost = parentPost;
27 } else {
28 break;
29 }
30 parentCount++;
31 }
32
33 return thread.reverse();
34}
35
36export function parseThread(thread: Post[]) {
37 return yaml.dump({
38 uri: thread[0]!.uri,
39 posts: thread.map((post) => ({
40 author: `${post.author.displayName} (${post.author.handle})`,
41 text: post.text,
42 })),
43 });
44}
45
46/*
47 Split Responses
48*/
49export function exceedsGraphemes(content: string) {
50 return graphemeLength(content) > c.MAX_GRAPHEMES;
51}
52
53export function splitResponse(text: string): string[] {
54 const words = text.split(" ");
55 const chunks: string[] = [];
56 let currentChunk = "";
57
58 for (const word of words) {
59 if (currentChunk.length + word.length + 1 < c.MAX_GRAPHEMES - 10) {
60 currentChunk += ` ${word}`;
61 } else {
62 chunks.push(currentChunk.trim());
63 currentChunk = word;
64 }
65 }
66
67 if (currentChunk.trim()) {
68 chunks.push(currentChunk.trim());
69 }
70
71 const total = chunks.length;
72 if (total <= 1) return [text];
73
74 return chunks.map((chunk, i) => `(${i + 1}/${total}) ${chunk}`);
75}
76
77export async function multipartResponse(content: string, post?: Post) {
78 const parts = splitResponse(content).filter((p) => p.trim().length > 0);
79
80 let latest: PostReference;
81 let rootUri: string;
82
83 if (post) {
84 rootUri = (post as any).rootUri ?? (post as any).uri;
85 latest = await post.reply({ text: parts[0]! });
86 } else {
87 latest = await bot.post({ text: parts[0]! });
88 rootUri = latest.uri;
89 }
90
91 for (const text of parts.slice(1)) {
92 latest = await latest.reply({ text });
93 }
94
95 return rootUri;
96}
97
98/*
99 Misc.
100*/
101export async function isThreadMuted(post: Post): Promise<boolean> {
102 const root = post.root || post;
103 if (!root) return false;
104
105 console.log("Found root: ", root.text);
106
107 const [mute] = await db
108 .select()
109 .from(muted_threads)
110 .where(eq(muted_threads.uri, root.uri));
111
112 return mute !== undefined;
113}