Skip to content

Commit eebcb10

Browse files
mcaxtrshakkernerd
authored andcommitted
refactor(whatsapp): read inbound contexts in auto reply
1 parent b5295a6 commit eebcb10

19 files changed

Lines changed: 318 additions & 231 deletions

extensions/whatsapp/src/auto-reply/deliver-reply.ts

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
1515
import type { WhatsAppSendResult } from "../inbound/send-result.js";
1616
import { listWhatsAppSendResultMessageIds } from "../inbound/send-result.js";
17+
import type { WebInboundMessage } from "../inbound/types.js";
1718
import { loadWebMedia } from "../media.js";
1819
import {
1920
type DeliverableWhatsAppOutboundPayload,
@@ -28,8 +29,7 @@ import { formatError } from "../session.js";
2829
import { convertMarkdownTables } from "../text-runtime.js";
2930
import { markdownToWhatsApp } from "../text-runtime.js";
3031
import { whatsappOutboundLog } from "./loggers.js";
31-
import type { WebInboundMsg } from "./types.js";
32-
import { elide } from "./util.js";
32+
import { elide, markWhatsAppVisibleDeliveryError } from "./util.js";
3333

3434
export type WhatsAppReplyDeliveryResult = {
3535
results: WhatsAppSendResult[];
@@ -84,24 +84,10 @@ function createWhatsAppReplyDeliveryReceipt(
8484
});
8585
}
8686

87-
function markWhatsAppVisibleDeliveryError(error: unknown): unknown {
88-
if (typeof error === "object" && error !== null && !Array.isArray(error)) {
89-
try {
90-
Object.assign(error, { sentBeforeError: true, visibleReplySent: true });
91-
return error;
92-
} catch {
93-
// Fall back to a wrapper when a platform error object is non-extensible.
94-
}
95-
}
96-
const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
97-
Object.assign(visibleError, { sentBeforeError: true, visibleReplySent: true });
98-
return visibleError;
99-
}
100-
10187
export async function deliverWebReply(params: {
10288
replyResult: ReplyPayload;
10389
normalizedReplyResult?: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
104-
msg: WebInboundMsg;
90+
msg: WebInboundMessage;
10591
mediaLocalRoots?: readonly string[];
10692
maxMediaBytes: number;
10793
textLimit: number;
@@ -151,15 +137,20 @@ export async function deliverWebReply(params: {
151137
if (!replyResult.replyToId) {
152138
return undefined;
153139
}
154-
// Use replyToId (not msg.id) so batched payloads quote the correct
140+
// Use replyToId (not msg.event.id) so batched payloads quote the correct
155141
// per-message target. Look up cached metadata for the specific
156-
// message being quoted — msg.body may be a combined batch body.
157-
const cached = lookupInboundMessageMeta(msg.accountId, msg.chatId, replyResult.replyToId);
142+
// message being quoted — msg.payload.body may be a combined batch body.
143+
const cached = lookupInboundMessageMeta(
144+
msg.accountId,
145+
msg.platform.chatJid,
146+
replyResult.replyToId,
147+
);
158148
return buildQuotedMessageOptions({
159149
messageId: replyResult.replyToId,
160-
remoteJid: msg.chatId,
150+
remoteJid: msg.platform.chatJid,
161151
fromMe: cached?.fromMe ?? false,
162-
participant: cached?.participant ?? (msg.chatType === "group" ? msg.senderJid : undefined),
152+
participant:
153+
cached?.participant ?? (msg.chatType === "group" ? msg.platform.senderJid : undefined),
163154
messageText: cached?.body ?? "",
164155
});
165156
};
@@ -189,7 +180,7 @@ export async function deliverWebReply(params: {
189180
for (const [index, chunk] of textChunks.entries()) {
190181
const chunkStarted = Date.now();
191182
const quote = getQuote();
192-
rememberSendResult(await sendWithRetry(() => msg.reply(chunk, quote), "text"));
183+
rememberSendResult(await sendWithRetry(() => msg.platform.reply(chunk, quote), "text"));
193184
if (!skipLog) {
194185
const durationMs = Date.now() - chunkStarted;
195186
whatsappOutboundLog.debug(
@@ -199,10 +190,10 @@ export async function deliverWebReply(params: {
199190
}
200191
const delivery = finishDelivery();
201192
const logPayload = {
202-
correlationId: msg.id ?? newConnectionId(),
193+
correlationId: msg.event.id ?? newConnectionId(),
203194
connectionId: connectionId ?? null,
204195
to: msg.from,
205-
from: msg.to,
196+
from: msg.platform.recipientJid,
206197
text: elide(replyResult.text, 240),
207198
mediaUrl: null,
208199
mediaSizeBytes: null,
@@ -243,7 +234,7 @@ export async function deliverWebReply(params: {
243234
rememberSendResult(
244235
await sendWithRetry(
245236
() =>
246-
msg.sendMedia(
237+
msg.platform.sendMedia(
247238
{
248239
image: media.buffer,
249240
caption,
@@ -259,7 +250,7 @@ export async function deliverWebReply(params: {
259250
rememberSendResult(
260251
await sendWithRetry(
261252
() =>
262-
msg.sendMedia(
253+
msg.platform.sendMedia(
263254
{
264255
audio: media.buffer,
265256
ptt: true,
@@ -272,15 +263,15 @@ export async function deliverWebReply(params: {
272263
);
273264
if (caption) {
274265
rememberSendResult(
275-
await sendWithRetry(() => msg.reply(caption, quote), "media:audio-text"),
266+
await sendWithRetry(() => msg.platform.reply(caption, quote), "media:audio-text"),
276267
);
277268
}
278269
} else if (media.kind === "video") {
279270
const quote = getQuote();
280271
rememberSendResult(
281272
await sendWithRetry(
282273
() =>
283-
msg.sendMedia(
274+
msg.platform.sendMedia(
284275
{
285276
video: media.buffer,
286277
caption,
@@ -296,7 +287,7 @@ export async function deliverWebReply(params: {
296287
rememberSendResult(
297288
await sendWithRetry(
298289
() =>
299-
msg.sendMedia(
290+
msg.platform.sendMedia(
300291
{
301292
document: media.buffer,
302293
fileName: media.fileName,
@@ -314,10 +305,10 @@ export async function deliverWebReply(params: {
314305
);
315306
replyLogger.info(
316307
{
317-
correlationId: msg.id ?? newConnectionId(),
308+
correlationId: msg.event.id ?? newConnectionId(),
318309
connectionId: connectionId ?? null,
319310
to: msg.from,
320-
from: msg.to,
311+
from: msg.platform.recipientJid,
321312
text: caption ?? null,
322313
mediaUrl,
323314
mediaSizeBytes: media.buffer.length,
@@ -341,14 +332,19 @@ export async function deliverWebReply(params: {
341332
}
342333
whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
343334
rememberSendResult(
344-
await sendWithRetry(() => msg.reply(fallbackText, getQuote()), "media:fallback-text"),
335+
await sendWithRetry(
336+
() => msg.platform.reply(fallbackText, getQuote()),
337+
"media:fallback-text",
338+
),
345339
);
346340
},
347341
});
348342

349343
// Remaining text chunks after media
350344
for (const chunk of remainingText) {
351-
rememberSendResult(await sendWithRetry(() => msg.reply(chunk, getQuote()), "media:text"));
345+
rememberSendResult(
346+
await sendWithRetry(() => msg.platform.reply(chunk, getQuote()), "media:text"),
347+
);
352348
}
353349
return finishDelivery();
354350
}

extensions/whatsapp/src/auto-reply/mentions.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
identitiesOverlap,
1212
type WhatsAppIdentity,
1313
} from "../identity.js";
14+
import type { WebInboundMessage } from "../inbound/types.js";
1415
import { isWhatsAppGroupJid } from "../normalize-target.js";
1516
import { isSelfChatMode, normalizeE164 } from "../text-runtime.js";
16-
import type { WebInboundMsg } from "./types.js";
1717

1818
export type MentionConfig = {
1919
mentionRegexes: RegExp[];
@@ -35,14 +35,14 @@ export function buildMentionConfig(
3535
return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
3636
}
3737

38-
export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
38+
export function resolveMentionTargets(msg: WebInboundMessage, authDir?: string): MentionTargets {
3939
const normalizedMentions = getMentionIdentities(msg, authDir);
4040
const self = getSelfIdentity(msg, authDir);
4141
return { normalizedMentions, self };
4242
}
4343

4444
export function isBotMentionedFromTargets(
45-
msg: WebInboundMsg,
45+
msg: WebInboundMessage,
4646
mentionCfg: MentionConfig,
4747
targets: MentionTargets,
4848
): boolean {
@@ -78,7 +78,7 @@ export function isBotMentionedFromTargets(
7878
} else if (hasMentions && isSelfChat) {
7979
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in self-chat triggers the bot.
8080
}
81-
const bodyClean = clean(msg.body);
81+
const bodyClean = clean(msg.payload.body);
8282
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) {
8383
return true;
8484
}
@@ -91,7 +91,7 @@ export function isBotMentionedFromTargets(
9191
if (bodyDigits.includes(selfDigits)) {
9292
return true;
9393
}
94-
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
94+
const bodyNoSpace = msg.payload.body.replace(/[\s-]/g, "");
9595
const pattern = new RegExp(`\\+?${selfDigits}`, "i");
9696
if (pattern.test(bodyNoSpace)) {
9797
return true;
@@ -103,23 +103,23 @@ export function isBotMentionedFromTargets(
103103
}
104104

105105
export function debugMention(
106-
msg: WebInboundMsg,
106+
msg: WebInboundMessage,
107107
mentionCfg: MentionConfig,
108108
authDir?: string,
109109
): { wasMentioned: boolean; details: Record<string, unknown> } {
110110
const mentionTargets = resolveMentionTargets(msg, authDir);
111111
const result = isBotMentionedFromTargets(msg, mentionCfg, mentionTargets);
112112
const details = {
113113
from: msg.from,
114-
body: msg.body,
115-
bodyClean: normalizeMentionText(msg.body),
116-
mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
114+
body: msg.payload.body,
115+
bodyClean: normalizeMentionText(msg.payload.body),
116+
mentionedJids: msg.group?.mentions?.jids ?? null,
117117
normalizedMentionedJids: mentionTargets.normalizedMentions.length
118118
? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity))
119119
: null,
120-
selfJid: msg.self?.jid ?? msg.selfJid ?? null,
121-
selfLid: msg.self?.lid ?? msg.selfLid ?? null,
122-
selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
120+
selfJid: msg.platform.self?.jid ?? msg.platform.selfJid ?? null,
121+
selfLid: msg.platform.self?.lid ?? msg.platform.selfLid ?? null,
122+
selfE164: msg.platform.self?.e164 ?? msg.platform.selfE164 ?? null,
123123
resolvedSelf: mentionTargets.self,
124124
};
125125
return { wasMentioned: result, details };

extensions/whatsapp/src/auto-reply/monitor.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import {
2626
type ManagedWhatsAppListener,
2727
} from "../connection-controller.js";
2828
import { resolveWhatsAppInboundPolicy } from "../inbound-policy.js";
29+
import { normalizeWebInboundMessage } from "../inbound/message-aliases.js";
2930
import { attachWebInboxToSocket, type WhatsAppGroupMetadataCache } from "../inbound/monitor.js";
31+
import type { WebInboundMessageInput } from "../inbound/types.js";
3032
import {
3133
newConnectionId,
3234
resolveHeartbeatSeconds,
@@ -42,7 +44,7 @@ import { createWebChannelStatusController } from "./monitor-state.js";
4244
import { createEchoTracker } from "./monitor/echo.js";
4345
import { formatWhatsAppInboundListeningLog } from "./monitor/listener-log.js";
4446
import { createWebOnMessageHandler } from "./monitor/on-message.js";
45-
import type { WebInboundMsg, WebMonitorTuning } from "./types.js";
47+
import type { WebMonitorTuning } from "./types.js";
4648
import { isLikelyWhatsAppCryptoError } from "./util.js";
4749

4850
function isNonRetryableWebCloseStatus(statusCode: unknown): boolean {
@@ -286,17 +288,18 @@ export async function monitorWebChannel(
286288
accountId: account.accountId,
287289
}),
288290
});
289-
const shouldDebounce = (msg: WebInboundMsg) => {
290-
if (msg.mediaPath || msg.mediaType) {
291+
const shouldDebounce = (msg: WebInboundMessageInput) => {
292+
const normalized = normalizeWebInboundMessage(msg);
293+
if (normalized.payload.media?.path || normalized.payload.media?.type) {
291294
return false;
292295
}
293-
if (msg.location) {
296+
if (normalized.payload.location) {
294297
return false;
295298
}
296-
if (msg.replyToId || msg.replyToBody) {
299+
if (normalized.quote?.id || normalized.quote?.body) {
297300
return false;
298301
}
299-
return !isControlCommandMessage(msg.body, cfg);
302+
return !isControlCommandMessage(normalized.payload.body, cfg);
300303
};
301304

302305
let connection;
@@ -337,11 +340,12 @@ export async function monitorWebChannel(
337340
disconnectRetryPolicy: reconnectPolicy,
338341
disconnectRetryAbortSignal: controller.getDisconnectRetryAbortSignal(),
339342
groupMetadataCache,
340-
onMessage: async (msg: WebInboundMsg) => {
343+
onMessage: async (msg: WebInboundMessageInput) => {
344+
const normalized = normalizeWebInboundMessage(msg);
341345
const inboundAt = Date.now();
342346
controller.noteInbound(inboundAt);
343347
statusController.noteInbound(inboundAt);
344-
await onMessage(msg);
348+
await onMessage(normalized);
345349
},
346350
sock,
347351
})) as ManagedWhatsAppListener;

extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ import {
77
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
88
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
99
import { getSenderIdentity } from "../../identity.js";
10+
import type { WebInboundMessage } from "../../inbound/types.js";
1011
import { resolveWhatsAppReactionLevel } from "../../reaction-level.js";
1112
import { sendReactionWhatsApp } from "../../send.js";
1213
import { formatError } from "../../session.js";
13-
import type { WebInboundMsg } from "../types.js";
1414
import { resolveWhatsAppAckEmoji } from "./ack-emoji.js";
1515
import { resolveGroupActivationFor } from "./group-activation.js";
1616

1717
export async function maybeSendAckReaction(params: {
1818
cfg: OpenClawConfig;
19-
msg: WebInboundMsg;
19+
msg: WebInboundMessage;
2020
agentId: string;
2121
sessionKey: string;
2222
conversationId: string;
@@ -25,7 +25,7 @@ export async function maybeSendAckReaction(params: {
2525
info: (obj: unknown, msg: string) => void;
2626
warn: (obj: unknown, msg: string) => void;
2727
}): Promise<AckReactionHandle | null> {
28-
if (!params.msg.id) {
28+
if (!params.msg.event.id) {
2929
return null;
3030
}
3131

@@ -75,7 +75,7 @@ export async function maybeSendAckReaction(params: {
7575
}
7676

7777
params.info(
78-
{ chatId: params.msg.chatId, messageId: params.msg.id, emoji },
78+
{ chatId: params.msg.platform.chatJid, messageId: params.msg.event.id, emoji },
7979
"sending ack reaction",
8080
);
8181
const sender = getSenderIdentity(params.msg);
@@ -88,18 +88,27 @@ export async function maybeSendAckReaction(params: {
8888
};
8989
return createAckReactionHandle({
9090
ackReactionValue: emoji,
91-
send: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, emoji, reactionOptions),
92-
remove: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, "", reactionOptions),
91+
send: () =>
92+
sendReactionWhatsApp(
93+
params.msg.platform.chatJid,
94+
params.msg.event.id!,
95+
emoji,
96+
reactionOptions,
97+
),
98+
remove: () =>
99+
sendReactionWhatsApp(params.msg.platform.chatJid, params.msg.event.id!, "", reactionOptions),
93100
onSendError: (err) => {
94101
params.warn(
95102
{
96103
error: formatError(err),
97-
chatId: params.msg.chatId,
98-
messageId: params.msg.id,
104+
chatId: params.msg.platform.chatJid,
105+
messageId: params.msg.event.id,
99106
},
100107
"failed to send ack reaction",
101108
);
102-
logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`);
109+
logVerbose(
110+
`WhatsApp ack reaction failed for chat ${params.msg.platform.chatJid}: ${formatError(err)}`,
111+
);
103112
},
104113
});
105114
}

0 commit comments

Comments
 (0)