feat(whatsapp): support native outbound mentions (#73961)

Merged via squash.

Prepared head SHA: bb1df9e681
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Reviewed-by: @BunsDev
This commit is contained in:
openclaw-clownfish[bot]
2026-05-02 23:50:54 -05:00
committed by GitHub
parent b1f8172867
commit 22748b1c36
7 changed files with 552 additions and 18 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Channels/secrets: resolve SecretRef-backed channel credentials through external plugin secret contracts after the plugin split, covering runtime startup, target discovery, webhook auth, disabled-account enumeration, and late-bound web_search config. (#76449) Thanks @joshavant.
- Docker/Gateway: pass Docker setup `.env` values into gateway and CLI containers and preserve exec SecretRef `passEnv` keys in managed service plans, so 1Password Connect-backed Discord tokens keep resolving after doctor or plugin repair. Thanks @vincentkoc.
- Control UI/WebChat: explain compaction boundaries in chat history and link directly to session checkpoint controls so pre-compaction turns no longer look silently lost after refresh. Fixes #76415. Thanks @BunsDev.
- Channels/WhatsApp: attach native outbound mention metadata for group text and media captions by resolving `@+<digits>` and `@<digits>` tokens against WhatsApp participant data, including LID groups. Fixes #39879; carries forward #56863. Thanks @kengi1437, @joe2643, and @fridayck.
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.

View File

@@ -153,6 +153,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window; after a transient reconnect for a recently active session, that application-silence check uses the normal message timeout for the first recovery window.
- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` controls WhatsApp Web application pings, `connectTimeoutMs` controls the opening handshake timeout, and `defaultQueryTimeoutMs` controls Baileys query timeouts.
- Outbound sends require an active WhatsApp listener for the target account.
- Group sends attach native mention metadata for `@+<digits>` and `@<digits>` tokens in text and media captions when the token matches current WhatsApp participant metadata, including LID-backed groups.
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
- The reconnect watchdog follows WhatsApp Web transport activity, not only inbound app-message volume: quiet linked-device sessions stay up while transport frames continue, but a transport stall forces reconnect well before the later remote disconnect path.
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).

View File

@@ -40,6 +40,12 @@ import {
} from "./extract.js";
import { attachEmitterListener, closeInboundMonitorSocket } from "./lifecycle.js";
import { downloadInboundMedia, downloadQuotedInboundMedia } from "./media.js";
import {
addWhatsAppOutboundMentionsToContent,
mayContainWhatsAppOutboundMention,
resolveWhatsAppOutboundMentions,
type WhatsAppOutboundMentionParticipant,
} from "./outbound-mentions.js";
import { DisconnectReason, isJidGroup, saveMediaBuffer } from "./runtime-api.js";
import { createWebSendApi } from "./send-api.js";
import { normalizeWhatsAppSendResult } from "./send-result.js";
@@ -57,6 +63,7 @@ type WhatsAppGroupMetadataCacheEntry = {
export type WhatsAppGroupMetadataCache = Map<string, WhatsAppGroupMetadataCacheEntry>;
type LocalGroupMetadataCacheEntry = WhatsAppGroupMetadataCacheEntry & {
participants?: string[];
mentionParticipants?: WhatsAppOutboundMentionParticipant[];
};
function rememberGroupMetadataCacheEntry<T extends WhatsAppGroupMetadataCacheEntry>(
@@ -355,18 +362,26 @@ export async function attachWebInboxToSocket(
};
const summarizeGroupMeta = async (meta: GroupMetadata) => {
const participants =
(
await Promise.all(
meta.participants?.map(async (p) => {
const mapped = await resolveInboundJid(p.id);
return mapped ?? p.id;
}) ?? [],
)
).filter(Boolean) ?? [];
const participantEntries = await Promise.all(
meta.participants?.map(async (p) => {
const mapped = await resolveInboundJid(p.id);
return {
display: mapped ?? p.id,
mention: {
id: p.id,
lid: p.lid,
phoneNumber: p.phoneNumber,
e164: mapped,
} satisfies WhatsAppOutboundMentionParticipant,
};
}) ?? [],
);
const participants = participantEntries.map((entry) => entry.display).filter(Boolean);
const mentionParticipants = participantEntries.map((entry) => entry.mention);
return {
subject: meta.subject,
participants,
mentionParticipants,
expires: Date.now() + GROUP_META_TTL_MS,
};
};
@@ -384,7 +399,7 @@ export async function attachWebInboxToSocket(
return cached;
}
try {
const meta = await sock.groupMetadata(jid);
const meta = await (getCurrentSock() ?? sock).groupMetadata(jid);
const entry = await summarizeGroupMeta(meta);
rememberGroupMetadataCacheEntry(groupMetadataCache, jid, {
subject: entry.subject,
@@ -410,6 +425,43 @@ export async function attachWebInboxToSocket(
}
};
const resolveOutboundMentionsForGroup = async (
jid: string,
text: string,
): Promise<{ text: string; mentionedJids: string[] }> => {
if (!isGroupJid(jid) || !mayContainWhatsAppOutboundMention(text)) {
return { text, mentionedJids: [] };
}
const meta = await getGroupMeta(jid);
return resolveWhatsAppOutboundMentions({
chatJid: jid,
text,
participants: meta.mentionParticipants,
});
};
const applyOutboundMentionsToContent = async (
jid: string,
content: AnyMessageContent,
): Promise<AnyMessageContent> => {
if ("text" in content && typeof content.text === "string") {
const resolved = await resolveOutboundMentionsForGroup(jid, content.text);
return addWhatsAppOutboundMentionsToContent(
{ ...content, text: resolved.text } as AnyMessageContent,
resolved.mentionedJids,
);
}
const caption = (content as { caption?: unknown }).caption;
if (typeof caption === "string") {
const resolved = await resolveOutboundMentionsForGroup(jid, caption);
return addWhatsAppOutboundMentionsToContent(
{ ...content, caption: resolved.text } as AnyMessageContent,
resolved.mentionedJids,
);
}
return content;
};
type NormalizedInboundMessage = {
id?: string;
remoteJid: string;
@@ -632,14 +684,23 @@ export async function attachWebInboxToSocket(
}
};
const reply = async (text: string, options?: MiscMessageGenerationOptions) => {
const result = await sendTrackedMessage(chatJid, { text }, options);
const resolved = await resolveOutboundMentionsForGroup(chatJid, text);
const result = await sendTrackedMessage(
chatJid,
addWhatsAppOutboundMentionsToContent({ text: resolved.text }, resolved.mentionedJids),
options,
);
return normalizeWhatsAppSendResult(result, "text");
};
const sendMedia = async (
payload: AnyMessageContent,
options?: MiscMessageGenerationOptions,
) => {
const result = await sendTrackedMessage(chatJid, payload, options);
const result = await sendTrackedMessage(
chatJid,
await applyOutboundMentionsToContent(chatJid, payload),
options,
);
return normalizeWhatsAppSendResult(result, "media");
};
const timestamp = inbound.messageTimestampMs;
@@ -856,6 +917,7 @@ export async function attachWebInboxToSocket(
},
},
defaultAccountId: options.accountId,
resolveOutboundMentions: ({ jid, text }) => resolveOutboundMentionsForGroup(jid, text),
});
return {

View File

@@ -0,0 +1,133 @@
import { describe, expect, it } from "vitest";
import { resolveWhatsAppOutboundMentions } from "./outbound-mentions.js";
describe("resolveWhatsAppOutboundMentions", () => {
it("resolves phone-number tokens to WhatsApp participant JIDs", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "hi @+15551234567 and @15557654321",
participants: [{ id: "15551234567@s.whatsapp.net" }, { id: "15557654321@s.whatsapp.net" }],
}),
).toEqual({
text: "hi @+15551234567 and @15557654321",
mentionedJids: ["15551234567@s.whatsapp.net", "15557654321@s.whatsapp.net"],
});
});
it("rewrites phone-number tokens to LID mention text without device suffixes", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "ping @+5511976136970",
participants: [
{
id: "277038292303944:2@lid",
phoneNumber: "5511976136970@s.whatsapp.net",
},
],
}),
).toEqual({
text: "ping @277038292303944",
mentionedJids: ["277038292303944@lid"],
});
});
it("uses resolved E.164 metadata when LID participant records omit phoneNumber", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "ping @15551234567",
participants: [
{
id: "277038292303944@lid",
e164: "+15551234567",
},
],
}),
).toEqual({
text: "ping @277038292303944",
mentionedJids: ["277038292303944@lid"],
});
});
it("prefers explicit LID metadata over a phone JID id", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "ping @15551234567 and @277038292303944",
participants: [
{
id: "15551234567@s.whatsapp.net",
lid: "277038292303944@lid",
},
],
}),
).toEqual({
text: "ping @277038292303944 and @277038292303944",
mentionedJids: ["277038292303944@lid"],
});
});
it("uses bare digit tokens for LIDs before phone numbers when participant keys collide", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "ping @277038292303944 and @+277038292303944",
participants: [{ id: "277038292303944@s.whatsapp.net" }, { id: "277038292303944@lid" }],
}),
).toEqual({
text: "ping @277038292303944 and @+277038292303944",
mentionedJids: ["277038292303944@lid", "277038292303944@s.whatsapp.net"],
});
});
it("applies LID rewrites by match position while skipping code spans", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: [
"visible @+5511976136970",
"`inline @+5511976136970`",
"```",
"fenced @+5511976136970",
"```",
"again @+5511976136970",
].join("\n"),
participants: [
{
id: "277038292303944:9@lid",
phoneNumber: "5511976136970@s.whatsapp.net",
},
],
}),
).toEqual({
text: [
"visible @277038292303944",
"`inline @+5511976136970`",
"```",
"fenced @+5511976136970",
"```",
"again @277038292303944",
].join("\n"),
mentionedJids: ["277038292303944@lid"],
});
});
it("does not add mention metadata for direct chats or unmatched group participants", () => {
expect(
resolveWhatsAppOutboundMentions({
chatJid: "15551234567@s.whatsapp.net",
text: "hi @+15551234567",
participants: [{ id: "15551234567@s.whatsapp.net" }],
}),
).toEqual({ text: "hi @+15551234567", mentionedJids: [] });
expect(
resolveWhatsAppOutboundMentions({
chatJid: "120363000000000000@g.us",
text: "hi @+15551234567",
participants: [{ id: "15550000000@s.whatsapp.net" }],
}),
).toEqual({ text: "hi @+15551234567", mentionedJids: [] });
});
});

View File

@@ -0,0 +1,258 @@
import type { AnyMessageContent } from "@whiskeysockets/baileys";
export type WhatsAppOutboundMentionParticipant =
| string
| {
id?: string | null;
lid?: string | null;
phoneNumber?: string | null;
e164?: string | null;
};
export type WhatsAppOutboundMentionResolution = {
text: string;
mentionedJids: string[];
};
const CODE_FENCE_RE = /```[\s\S]*?```/g;
const INLINE_CODE_RE = /`[^`\n]+`/g;
const OUTBOUND_MENTION_RE = /@(\+?\d+)/g;
const KNOWN_USER_JID_RE = /^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted|lid|hosted\.lid|c\.us)$/i;
const PHONE_JID_DOMAIN_RE = /^(s\.whatsapp\.net|hosted|c\.us)$/i;
const LID_JID_DOMAIN_RE = /^(lid|hosted\.lid)$/i;
type TextRange = {
start: number;
end: number;
};
type MentionTarget = {
mentionJid: string;
replacementText?: string;
};
function isWhatsAppGroupJid(jid: string): boolean {
return jid.endsWith("@g.us");
}
export function mayContainWhatsAppOutboundMention(text: string): boolean {
return /@\+?\d/.test(text);
}
function collectCodeRanges(text: string): TextRange[] {
const ranges: TextRange[] = [];
for (const match of text.matchAll(CODE_FENCE_RE)) {
ranges.push({ start: match.index, end: match.index + match[0].length });
}
for (const match of text.matchAll(INLINE_CODE_RE)) {
const start = match.index;
if (ranges.some((range) => start >= range.start && start < range.end)) {
continue;
}
ranges.push({ start, end: start + match[0].length });
}
return ranges.toSorted((a, b) => a.start - b.start);
}
function isInRange(index: number, ranges: readonly TextRange[]): boolean {
return ranges.some((range) => index >= range.start && index < range.end);
}
function normalizeKnownUserJid(value: string): string | null {
const trimmed = value.replace(/^whatsapp:/i, "").trim();
const jidMatch = trimmed.match(KNOWN_USER_JID_RE);
if (jidMatch) {
const domain =
jidMatch[2].toLowerCase() === "c.us" ? "s.whatsapp.net" : jidMatch[2].toLowerCase();
return `${jidMatch[1]}@${domain}`;
}
const digits = trimmed.startsWith("+")
? trimmed.replace(/\D/g, "")
: /^\d+$/.test(trimmed)
? trimmed
: "";
return digits ? `${digits}@s.whatsapp.net` : null;
}
function extractKnownJidParts(value: string): { user: string; domain: string } | null {
const normalized = normalizeKnownUserJid(value);
if (!normalized) {
return null;
}
const match = normalized.match(/^(\d+)@(.+)$/);
return match ? { user: match[1], domain: match[2] } : null;
}
function extractPhoneDigits(value: string | null | undefined): string | null {
if (!value) {
return null;
}
const trimmed = value.replace(/^whatsapp:/i, "").trim();
if (trimmed.startsWith("+") || /^\d+$/.test(trimmed)) {
const digits = trimmed.replace(/\D/g, "");
return digits || null;
}
const parts = extractKnownJidParts(trimmed);
return parts && PHONE_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
}
function extractLidDigits(value: string | null | undefined): string | null {
if (!value) {
return null;
}
const parts = extractKnownJidParts(value);
return parts && LID_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
}
function isLidJid(jid: string): boolean {
const parts = extractKnownJidParts(jid);
return Boolean(parts && LID_JID_DOMAIN_RE.test(parts.domain));
}
function lidReplacementText(jid: string): string | undefined {
const parts = extractKnownJidParts(jid);
if (!parts || !LID_JID_DOMAIN_RE.test(parts.domain)) {
return undefined;
}
return `@${parts.user}`;
}
function participantValues(participant: WhatsAppOutboundMentionParticipant): {
id?: string | null;
lid?: string | null;
phoneNumber?: string | null;
e164?: string | null;
} {
return typeof participant === "string" ? { id: participant } : participant;
}
function chooseMentionJid(participant: WhatsAppOutboundMentionParticipant): string | null {
const values = participantValues(participant);
const idJid = normalizeKnownUserJid(values.id ?? "");
const lidJid = normalizeKnownUserJid(values.lid ?? "");
return (
(idJid && isLidJid(idJid) ? idJid : null) ??
(lidJid && isLidJid(lidJid) ? lidJid : null) ??
idJid ??
lidJid ??
normalizeKnownUserJid(values.phoneNumber ?? "") ??
normalizeKnownUserJid(values.e164 ?? "")
);
}
function buildMentionTargetMaps(participants: readonly WhatsAppOutboundMentionParticipant[]): {
byPhone: Map<string, MentionTarget>;
byLid: Map<string, MentionTarget>;
} {
const byPhone = new Map<string, MentionTarget>();
const byLid = new Map<string, MentionTarget>();
for (const participant of participants) {
const mentionJid = chooseMentionJid(participant);
if (!mentionJid) {
continue;
}
const target = {
mentionJid,
...(isLidJid(mentionJid) ? { replacementText: lidReplacementText(mentionJid) } : {}),
};
const values = participantValues(participant);
for (const value of [values.id, values.phoneNumber, values.e164]) {
const digits = extractPhoneDigits(value);
if (digits && !byPhone.has(digits)) {
byPhone.set(digits, target);
}
}
for (const value of [values.id, values.lid]) {
const digits = extractLidDigits(value);
if (digits && !byLid.has(digits)) {
byLid.set(digits, target);
}
}
}
return { byPhone, byLid };
}
function shouldSkipMentionAt(
text: string,
index: number,
codeRanges: readonly TextRange[],
): boolean {
if (isInRange(index, codeRanges)) {
return true;
}
const previous = index > 0 ? text[index - 1] : "";
return Boolean(previous && /[\w@]/.test(previous));
}
export function resolveWhatsAppOutboundMentions(params: {
chatJid: string;
text: string;
participants?: readonly WhatsAppOutboundMentionParticipant[];
}): WhatsAppOutboundMentionResolution {
if (
!isWhatsAppGroupJid(params.chatJid) ||
!mayContainWhatsAppOutboundMention(params.text) ||
!params.participants?.length
) {
return { text: params.text, mentionedJids: [] };
}
const { byPhone, byLid } = buildMentionTargetMaps(params.participants);
if (byPhone.size === 0 && byLid.size === 0) {
return { text: params.text, mentionedJids: [] };
}
const codeRanges = collectCodeRanges(params.text);
const replacements: Array<{ start: number; end: number; text: string }> = [];
const mentionedJids: string[] = [];
const seenMentionJids = new Set<string>();
for (const match of params.text.matchAll(OUTBOUND_MENTION_RE)) {
const start = match.index;
if (shouldSkipMentionAt(params.text, start, codeRanges)) {
continue;
}
const token = match[0];
const digits = match[1].replace(/\D/g, "");
const target = token.startsWith("@+")
? (byPhone.get(digits) ?? byLid.get(digits))
: (byLid.get(digits) ?? byPhone.get(digits));
if (!target) {
continue;
}
if (!seenMentionJids.has(target.mentionJid)) {
seenMentionJids.add(target.mentionJid);
mentionedJids.push(target.mentionJid);
}
if (target.replacementText && target.replacementText !== token) {
replacements.push({
start,
end: start + token.length,
text: target.replacementText,
});
}
}
if (replacements.length === 0) {
return { text: params.text, mentionedJids };
}
let text = "";
let cursor = 0;
for (const replacement of replacements) {
text += params.text.slice(cursor, replacement.start);
text += replacement.text;
cursor = replacement.end;
}
text += params.text.slice(cursor);
return { text, mentionedJids };
}
export function addWhatsAppOutboundMentionsToContent(
content: AnyMessageContent,
mentionedJids: readonly string[],
): AnyMessageContent {
return mentionedJids.length > 0
? ({ ...content, mentions: [...mentionedJids] } as AnyMessageContent)
: content;
}

View File

@@ -4,6 +4,7 @@ import type {
WAMessage,
} from "@whiskeysockets/baileys";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveWhatsAppOutboundMentions } from "./outbound-mentions.js";
import { createWebSendApi } from "./send-api.js";
const recordChannelActivity = vi.hoisted(() => vi.fn());
@@ -86,6 +87,31 @@ describe("createWebSendApi", () => {
});
});
it("adds native mention metadata to group text sends", async () => {
api = createWebSendApi({
sock: { sendMessage, sendPresenceUpdate },
defaultAccountId: "main",
resolveOutboundMentions: ({ jid, text }) =>
resolveWhatsAppOutboundMentions({
chatJid: jid,
text,
participants: [
{
id: "277038292303944:4@lid",
phoneNumber: "5511976136970@s.whatsapp.net",
},
],
}),
});
await api.sendMessage("120363000000000000@g.us", "ping @+5511976136970");
expect(sendMessage).toHaveBeenCalledWith("120363000000000000@g.us", {
text: "ping @277038292303944",
mentions: ["277038292303944@lid"],
});
});
it("supports image media with caption", async () => {
const payload = Buffer.from("img");
await api.sendMessage("+1555", "cap", payload, "image/jpeg");
@@ -99,6 +125,32 @@ describe("createWebSendApi", () => {
);
});
it("adds native mention metadata to group media captions", async () => {
api = createWebSendApi({
sock: { sendMessage, sendPresenceUpdate },
defaultAccountId: "main",
resolveOutboundMentions: ({ jid, text }) =>
resolveWhatsAppOutboundMentions({
chatJid: jid,
text,
participants: [{ id: "15551234567@s.whatsapp.net" }],
}),
});
const payload = Buffer.from("img");
await api.sendMessage("120363000000000000@g.us", "cap @15551234567", payload, "image/jpeg");
expect(sendMessage).toHaveBeenCalledWith(
"120363000000000000@g.us",
expect.objectContaining({
image: payload,
caption: "cap @15551234567",
mimetype: "image/jpeg",
mentions: ["15551234567@s.whatsapp.net"],
}),
);
});
it("supports audio as push-to-talk voice note", async () => {
const payload = Buffer.from("aud");
await api.sendMessage("+1555", "", payload, "audio/ogg", { accountId: "alt" });

View File

@@ -8,6 +8,10 @@ import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runt
import { isWhatsAppNewsletterJid } from "../normalize.js";
import { buildQuotedMessageOptions } from "../quoted-message.js";
import { toWhatsappJid } from "../text-runtime.js";
import {
addWhatsAppOutboundMentionsToContent,
type WhatsAppOutboundMentionResolution,
} from "./outbound-mentions.js";
import {
combineWhatsAppSendResults,
normalizeWhatsAppSendResult,
@@ -33,7 +37,19 @@ export function createWebSendApi(params: {
sendPresenceUpdate: (presence: WAPresence, jid?: string) => Promise<unknown>;
};
defaultAccountId: string;
resolveOutboundMentions?: (params: {
jid: string;
text: string;
}) => Promise<WhatsAppOutboundMentionResolution> | WhatsAppOutboundMentionResolution;
}) {
const resolveMentions = async (
jid: string,
text: string,
): Promise<WhatsAppOutboundMentionResolution> =>
params.resolveOutboundMentions
? await params.resolveOutboundMentions({ jid, text })
: { text, mentionedJids: [] };
return {
sendMessage: async (
to: string,
@@ -47,11 +63,17 @@ export function createWebSendApi(params: {
if (mediaBuffer) {
mediaType ??= "application/octet-stream";
}
const shouldSendAudioText = Boolean(
mediaBuffer && mediaType?.startsWith("audio/") && text.trim(),
);
const resolvedPayloadText = shouldSendAudioText
? { text, mentionedJids: [] }
: await resolveMentions(jid, text);
if (mediaBuffer && mediaType) {
if (mediaType.startsWith("image/")) {
payload = {
image: mediaBuffer,
caption: text || undefined,
caption: resolvedPayloadText.text || undefined,
mimetype: mediaType,
};
} else if (mediaType.startsWith("audio/")) {
@@ -60,7 +82,7 @@ export function createWebSendApi(params: {
const gifPlayback = sendOptions?.gifPlayback;
payload = {
video: mediaBuffer,
caption: text || undefined,
caption: resolvedPayloadText.text || undefined,
mimetype: mediaType,
...(gifPlayback ? { gifPlayback: true } : {}),
};
@@ -69,13 +91,14 @@ export function createWebSendApi(params: {
payload = {
document: mediaBuffer,
fileName,
caption: text || undefined,
caption: resolvedPayloadText.text || undefined,
mimetype: mediaType,
};
}
} else {
payload = { text };
payload = { text: resolvedPayloadText.text };
}
payload = addWhatsAppOutboundMentionsToContent(payload, resolvedPayloadText.mentionedJids);
const quotedOpts = buildQuotedMessageOptions({
messageId: sendOptions?.quotedMessageKey?.id,
remoteJid: sendOptions?.quotedMessageKey?.remoteJid,
@@ -87,8 +110,12 @@ export function createWebSendApi(params: {
? await params.sock.sendMessage(jid, payload, quotedOpts)
: await params.sock.sendMessage(jid, payload);
const results = [normalizeWhatsAppSendResult(result, mediaBuffer ? "media" : "text")];
if (mediaBuffer && mediaType?.startsWith("audio/") && text.trim()) {
const textPayload: AnyMessageContent = { text };
if (shouldSendAudioText) {
const resolvedAudioText = await resolveMentions(jid, text);
const textPayload = addWhatsAppOutboundMentionsToContent(
{ text: resolvedAudioText.text },
resolvedAudioText.mentionedJids,
);
const textResult = quotedOpts
? await params.sock.sendMessage(jid, textPayload, quotedOpts)
: await params.sock.sendMessage(jid, textPayload);