Files
clawdbot/src/web/outbound.ts
Peter Steinberger c379191f80 chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-14 15:02:19 +00:00

167 lines
5.4 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { normalizePollInput, type PollInput } from "../polls.js";
import { toWhatsappJid } from "../utils.js";
import { type ActiveWebSendOptions, requireActiveWebListener } from "./active-listener.js";
import { loadWebMedia } from "./media.js";
const outboundLog = createSubsystemLogger("gateway/channels/whatsapp").child("outbound");
export async function sendMessageWhatsApp(
to: string,
body: string,
options: {
verbose: boolean;
mediaUrl?: string;
gifPlayback?: boolean;
accountId?: string;
},
): Promise<{ messageId: string; toJid: string }> {
let text = body;
const correlationId = randomUUID();
const startedAt = Date.now();
const { listener: active, accountId: resolvedAccountId } = requireActiveWebListener(
options.accountId,
);
const logger = getChildLogger({
module: "web-outbound",
correlationId,
to,
});
try {
const jid = toWhatsappJid(to);
let mediaBuffer: Buffer | undefined;
let mediaType: string | undefined;
if (options.mediaUrl) {
const media = await loadWebMedia(options.mediaUrl);
const caption = text || undefined;
mediaBuffer = media.buffer;
mediaType = media.contentType;
if (media.kind === "audio") {
// WhatsApp expects explicit opus codec for PTT voice notes.
mediaType =
media.contentType === "audio/ogg"
? "audio/ogg; codecs=opus"
: (media.contentType ?? "application/octet-stream");
} else if (media.kind === "video") {
text = caption ?? "";
} else if (media.kind === "image") {
text = caption ?? "";
} else {
text = caption ?? "";
}
}
outboundLog.info(`Sending message -> ${jid}${options.mediaUrl ? " (media)" : ""}`);
logger.info({ jid, hasMedia: Boolean(options.mediaUrl) }, "sending message");
await active.sendComposingTo(to);
const hasExplicitAccountId = Boolean(options.accountId?.trim());
const accountId = hasExplicitAccountId ? resolvedAccountId : undefined;
const sendOptions: ActiveWebSendOptions | undefined =
options.gifPlayback || accountId
? {
...(options.gifPlayback ? { gifPlayback: true } : {}),
accountId,
}
: undefined;
const result = sendOptions
? await active.sendMessage(to, text, mediaBuffer, mediaType, sendOptions)
: await active.sendMessage(to, text, mediaBuffer, mediaType);
const messageId = (result as { messageId?: string })?.messageId ?? "unknown";
const durationMs = Date.now() - startedAt;
outboundLog.info(
`Sent message ${messageId} -> ${jid}${options.mediaUrl ? " (media)" : ""} (${durationMs}ms)`,
);
logger.info({ jid, messageId }, "sent message");
return { messageId, toJid: jid };
} catch (err) {
logger.error(
{ err: String(err), to, hasMedia: Boolean(options.mediaUrl) },
"failed to send via web session",
);
throw err;
}
}
export async function sendReactionWhatsApp(
chatJid: string,
messageId: string,
emoji: string,
options: {
verbose: boolean;
fromMe?: boolean;
participant?: string;
accountId?: string;
},
): Promise<void> {
const correlationId = randomUUID();
const { listener: active } = requireActiveWebListener(options.accountId);
const logger = getChildLogger({
module: "web-outbound",
correlationId,
chatJid,
messageId,
});
try {
const jid = toWhatsappJid(chatJid);
outboundLog.info(`Sending reaction "${emoji}" -> message ${messageId}`);
logger.info({ chatJid: jid, messageId, emoji }, "sending reaction");
await active.sendReaction(
chatJid,
messageId,
emoji,
options.fromMe ?? false,
options.participant,
);
outboundLog.info(`Sent reaction "${emoji}" -> message ${messageId}`);
logger.info({ chatJid: jid, messageId, emoji }, "sent reaction");
} catch (err) {
logger.error(
{ err: String(err), chatJid, messageId, emoji },
"failed to send reaction via web session",
);
throw err;
}
}
export async function sendPollWhatsApp(
to: string,
poll: PollInput,
options: { verbose: boolean; accountId?: string },
): Promise<{ messageId: string; toJid: string }> {
const correlationId = randomUUID();
const startedAt = Date.now();
const { listener: active } = requireActiveWebListener(options.accountId);
const logger = getChildLogger({
module: "web-outbound",
correlationId,
to,
});
try {
const jid = toWhatsappJid(to);
const normalized = normalizePollInput(poll, { maxOptions: 12 });
outboundLog.info(`Sending poll -> ${jid}: "${normalized.question}"`);
logger.info(
{
jid,
question: normalized.question,
optionCount: normalized.options.length,
maxSelections: normalized.maxSelections,
},
"sending poll",
);
const result = await active.sendPoll(to, normalized);
const messageId = (result as { messageId?: string })?.messageId ?? "unknown";
const durationMs = Date.now() - startedAt;
outboundLog.info(`Sent poll ${messageId} -> ${jid} (${durationMs}ms)`);
logger.info({ jid, messageId }, "sent poll");
return { messageId, toJid: jid };
} catch (err) {
logger.error(
{ err: String(err), to, question: poll.question },
"failed to send poll via web session",
);
throw err;
}
}