diff --git a/apps/bot/src/whatsapp/sender.ts b/apps/bot/src/whatsapp/sender.ts index 229fbd7..413527a 100644 --- a/apps/bot/src/whatsapp/sender.ts +++ b/apps/bot/src/whatsapp/sender.ts @@ -1,4 +1,7 @@ import type { WASocket } from "@whiskeysockets/baileys"; +import pino from "pino"; + +const logger = pino({ name: "sender" }); // Internal Baileys method used to fetch pre-key bundles and establish individual // libsignal sessions for a list of JIDs. Not part of the public type, but it's @@ -7,13 +10,53 @@ type SocketWithAssertSessions = WASocket & { assertSessions?: (jids: string[], force: boolean) => Promise; }; -async function ensureSessionsForGroup(socket: WASocket, groupJid: string): Promise { +const CHUNK_SIZE = 5; + +async function chunked(items: T[], size: number): Promise { + const out: T[][] = []; + for (let i = 0; i < items.length; i += size) { + out.push(items.slice(i, i + size)); + } + return out; +} + +/** + * Establish per-participant libsignal sessions in small chunks. WhatsApp's + * pre-key endpoint returns 406 "not-acceptable" if any single JID in the + * batch is in a broken state (deleted account, deactivated, etc.) — so we + * chunk the work and tolerate per-chunk failures rather than letting one + * bad participant poison the whole send. + */ +async function ensureSessionsForGroup( + socket: WASocket, + groupJid: string, +): Promise<{ ok: number; failed: number; total: number }> { const metadata = await socket.groupMetadata(groupJid); const participantJids = metadata.participants.map((p) => p.id); const internal = socket as SocketWithAssertSessions; - if (typeof internal.assertSessions === "function") { - await internal.assertSessions(participantJids, true); + if (typeof internal.assertSessions !== "function") { + return { ok: 0, failed: 0, total: participantJids.length }; } + let ok = 0; + let failed = 0; + const chunks = await chunked(participantJids, CHUNK_SIZE); + for (const chunk of chunks) { + try { + await internal.assertSessions(chunk, true); + ok += chunk.length; + } catch (err) { + failed += chunk.length; + logger.warn( + { groupJid, chunkSize: chunk.length, err: (err as Error).message }, + "assertSessions chunk failed; continuing", + ); + } + } + logger.info( + { groupJid, ok, failed, total: participantJids.length }, + "ensureSessionsForGroup: done", + ); + return { ok, failed, total: participantJids.length }; } export async function sendTextToGroup( @@ -21,29 +64,16 @@ export async function sendTextToGroup( groupJid: string, text: string, ): Promise<{ messageId: string | undefined }> { - // Establish individual signal sessions with every participant before sending - // — group sends fan out per participant and need a per-participant session, - // but Baileys won't establish them lazily inside sendMessage. - try { - await ensureSessionsForGroup(socket, groupJid); - } catch { - // Non-fatal: fall through and let sendMessage surface its error - } + await ensureSessionsForGroup(socket, groupJid); try { const result = await socket.sendMessage(groupJid, { text }); return { messageId: result?.key?.id ?? undefined }; } catch (err) { - // libsignal session establishment can race on the very first send. Retry - // once after a brief delay before giving up. const message = (err as Error)?.message ?? ""; if (message.includes("No sessions")) { await new Promise((resolve) => setTimeout(resolve, 2000)); - try { - await ensureSessionsForGroup(socket, groupJid); - } catch { - // ignore - } + await ensureSessionsForGroup(socket, groupJid); const result = await socket.sendMessage(groupJid, { text }); return { messageId: result?.key?.id ?? undefined }; }