feat(bot): extend sender with image/video/document support
This commit is contained in:
parent
1aef3e969c
commit
d9a5f5a5e2
@ -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";
|
||||
|
||||
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 & {
|
||||
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[][]> {
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < items.length; i += size) {
|
||||
out.push(items.slice(i, i + size));
|
||||
}
|
||||
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,
|
||||
@ -39,44 +28,73 @@ async function ensureSessionsForGroup(
|
||||
}
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
const chunks = await chunked(participantJids, CHUNK_SIZE);
|
||||
for (const chunk of chunks) {
|
||||
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, chunkSize: chunk.length, err: (err as Error).message },
|
||||
"assertSessions chunk failed; continuing",
|
||||
);
|
||||
logger.warn({ groupJid, err: (err as Error).message }, "assertSessions chunk failed");
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
{ groupJid, ok, failed, total: participantJids.length },
|
||||
"ensureSessionsForGroup: done",
|
||||
);
|
||||
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 }> {
|
||||
await ensureSessionsForGroup(socket, groupJid);
|
||||
return sendWithRetry(socket, groupJid, { text });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await socket.sendMessage(groupJid, { text });
|
||||
return { messageId: result?.key?.id ?? undefined };
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message ?? "";
|
||||
if (message.includes("No sessions")) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await ensureSessionsForGroup(socket, groupJid);
|
||||
const result = await socket.sendMessage(groupJid, { text });
|
||||
return { messageId: result?.key?.id ?? undefined };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user