Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.
Merged
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
44 changes: 37 additions & 7 deletions src/channels/whatsapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export class WhatsAppChannel implements Channel {
private flushing = false;
private groupSyncTimerStarted = false;

// Store event handlers so they can be removed on reconnect
private messageHandler: ((data: any) => Promise<void>) | null = null;
private reactionHandler: ((data: any) => Promise<void>) | null = null;
private connectionHandler: ((data: any) => void) | null = null;
private credsHandler: (() => Promise<void>) | null = null;

private opts: WhatsAppChannelOpts;

constructor(opts: WhatsAppChannelOpts) {
Expand All @@ -57,6 +63,22 @@ export class WhatsAppChannel implements Channel {

const { state, saveCreds } = await useMultiFileAuthState(authDir);

// Remove old event listeners before creating new socket (prevents memory leaks)
if (this.sock?.ev) {
if (this.connectionHandler) {
this.sock.ev.off('connection.update', this.connectionHandler);
}
if (this.credsHandler) {
this.sock.ev.off('creds.update', this.credsHandler);
}
if (this.messageHandler) {
this.sock.ev.off('messages.upsert', this.messageHandler);
}
if (this.reactionHandler) {
this.sock.ev.off('messages.reaction', this.reactionHandler);
}
}

this.sock = makeWASocket({
auth: {
creds: state.creds,
Expand All @@ -67,7 +89,8 @@ export class WhatsAppChannel implements Channel {
browser: Browsers.macOS('Chrome'),
});

this.sock.ev.on('connection.update', (update) => {
// Store connection handler for later removal
this.connectionHandler = (update) => {
const { connection, lastDisconnect, qr } = update;

if (qr) {
Expand Down Expand Up @@ -140,11 +163,15 @@ export class WhatsAppChannel implements Channel {
onFirstOpen = undefined;
}
}
});
};
this.sock.ev.on('connection.update', this.connectionHandler);

this.sock.ev.on('creds.update', saveCreds);
// Store creds handler for later removal
this.credsHandler = saveCreds;
this.sock.ev.on('creds.update', this.credsHandler);

this.sock.ev.on('messages.upsert', async ({ messages }) => {
// Store message handler for later removal
this.messageHandler = async ({ messages }) => {
for (const msg of messages) {
if (!msg.message) continue;
const rawJid = msg.key.remoteJid;
Expand Down Expand Up @@ -183,15 +210,18 @@ export class WhatsAppChannel implements Channel {
});
}
}
});
};
this.sock.ev.on('messages.upsert', this.messageHandler);

this.sock.ev.on('messages.reaction', async (reactions) => {
// Store reaction handler for later removal
this.reactionHandler = async (reactions) => {
for (const { key, reaction } of reactions) {
if (!reaction?.text || !key.id || !key.remoteJid) continue;
const chatJid = await this.translateJid(key.remoteJid);
this.opts.onReaction?.(chatJid, key.id, reaction.text);
}
});
};
this.sock.ev.on('messages.reaction', this.reactionHandler);
}

async sendMessage(jid: string, text: string): Promise<string | void> {
Expand Down
12 changes: 10 additions & 2 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ export function updateTask(
>
>,
): void {
// SECURITY NOTE: Field names are hardcoded below (not user-controlled), making this safe from SQL injection.
// All values use parameterized queries (?). If this logic changes to allow dynamic field selection,
// ensure field names are validated against an allowlist.
const fields: string[] = [];
const values: (string | null)[] = [];

Expand Down Expand Up @@ -450,8 +453,13 @@ export function updateTask(

export function deleteTask(id: string): void {
// Delete child records first (FK constraint)
db.query('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
db.query('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
// Wrap in transaction to ensure both deletes succeed or both roll back
const transaction = db.transaction(() => {
db.query('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
db.query('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
});

transaction();
}

export function getDueTasks(): ScheduledTask[] {
Expand Down
26 changes: 26 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,28 @@ function saveState(): void {
}

function registerGroup(jid: string, group: RegisteredGroup): void {
// Validate folder name to prevent path traversal attacks
if (!/^[a-zA-Z0-9_-]+$/.test(group.folder)) {
throw new Error(
`Invalid group folder name: "${group.folder}". Only alphanumeric characters, hyphens, and underscores are allowed.`,
);
}

registeredGroups[jid] = group;
setRegisteredGroup(jid, group);

// Create group folder
const groupDir = path.join(DATA_DIR, '..', 'groups', group.folder);

// Additional safety check: ensure the resolved path is within the groups directory
const groupsRoot = path.resolve(path.join(DATA_DIR, '..', 'groups'));
const resolvedGroupDir = path.resolve(groupDir);
if (!resolvedGroupDir.startsWith(groupsRoot + path.sep)) {
throw new Error(
`Path traversal detected in group folder: "${group.folder}"`,
);
}

fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });

// Seed CLAUDE.md for Discord groups with secondary-channel instructions
Expand Down Expand Up @@ -688,6 +705,15 @@ async function main(): Promise<void> {
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Handle unhandled promise rejections to prevent process crashes
process.on('unhandledRejection', (reason, promise) => {
logger.error(
{ reason, promise },
'CRITICAL: Unhandled promise rejection detected - this should be fixed'
);
// Don't exit - log and continue to prevent service outage
});

// Create WhatsApp channel
whatsapp = new WhatsAppChannel({
onMessage: (chatJid, msg) => storeMessage(msg),
Expand Down
Loading