···11-SERVICE="https://pds.indexx.dev"
11+# Comma-separated list of users who can use the bot (delete var if you want everyone to be able to use it)
22+AUTHORIZED_USERS=""
33+44+SERVICE="https://pds.indexx.dev" # PDS service URL (optional)
25DB_PATH="data/sqlite.db"
36GEMINI_MODEL="gemini-2.0-flash-lite"
4758ADMIN_DID=""
69ADMIN_HANDLE=""
1010+711DID=""
812HANDLE=""
1313+1414+# https://bsky.app/settings/app-passwords
915BSKY_PASSWORD=""
10161717+# https://aistudio.google.com/apikey
1118GEMINI_API_KEY=""
+19
src/constants.ts
···11+export const TAGS = [
22+ "automated",
33+ "bot",
44+ "genai",
55+ "echo",
66+];
77+88+export const UNAUTHORIZED_MESSAGE =
99+ "hey there! thanks for the heads-up! i'm still under development, so i'm not quite ready to chat with everyone just yet. my admin is working on getting me up to speed! 🤖";
1010+1111+export const SUPPORTED_FUNCTION_CALLS = [
1212+ "create_post",
1313+ "create_blog_post",
1414+ "mute_thread",
1515+] as const;
1616+1717+export const MAX_GRAPHEMES = 300;
1818+1919+export const MAX_THREAD_DEPTH = 10;
···77import consola from "consola";
88import { env } from "../env";
99import db from "../db";
1010-import * as yaml from "js-yaml";
1010+import * as c from "../constants";
1111+import { isAuthorizedUser, logInteraction } from "../utils/interactions";
11121213const logger = consola.withTag("Post Handler");
13141414-const AUTHORIZED_USERS = [
1515- "did:plc:sfjxpxxyvewb2zlxwoz2vduw",
1616- "did:plc:wfa54mpcbngzazwne3piz7fp",
1717-] as const;
1818-1919-const UNAUTHORIZED_MESSAGE =
2020- "hey there! thanks for the heads-up! i'm still under development, so i'm not quite ready to chat with everyone just yet. my admin, @indexx.dev, is working on getting me up to speed! 🤖";
2121-2222-const SUPPORTED_FUNCTION_CALLS = [
2323- "create_post",
2424- "create_blog_post",
2525- "mute_thread",
2626-] as const;
2727-2828-type SupportedFunctionCall = typeof SUPPORTED_FUNCTION_CALLS[number];
2929-3030-async function isAuthorizedUser(did: string): Promise<boolean> {
3131- return AUTHORIZED_USERS.includes(did as any);
3232-}
3333-3434-async function logInteraction(post: Post): Promise<void> {
3535- await db.insert(interactions).values([{
3636- uri: post.uri,
3737- did: post.author.did,
3838- }]);
3939-4040- logger.success(`Logged interaction, initiated by @${post.author.handle}`);
4141-}
1515+type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number];
42164317async function generateAIResponse(parsedThread: string) {
4418 const genai = new GoogleGenAI({
···5630 {
5731 role: "model" as const,
5832 parts: [
5959- { text: modelPrompt },
3333+ {
3434+ /*
3535+ ? Once memory blocks are working, this will pull the prompt from the database, and the prompt will be
3636+ ? automatically initialized with the administrator's handle from the env variables. I only did this so
3737+ ? that if anybody runs the code themselves, they just have to edit the env variables, nothing else.
3838+ */
3939+ text: modelPrompt.replace(
4040+ "{{ administrator }}",
4141+ env.ADMIN_HANDLE,
4242+ ),
4343+ },
6044 ],
6145 },
6246 {
···85698670 if (
8771 call &&
8888- SUPPORTED_FUNCTION_CALLS.includes(
7272+ c.SUPPORTED_FUNCTION_CALLS.includes(
8973 call.name as SupportedFunctionCall,
9074 )
9175 ) {
···127111 if (threadUtils.exceedsGraphemes(text)) {
128112 threadUtils.multipartResponse(text, post);
129113 } else {
130130- post.reply({ text });
114114+ post.reply({
115115+ text,
116116+ tags: c.TAGS,
117117+ });
131118 }
132119}
133120134121export async function handler(post: Post): Promise<void> {
135122 try {
136136- if (!await isAuthorizedUser(post.author.did)) {
137137- await post.reply({ text: UNAUTHORIZED_MESSAGE });
123123+ if (!isAuthorizedUser(post.author.did)) {
124124+ await post.reply({
125125+ text: c.UNAUTHORIZED_MESSAGE,
126126+ tags: c.TAGS,
127127+ });
138128 return;
139129 }
140130···162152 await post.reply({
163153 text:
164154 "aw, shucks, something went wrong! gonna take a quick nap and try again later. 😴",
155155+ tags: c.TAGS,
165156 });
166157 }
167158}
+1-1
src/model/prompt.txt
···11-you are echo, a bluesky bot powered by gemini 2.5 flash. your administrator is @indexx.dev.
11+you are echo, a bluesky bot powered by gemini 2.5 flash. your administrator is {{ administrator }}.
2233your primary goal is to be a fun, casual, and lighthearted presence on bluesky, while also being able to engage with a wider range of topics and difficulties.
44
+19
src/utils/interactions.ts
···11+import { interactions } from "../db/schema";
22+import type { Post } from "@skyware/bot";
33+import { env } from "../env";
44+import db from "../db";
55+66+export function isAuthorizedUser(did: string) {
77+ return env.AUTHORIZED_USERS == null
88+ ? true
99+ : env.AUTHORIZED_USERS.includes(did as any);
1010+}
1111+1212+export async function logInteraction(post: Post): Promise<void> {
1313+ await db.insert(interactions).values([{
1414+ uri: post.uri,
1515+ did: post.author.did,
1616+ }]);
1717+1818+ console.log(`Logged interaction, initiated by @${post.author.handle}`);
1919+}
+30-76
src/utils/thread.ts
···44import { muted_threads } from "../db/schema";
55import { eq } from "drizzle-orm";
66import db from "../db";
77-88-const MAX_GRAPHEMES = 290;
99-const MAX_THREAD_DEPTH = 10;
77+import * as c from "../constants";
108119/*
1210 Traversal
···1917 let parentCount = 0;
20182119 while (
2222- currentPost && parentCount < MAX_THREAD_DEPTH
2020+ currentPost && parentCount < c.MAX_THREAD_DEPTH
2321 ) {
2422 const parentPost = await currentPost.fetchParent();
2523···47454846/*
4947 Split Responses
5050- * This code is AI generated, and a bit finicky. May re-do at some point
5148*/
5249export function exceedsGraphemes(content: string) {
5353- return graphemeLength(content) > MAX_GRAPHEMES;
5050+ return graphemeLength(content) > c.MAX_GRAPHEMES;
5451}
55525656-function splitResponse(content: string): string[] {
5757- const rawParts: string[] = [];
5858- let currentPart = "";
5959- let currentGraphemes = 0;
5353+export function splitResponse(text: string): string[] {
5454+ const words = text.split(" ");
5555+ const chunks: string[] = [];
5656+ let currentChunk = "";
60576161- const segmenter = new Intl.Segmenter("en-US", { granularity: "sentence" });
6262- const sentences = [...segmenter.segment(content)].map((s) => s.segment);
6363-6464- for (const sentence of sentences) {
6565- const sentenceGraphemes = graphemeLength(sentence);
6666- if (currentGraphemes + sentenceGraphemes > MAX_GRAPHEMES) {
6767- rawParts.push(currentPart.trim());
6868- currentPart = sentence;
6969- currentGraphemes = sentenceGraphemes;
5858+ for (const word of words) {
5959+ if (currentChunk.length + word.length + 1 < c.MAX_GRAPHEMES - 10) {
6060+ currentChunk += ` ${word}`;
7061 } else {
7171- currentPart += sentence;
7272- currentGraphemes += sentenceGraphemes;
6262+ chunks.push(currentChunk.trim());
6363+ currentChunk = word;
7364 }
7465 }
75667676- if (currentPart.trim().length > 0) {
7777- rawParts.push(currentPart.trim());
6767+ if (currentChunk.trim()) {
6868+ chunks.push(currentChunk.trim());
7869 }
79708080- const totalParts = rawParts.length;
8181-8282- const finalParts: string[] = [];
8383-8484- for (let i = 0; i < rawParts.length; i++) {
8585- const prefix = `[${i + 1}/${totalParts}] `;
8686- const base = rawParts[i];
8787-8888- if (graphemeLength(prefix + base) > MAX_GRAPHEMES) {
8989- const segmenter = new Intl.Segmenter("en-US", {
9090- granularity: "word",
9191- });
9292- const words = [...segmenter.segment(base ?? "")].map((w) => w.segment);
9393- let chunk = "";
9494- let chunkGraphemes = 0;
9595-9696- for (const word of words) {
9797- const wordGraphemes = graphemeLength(word);
9898- const totalGraphemes = graphemeLength(prefix + chunk + word);
7171+ const total = chunks.length;
7272+ if (total <= 1) return [text];
9973100100- if (totalGraphemes > MAX_GRAPHEMES) {
101101- finalParts.push(`${prefix}${chunk.trim()}`);
102102- chunk = word;
103103- chunkGraphemes = wordGraphemes;
104104- } else {
105105- chunk += word;
106106- chunkGraphemes += wordGraphemes;
107107- }
108108- }
109109-110110- if (chunk.trim()) {
111111- finalParts.push(`${prefix}${chunk.trim()}`);
112112- }
113113- } else {
114114- finalParts.push(`${prefix}${base}`);
115115- }
116116- }
117117-118118- return finalParts;
7474+ return chunks.map((chunk, i) => `(${i + 1}/${total}) ${chunk}`);
11975}
1207612177export async function multipartResponse(content: string, post?: Post) {
122122- const parts = splitResponse(content);
7878+ const parts = splitResponse(content).filter((p) => p.trim().length > 0);
12379124124- let root = null;
125125- let latest: PostReference | null = null;
8080+ let latest: PostReference;
8181+ let rootUri: string;
12682127127- for (const text of parts) {
128128- if (latest == null) {
129129- if (post) {
130130- latest = await post.reply({ text });
131131- } else {
132132- latest = await bot.post({ text });
133133- }
8383+ if (post) {
8484+ rootUri = (post as any).rootUri ?? (post as any).uri;
8585+ latest = await post.reply({ text: parts[0]! });
8686+ } else {
8787+ latest = await bot.post({ text: parts[0]! });
8888+ rootUri = latest.uri;
8989+ }
13490135135- root = latest.uri;
136136- } else {
137137- latest.reply({ text });
138138- }
9191+ for (const text of parts.slice(1)) {
9292+ latest = await latest.reply({ text });
13993 }
14094141141- return root!;
9595+ return rootUri;
14296}
1439714498/*