tangled
alpha
login
or
join now
indexx.dev
/
aero
9
fork
atom
A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.
9
fork
atom
overview
issues
3
pulls
pipelines
feat: use jetstream instead of firehose
indexx.dev
4 months ago
dda25521
f237f339
+58
-12
6 changed files
expand all
collapse all
unified
split
.env.example
docker-compose.yml
src
core.ts
env.ts
handlers
messages.ts
index.ts
+2
-2
.env.example
···
11
HANDLE=""
12
13
# https://bsky.app/settings/app-passwords
14
-
BSKY_PASSWORD=""
15
16
# https://aistudio.google.com/apikey
17
GEMINI_API_KEY=""
18
19
DAILY_QUERY_LIMIT=15
20
-
USE_FIREHOSE=false
···
11
HANDLE=""
12
13
# https://bsky.app/settings/app-passwords
14
+
APP_PASSWORD=""
15
16
# https://aistudio.google.com/apikey
17
GEMINI_API_KEY=""
18
19
DAILY_QUERY_LIMIT=15
20
+
USE_JETSTREAM=false
+1
-1
docker-compose.yml
···
10
- "GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}"
11
- "DID=${DID:?}"
12
- "HANDLE=${HANDLE:?}"
13
-
- "BSKY_PASSWORD=${BSKY_PASSWORD:?}"
14
- "GEMINI_API_KEY=${GEMINI_API_KEY:?}"
15
volumes:
16
- aero_db:/sqlite.db
···
10
- "GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}"
11
- "DID=${DID:?}"
12
- "HANDLE=${HANDLE:?}"
13
+
- "APP_PASSWORD=${APP_PASSWORD:?}"
14
- "GEMINI_API_KEY=${GEMINI_API_KEY:?}"
15
volumes:
16
- aero_db:/sqlite.db
+46
-2
src/core.ts
···
1
import { GoogleGenAI } from "@google/genai";
2
import { Bot, EventStrategy } from "@skyware/bot";
3
import { env } from "./env";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4
5
export const bot = new Bot({
6
service: env.SERVICE,
7
emitChatEvents: true,
8
eventEmitterOptions: {
9
-
strategy: env.USE_FIREHOSE
10
-
? EventStrategy.Firehose
11
: EventStrategy.Polling,
12
},
13
});
···
1
import { GoogleGenAI } from "@google/genai";
2
import { Bot, EventStrategy } from "@skyware/bot";
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
+
}
48
49
export const bot = new Bot({
50
service: env.SERVICE,
51
emitChatEvents: true,
52
eventEmitterOptions: {
53
+
strategy: env.USE_JETSTREAM
54
+
? EventStrategy.Jetstream
55
: EventStrategy.Polling,
56
},
57
});
+4
-2
src/env.ts
···
11
DB_PATH: z.string().default("sqlite.db"),
12
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
13
0
0
14
DID: z.string(),
15
HANDLE: z.string(),
16
-
BSKY_PASSWORD: z.string(),
17
18
GEMINI_API_KEY: z.string(),
19
DAILY_QUERY_LIMIT: z.preprocess(
···
21
(typeof val === "string" && val.trim() !== "") ? Number(val) : undefined,
22
z.number().int().positive().default(15),
23
),
24
-
USE_FIREHOSE: z.preprocess(
25
(val) => val === "true",
26
z.boolean().default(false),
27
),
···
11
DB_PATH: z.string().default("sqlite.db"),
12
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
13
14
+
ADMIN_DID: z.string().optional(),
15
+
16
DID: z.string(),
17
HANDLE: z.string(),
18
+
APP_PASSWORD: z.string(),
19
20
GEMINI_API_KEY: z.string(),
21
DAILY_QUERY_LIMIT: z.preprocess(
···
23
(typeof val === "string" && val.trim() !== "") ? Number(val) : undefined,
24
z.number().int().positive().default(15),
25
),
26
+
USE_JETSTREAM: z.preprocess(
27
(val) => val === "true",
28
z.boolean().default(false),
29
),
+4
-4
src/handlers/messages.ts
···
170
throw new Error("Failed to generate text. Returned undefined.");
171
}
172
173
-
logger.success("Generated text:", inference.text);
174
-
175
-
saveMessage(conversation, env.DID, inference.text!);
176
-
177
const responseText = inference.text;
0
178
if (responseText) {
0
0
0
179
await sendResponse(conversation, responseText);
180
}
181
} catch (error) {
···
170
throw new Error("Failed to generate text. Returned undefined.");
171
}
172
0
0
0
0
173
const responseText = inference.text;
174
+
175
if (responseText) {
176
+
logger.success("Generated text:", inference.text);
177
+
saveMessage(conversation, env.DID, inference.text!);
178
+
179
await sendResponse(conversation, responseText);
180
}
181
} catch (error) {
+1
-1
src/index.ts
···
11
try {
12
await bot.login({
13
identifier: env.HANDLE,
14
-
password: env.BSKY_PASSWORD,
15
});
16
17
logger.success(`Logged in as @${env.HANDLE} (${env.DID})`);
···
11
try {
12
await bot.login({
13
identifier: env.HANDLE,
14
+
password: env.APP_PASSWORD,
15
});
16
17
logger.success(`Logged in as @${env.HANDLE} (${env.DID})`);