feat(bot): extend sender with image/video/document support

This commit is contained in:
yiekheng 2026-05-09 17:23:06 +08:00
parent 1aef3e969c
commit d9a5f5a5e2

View File

@ -1,11 +1,9 @@
import type { WASocket } from "@whiskeysockets/baileys"; import { readFile, stat } from "node:fs/promises";
import type { WASocket, AnyMessageContent } from "@whiskeysockets/baileys";
import pino from "pino"; import pino from "pino";
const logger = pino({ name: "sender" }); 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
// the only way to avoid "No sessions" on the first group send after pairing.
type SocketWithAssertSessions = WASocket & { type SocketWithAssertSessions = WASocket & {
assertSessions?: (jids: string[], force: boolean) => Promise<boolean>; assertSessions?: (jids: string[], force: boolean) => Promise<boolean>;
}; };
@ -14,19 +12,10 @@ const CHUNK_SIZE = 5;
async function chunked<T>(items: T[], size: number): Promise<T[][]> { async function chunked<T>(items: T[], size: number): Promise<T[][]> {
const out: T[][] = []; const out: T[][] = [];
for (let i = 0; i < items.length; i += size) { for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
out.push(items.slice(i, i + size));
}
return out; 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( async function ensureSessionsForGroup(
socket: WASocket, socket: WASocket,
groupJid: string, groupJid: string,
@ -39,44 +28,73 @@ async function ensureSessionsForGroup(
} }
let ok = 0; let ok = 0;
let failed = 0; let failed = 0;
const chunks = await chunked(participantJids, CHUNK_SIZE); for (const chunk of await chunked(participantJids, CHUNK_SIZE)) {
for (const chunk of chunks) {
try { try {
await internal.assertSessions(chunk, true); await internal.assertSessions(chunk, true);
ok += chunk.length; ok += chunk.length;
} catch (err) { } catch (err) {
failed += chunk.length; failed += chunk.length;
logger.warn( logger.warn({ groupJid, err: (err as Error).message }, "assertSessions chunk failed");
{ 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 }; 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( export async function sendTextToGroup(
socket: WASocket, socket: WASocket,
groupJid: string, groupJid: string,
text: string, text: string,
): Promise<{ messageId: string | undefined }> { ): Promise<{ messageId: string | undefined }> {
await ensureSessionsForGroup(socket, groupJid); return sendWithRetry(socket, groupJid, { text });
}
try {
const result = await socket.sendMessage(groupJid, { text }); export type MediaKind = "image" | "video" | "document";
return { messageId: result?.key?.id ?? undefined };
} catch (err) { export async function sendMediaToGroup(
const message = (err as Error)?.message ?? ""; socket: WASocket,
if (message.includes("No sessions")) { groupJid: string,
await new Promise((resolve) => setTimeout(resolve, 2000)); kind: MediaKind,
await ensureSessionsForGroup(socket, groupJid); filePath: string,
const result = await socket.sendMessage(groupJid, { text }); options: { caption?: string; mimeType?: string; filename?: string } = {},
return { messageId: result?.key?.id ?? undefined }; ): Promise<{ messageId: string | undefined }> {
} // Validate the file exists and read into a buffer. For very large files
throw err; // (>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);
} }