openclaw/src/web/outbound.ts

114 lines
3.8 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { AnyMessageContent } from "@whiskeysockets/baileys";
import { logVerbose } from "../globals.js";
import { logInfo } from "../logger.js";
import { getChildLogger } from "../logging.js";
import { toWhatsappJid } from "../utils.js";
import { loadWebMedia } from "./media.js";
import { getActiveWebListener } from "./active-listener.js";
import { createWaSocket, waitForWaConnection } from "./session.js";
export async function sendMessageWhatsApp(
to: string,
body: string,
options: { verbose: boolean; mediaUrl?: string },
): Promise<{ messageId: string; toJid: string }> {
const correlationId = randomUUID();
const active = getActiveWebListener();
const usingActive = Boolean(active);
const sock = usingActive ? null : await createWaSocket(false, options.verbose);
const logger = getChildLogger({
module: "web-outbound",
correlationId,
to,
});
try {
const jid = toWhatsappJid(to);
if (!usingActive) {
logInfo("🔌 Connecting to WhatsApp Web…");
logger.info("connecting to whatsapp web");
await waitForWaConnection(sock!);
try {
await sock!.sendPresenceUpdate("composing", jid);
} catch (err) {
logVerbose(`Presence update skipped: ${String(err)}`);
}
}
let payload: AnyMessageContent = { text: body };
if (options.mediaUrl) {
const media = await loadWebMedia(options.mediaUrl);
const caption = body || undefined;
if (media.kind === "audio") {
// WhatsApp expects explicit opus codec for PTT voice notes.
const mimetype =
media.contentType === "audio/ogg"
? "audio/ogg; codecs=opus"
: (media.contentType ?? "application/octet-stream");
payload = { audio: media.buffer, ptt: true, mimetype };
} else if (media.kind === "video") {
const mimetype = media.contentType ?? "application/octet-stream";
payload = {
video: media.buffer,
caption,
mimetype,
};
} else if (media.kind === "image") {
const mimetype = media.contentType ?? "application/octet-stream";
payload = {
image: media.buffer,
caption,
mimetype,
};
} else {
// Fallback to document for anything else (pdf, etc.).
const fileName = media.fileName ?? "file";
const mimetype = media.contentType ?? "application/octet-stream";
payload = {
document: media.buffer,
fileName,
caption,
mimetype,
};
}
}
logInfo(
`📤 Sending via web session -> ${jid}${options.mediaUrl ? " (media)" : ""}`,
);
logger.info(
{ jid, hasMedia: Boolean(options.mediaUrl) },
"sending message",
);
const result = usingActive
? await (async () => {
let mediaBuffer: Buffer | undefined;
let mediaType: string | undefined;
if (options.mediaUrl) {
const media = await loadWebMedia(options.mediaUrl);
mediaBuffer = media.buffer;
mediaType = media.contentType;
}
await active!.sendComposingTo(to);
return active!.sendMessage(to, body, mediaBuffer, mediaType);
})()
: await sock!.sendMessage(jid, payload);
const messageId = usingActive
? (result as { messageId?: string })?.messageId ?? "unknown"
: (result as any)?.key?.id ?? "unknown";
logInfo(
`✅ Sent via web session. Message ID: ${messageId} -> ${jid}${options.mediaUrl ? " (media)" : ""}`,
);
logger.info({ jid, messageId }, "sent message");
return { messageId, toJid: jid };
} finally {
if (!usingActive) {
try {
sock?.ws?.close();
} catch (err) {
logVerbose(`Socket close failed: ${String(err)}`);
}
}
}
}