import { readFile, stat } from "node:fs/promises"; import type { WASocket, AnyMessageContent } from "@whiskeysockets/baileys"; import pino from "pino"; const logger = pino({ name: "sender" }); type SocketWithAssertSessions = WASocket & { assertSessions?: (jids: string[], force: boolean) => 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; } 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") { return { ok: 0, failed: 0, total: participantJids.length }; } let ok = 0; let failed = 0; for (const chunk of await chunked(participantJids, CHUNK_SIZE)) { try { await internal.assertSessions(chunk, true); ok += chunk.length; } catch (err) { failed += chunk.length; logger.warn({ groupJid, err: (err as Error).message }, "assertSessions chunk failed"); } } return { ok, failed, total: participantJids.length }; } async function sendWithRetry( socket: WASocket, groupJid: string, content: AnyMessageContent, ): Promise<{ messageId: string | undefined }> { await ensureSessionsForGroup(socket, groupJid); try { const result = await socket.sendMessage(groupJid, content); return { messageId: result?.key?.id ?? undefined }; } catch (err) { const message = (err as Error)?.message ?? ""; if (message.includes("No sessions")) { await new Promise((r) => setTimeout(r, 2000)); await ensureSessionsForGroup(socket, groupJid); const result = await socket.sendMessage(groupJid, content); return { messageId: result?.key?.id ?? undefined }; } throw err; } } export async function sendTextToGroup( socket: WASocket, groupJid: string, text: string, ): Promise<{ messageId: string | undefined }> { return sendWithRetry(socket, groupJid, { text }); } export type MediaKind = "image" | "video" | "document"; export async function sendMediaToGroup( socket: WASocket, groupJid: string, kind: MediaKind, filePath: string, options: { caption?: string; mimeType?: string; filename?: string } = {}, ): Promise<{ messageId: string | undefined }> { // Validate the file exists and read into a buffer. For very large files // (>50MB) Baileys also accepts a stream, but for our reminder use case // files are typically <30MB which fits comfortably in memory. await stat(filePath); const buffer = await readFile(filePath); const content: AnyMessageContent = kind === "image" ? { image: buffer, caption: options.caption, mimetype: options.mimeType } : kind === "video" ? { video: buffer, caption: options.caption, mimetype: options.mimeType } : { document: buffer, caption: options.caption, fileName: options.filename ?? "file", mimetype: options.mimeType ?? "application/octet-stream", }; return sendWithRetry(socket, groupJid, content); }