Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/bot/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
16 changes: 8 additions & 8 deletions packages/bot/discord/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
16 changes: 8 additions & 8 deletions packages/bot/signal/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
16 changes: 8 additions & 8 deletions packages/bot/telegram/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
247 changes: 124 additions & 123 deletions packages/bot/whatsapp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,129 @@
import makeBaileysBot, { type WASocket } from "baileys";
import { z } from "zod";

const configSchema = z.object({
botName: z.string().default("Bot"),
aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"),
aiCliPath: z.string().default("claude"),
aiModel: z.string().optional(),
sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000),
maxOutputLength: z.number().int().positive().default(4000),
maxConcurrentSessions: z.number().int().positive().default(5),
allowedUsers: z.array(z.string()).default([]),
adminUsers: z.array(z.string()).default([]),
});

export type Config = z.infer<typeof configSchema>;

export interface IncomingMessage {
source: string;
sourceName: string;
text: string;
timestamp: number;
chatId: string;
isGroup: boolean;
attachments: Array<{ filename: string; url: string }>;
raw: any;
}

type MessageHandler = (msg: IncomingMessage) => void | Promise<void>;

export class WhatsAppBot {
private sock: WASocket | null = null;
private handlers: MessageHandler[] = [];
private config: Config;
private running = false;

constructor(config: Config) {
this.config = config;
}

onMessage(handler: MessageHandler): void {
this.handlers.push(handler);
}

async start(): Promise<void> {
const { state, saveState } = useMultiFileAuthState("./auth");

this.sock = makeBaileysBot(state, {
print: console.log,
import makeBaileysBot, { type WASocket } from "baileys";
import { z } from "zod";
const configSchema = z.object({
botName: z.string().default("Bot"),
aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"),
aiCliPath: z.string().default("claude"),
aiModel: z.string().optional(),
sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000),
maxOutputLength: z.number().int().positive().default(4000),
maxConcurrentSessions: z.number().int().positive().default(5),
allowedUsers: z.array(z.string()).default([]),
adminUsers: z.array(z.string()).default([]),
});
export type Config = z.infer<typeof configSchema>;
export interface IncomingMessage {
source: string;
sourceName: string;
text: string;
timestamp: number;
chatId: string;
isGroup: boolean;
attachments: Array<{ filename: string; url: string }>;
raw: any;
}
type MessageHandler = (msg: IncomingMessage) => void | Promise<void>;
export class WhatsAppBot {
private sock: WASocket | null = null;
private handlers: MessageHandler[] = [];
private config: Config;
private running = false;
constructor(config: Config) {
this.config = config;
}
onMessage(handler: MessageHandler): void {
this.handlers.push(handler);
}
async start(): Promise<void> {
const { state, saveState } = useMultiFileAuthState("./auth");
this.sock = makeBaileysBot({
auth: state,
browser: ["sh1pt-bot", "Chrome", "120"],
});
this.sock.ev.on("creds.update", saveState);

this.sock.ev.on("messages.upsert", async ({ messages }) => {
for (const msg of messages) {
if (!msg.message || msg.key.fromMe) continue;

const chatId = msg.key.remoteJid!;
const isGroup = chatId.endsWith("@g.us");
const text =
msg.message.conversation ||
msg.message.extendedTextMessage?.text ||
"";

const incoming: IncomingMessage = {
source: chatId,
sourceName: msg.pushName || "User",
text,
timestamp: msg.messageTimestamp * 1000,
chatId,
isGroup,
attachments: [],
raw: msg,
};

for (const handler of this.handlers) {
try {
const result = handler(incoming);
if (result instanceof Promise) result.catch(console.error);
} catch (err) {
console.error("Handler error:", err);
}
}
}
});

this.running = true;
console.log("WhatsApp bot started");
}

async stop(): Promise<void> {
this.running = false;
}

isRunning(): boolean {
return this.running;
}

async reply(msg: IncomingMessage, text: string): Promise<void> {
await this.sock?.sendMessage(msg.chatId, { text });
}

async send(chatId: string, text: string): Promise<void> {
await this.sock?.sendMessage(chatId, { text });
}
for (const msg of messages) {
if (!msg.message || msg.key.fromMe) continue;

const chatId = msg.key.remoteJid!;
const isGroup = chatId.endsWith("@g.us");
const text =
msg.message.conversation ||
msg.message.extendedTextMessage?.text ||
"";

const incoming: IncomingMessage = {
source: chatId,
sourceName: msg.pushName || "User",
text,
timestamp: msg.messageTimestamp * 1000,
chatId,
isGroup,
attachments: [],
raw: msg,
};

for (const handler of this.handlers) {
try {
const result = handler(incoming);
if (result instanceof Promise) result.catch(console.error);
} catch (err) {
console.error("Handler error:", err);
}
}
}
});

this.running = true;
console.log("WhatsApp bot started");
}

async stop(): Promise<void> {
this.running = false;
}
Comment on lines +91 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 stop() leaves the socket connection open

stop() flips this.running but never calls this.sock?.end() (or the equivalent Baileys close method). The underlying WebSocket stays connected and event listeners keep firing after stop() returns, which can cause unexpected behavior or resource leaks if start() is called again.


isRunning(): boolean {
return this.running;
}

async reply(msg: IncomingMessage, text: string): Promise<void> {
await this.sock?.sendMessage(msg.chatId, { text });
}

async send(chatId: string, text: string): Promise<void> {
await this.sock?.sendMessage(chatId, { text });
}
}

function useMultiFileAuthState(
dir: string
): { state: any; saveState: () => void } {
return {
state: {},
saveState: () => {},
};
}
Comment on lines +108 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 useMultiFileAuthState stub silently discards all credentials

The local stub always returns state: {} (so Baileys starts unauthenticated on every launch) and saveState: () => {} (a no-op). Wiring creds.update to this callback — one of the stated goals of the PR — will never actually persist credentials to disk, so the bot will require a fresh QR-code scan on every restart. Baileys exports the real useMultiFileAuthState from the "baileys" package; it should be imported rather than re-implemented here.


export function loadConfig(env: Record<string, string | undefined>): Config {
return configSchema.parse({
botName: env.BOT_NAME || "Bot",
aiProvider: (env.AI_PROVIDER as any) || "claude-code",
aiCliPath: env.AI_CLI_PATH || "claude",
aiModel: env.AI_MODEL,
sessionTimeoutMs: parseInt(env.SESSION_TIMEOUT_MS || "1800000", 10),
maxOutputLength: parseInt(env.MAX_OUTPUT_LENGTH || "4000", 10),
maxConcurrentSessions: parseInt(env.MAX_CONCURRENT_SESSIONS || "5", 10),
allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [],
adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [],
});
}

function useMultiFileAuthState(
dir: string
): { state: any; saveState: () => void } {
return {
state: {},
saveState: () => {},
};
}

export function loadConfig(env: Record<string, string | undefined>): Config {
return configSchema.parse({
botName: env.BOT_NAME || "Bot",
aiProvider: (env.AI_PROVIDER as any) || "claude-code",
aiCliPath: env.AI_CLI_PATH || "claude",
aiModel: env.AI_MODEL,
sessionTimeoutMs: parseInt(env.SESSION_TIMEOUT_MS || "1800000", 10),
maxOutputLength: parseInt(env.MAX_OUTPUT_LENGTH || "4000", 10),
maxConcurrentSessions: parseInt(env.MAX_CONCURRENT_SESSIONS || "5", 10),
allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [],
adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [],
});
}
16 changes: 8 additions & 8 deletions packages/bot/whatsapp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}