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

feat: use jetstream instead of firehose

+2 -2
.env.example
··· 11 11 HANDLE="" 12 12 13 13 # https://bsky.app/settings/app-passwords 14 - BSKY_PASSWORD="" 14 + APP_PASSWORD="" 15 15 16 16 # https://aistudio.google.com/apikey 17 17 GEMINI_API_KEY="" 18 18 19 19 DAILY_QUERY_LIMIT=15 20 - USE_FIREHOSE=false 20 + USE_JETSTREAM=false
+1 -1
docker-compose.yml
··· 10 10 - "GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}" 11 11 - "DID=${DID:?}" 12 12 - "HANDLE=${HANDLE:?}" 13 - - "BSKY_PASSWORD=${BSKY_PASSWORD:?}" 13 + - "APP_PASSWORD=${APP_PASSWORD:?}" 14 14 - "GEMINI_API_KEY=${GEMINI_API_KEY:?}" 15 15 volumes: 16 16 - aero_db:/sqlite.db
+46 -2
src/core.ts
··· 1 1 import { GoogleGenAI } from "@google/genai"; 2 2 import { Bot, EventStrategy } from "@skyware/bot"; 3 3 import { env } from "./env"; 4 + import type { BinaryType } from "bun"; 5 + 6 + // Websocket patch was written by Claude, hopefully it doesn't suck 7 + const OriginalWebSocket = global.WebSocket; 8 + const binaryTypeDescriptor = Object.getOwnPropertyDescriptor( 9 + OriginalWebSocket.prototype, 10 + "binaryType", 11 + ); 12 + 13 + const originalSetter = binaryTypeDescriptor?.set; 14 + 15 + if (OriginalWebSocket && originalSetter) { 16 + global.WebSocket = new Proxy(OriginalWebSocket, { 17 + construct(target, args) { 18 + //@ts-ignore 19 + const ws = new target(...args) as WebSocket & { 20 + _binaryType?: BinaryType; 21 + }; 22 + 23 + Object.defineProperty(ws, "binaryType", { 24 + get(): BinaryType { 25 + return ws._binaryType || 26 + (binaryTypeDescriptor.get 27 + ? binaryTypeDescriptor.get.call(ws) 28 + : "arraybuffer"); 29 + }, 30 + set(value: BinaryType) { 31 + //@ts-ignore 32 + if (value === "blob") { 33 + originalSetter.call(ws, "arraybuffer"); 34 + //@ts-ignore 35 + ws._binaryType = "blob"; 36 + } else { 37 + originalSetter.call(ws, value); 38 + ws._binaryType = value; 39 + } 40 + }, 41 + configurable: true, 42 + }); 43 + 44 + return ws; 45 + }, 46 + }) as typeof WebSocket; 47 + } 4 48 5 49 export const bot = new Bot({ 6 50 service: env.SERVICE, 7 51 emitChatEvents: true, 8 52 eventEmitterOptions: { 9 - strategy: env.USE_FIREHOSE 10 - ? EventStrategy.Firehose 53 + strategy: env.USE_JETSTREAM 54 + ? EventStrategy.Jetstream 11 55 : EventStrategy.Polling, 12 56 }, 13 57 });
+4 -2
src/env.ts
··· 11 11 DB_PATH: z.string().default("sqlite.db"), 12 12 GEMINI_MODEL: z.string().default("gemini-2.5-flash"), 13 13 14 + ADMIN_DID: z.string().optional(), 15 + 14 16 DID: z.string(), 15 17 HANDLE: z.string(), 16 - BSKY_PASSWORD: z.string(), 18 + APP_PASSWORD: z.string(), 17 19 18 20 GEMINI_API_KEY: z.string(), 19 21 DAILY_QUERY_LIMIT: z.preprocess( ··· 21 23 (typeof val === "string" && val.trim() !== "") ? Number(val) : undefined, 22 24 z.number().int().positive().default(15), 23 25 ), 24 - USE_FIREHOSE: z.preprocess( 26 + USE_JETSTREAM: z.preprocess( 25 27 (val) => val === "true", 26 28 z.boolean().default(false), 27 29 ),
+4 -4
src/handlers/messages.ts
··· 170 170 throw new Error("Failed to generate text. Returned undefined."); 171 171 } 172 172 173 - logger.success("Generated text:", inference.text); 174 - 175 - saveMessage(conversation, env.DID, inference.text!); 176 - 177 173 const responseText = inference.text; 174 + 178 175 if (responseText) { 176 + logger.success("Generated text:", inference.text); 177 + saveMessage(conversation, env.DID, inference.text!); 178 + 179 179 await sendResponse(conversation, responseText); 180 180 } 181 181 } catch (error) {
+1 -1
src/index.ts
··· 11 11 try { 12 12 await bot.login({ 13 13 identifier: env.HANDLE, 14 - password: env.BSKY_PASSWORD, 14 + password: env.APP_PASSWORD, 15 15 }); 16 16 17 17 logger.success(`Logged in as @${env.HANDLE} (${env.DID})`);