mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 02:22:46 +00:00
refactor(whatsapp): read inbound contexts in auto reply
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { WhatsAppSendResult } from "../inbound/send-result.js";
|
||||
import { listWhatsAppSendResultMessageIds } from "../inbound/send-result.js";
|
||||
import type { WebInboundMessage } from "../inbound/types.js";
|
||||
import { loadWebMedia } from "../media.js";
|
||||
import {
|
||||
type DeliverableWhatsAppOutboundPayload,
|
||||
@@ -28,8 +29,7 @@ import { formatError } from "../session.js";
|
||||
import { convertMarkdownTables } from "../text-runtime.js";
|
||||
import { markdownToWhatsApp } from "../text-runtime.js";
|
||||
import { whatsappOutboundLog } from "./loggers.js";
|
||||
import type { WebInboundMsg } from "./types.js";
|
||||
import { elide } from "./util.js";
|
||||
import { elide, markWhatsAppVisibleDeliveryError } from "./util.js";
|
||||
|
||||
export type WhatsAppReplyDeliveryResult = {
|
||||
results: WhatsAppSendResult[];
|
||||
@@ -84,24 +84,10 @@ function createWhatsAppReplyDeliveryReceipt(
|
||||
});
|
||||
}
|
||||
|
||||
function markWhatsAppVisibleDeliveryError(error: unknown): unknown {
|
||||
if (typeof error === "object" && error !== null && !Array.isArray(error)) {
|
||||
try {
|
||||
Object.assign(error, { sentBeforeError: true, visibleReplySent: true });
|
||||
return error;
|
||||
} catch {
|
||||
// Fall back to a wrapper when a platform error object is non-extensible.
|
||||
}
|
||||
}
|
||||
const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
|
||||
Object.assign(visibleError, { sentBeforeError: true, visibleReplySent: true });
|
||||
return visibleError;
|
||||
}
|
||||
|
||||
export async function deliverWebReply(params: {
|
||||
replyResult: ReplyPayload;
|
||||
normalizedReplyResult?: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
maxMediaBytes: number;
|
||||
textLimit: number;
|
||||
@@ -151,15 +137,20 @@ export async function deliverWebReply(params: {
|
||||
if (!replyResult.replyToId) {
|
||||
return undefined;
|
||||
}
|
||||
// Use replyToId (not msg.id) so batched payloads quote the correct
|
||||
// Use replyToId (not msg.event.id) so batched payloads quote the correct
|
||||
// per-message target. Look up cached metadata for the specific
|
||||
// message being quoted — msg.body may be a combined batch body.
|
||||
const cached = lookupInboundMessageMeta(msg.accountId, msg.chatId, replyResult.replyToId);
|
||||
// message being quoted — msg.payload.body may be a combined batch body.
|
||||
const cached = lookupInboundMessageMeta(
|
||||
msg.accountId,
|
||||
msg.platform.chatJid,
|
||||
replyResult.replyToId,
|
||||
);
|
||||
return buildQuotedMessageOptions({
|
||||
messageId: replyResult.replyToId,
|
||||
remoteJid: msg.chatId,
|
||||
remoteJid: msg.platform.chatJid,
|
||||
fromMe: cached?.fromMe ?? false,
|
||||
participant: cached?.participant ?? (msg.chatType === "group" ? msg.senderJid : undefined),
|
||||
participant:
|
||||
cached?.participant ?? (msg.chatType === "group" ? msg.platform.senderJid : undefined),
|
||||
messageText: cached?.body ?? "",
|
||||
});
|
||||
};
|
||||
@@ -189,7 +180,7 @@ export async function deliverWebReply(params: {
|
||||
for (const [index, chunk] of textChunks.entries()) {
|
||||
const chunkStarted = Date.now();
|
||||
const quote = getQuote();
|
||||
rememberSendResult(await sendWithRetry(() => msg.reply(chunk, quote), "text"));
|
||||
rememberSendResult(await sendWithRetry(() => msg.platform.reply(chunk, quote), "text"));
|
||||
if (!skipLog) {
|
||||
const durationMs = Date.now() - chunkStarted;
|
||||
whatsappOutboundLog.debug(
|
||||
@@ -199,10 +190,10 @@ export async function deliverWebReply(params: {
|
||||
}
|
||||
const delivery = finishDelivery();
|
||||
const logPayload = {
|
||||
correlationId: msg.id ?? newConnectionId(),
|
||||
correlationId: msg.event.id ?? newConnectionId(),
|
||||
connectionId: connectionId ?? null,
|
||||
to: msg.from,
|
||||
from: msg.to,
|
||||
from: msg.platform.recipientJid,
|
||||
text: elide(replyResult.text, 240),
|
||||
mediaUrl: null,
|
||||
mediaSizeBytes: null,
|
||||
@@ -243,7 +234,7 @@ export async function deliverWebReply(params: {
|
||||
rememberSendResult(
|
||||
await sendWithRetry(
|
||||
() =>
|
||||
msg.sendMedia(
|
||||
msg.platform.sendMedia(
|
||||
{
|
||||
image: media.buffer,
|
||||
caption,
|
||||
@@ -259,7 +250,7 @@ export async function deliverWebReply(params: {
|
||||
rememberSendResult(
|
||||
await sendWithRetry(
|
||||
() =>
|
||||
msg.sendMedia(
|
||||
msg.platform.sendMedia(
|
||||
{
|
||||
audio: media.buffer,
|
||||
ptt: true,
|
||||
@@ -272,7 +263,7 @@ export async function deliverWebReply(params: {
|
||||
);
|
||||
if (caption) {
|
||||
rememberSendResult(
|
||||
await sendWithRetry(() => msg.reply(caption, quote), "media:audio-text"),
|
||||
await sendWithRetry(() => msg.platform.reply(caption, quote), "media:audio-text"),
|
||||
);
|
||||
}
|
||||
} else if (media.kind === "video") {
|
||||
@@ -280,7 +271,7 @@ export async function deliverWebReply(params: {
|
||||
rememberSendResult(
|
||||
await sendWithRetry(
|
||||
() =>
|
||||
msg.sendMedia(
|
||||
msg.platform.sendMedia(
|
||||
{
|
||||
video: media.buffer,
|
||||
caption,
|
||||
@@ -296,7 +287,7 @@ export async function deliverWebReply(params: {
|
||||
rememberSendResult(
|
||||
await sendWithRetry(
|
||||
() =>
|
||||
msg.sendMedia(
|
||||
msg.platform.sendMedia(
|
||||
{
|
||||
document: media.buffer,
|
||||
fileName: media.fileName,
|
||||
@@ -314,10 +305,10 @@ export async function deliverWebReply(params: {
|
||||
);
|
||||
replyLogger.info(
|
||||
{
|
||||
correlationId: msg.id ?? newConnectionId(),
|
||||
correlationId: msg.event.id ?? newConnectionId(),
|
||||
connectionId: connectionId ?? null,
|
||||
to: msg.from,
|
||||
from: msg.to,
|
||||
from: msg.platform.recipientJid,
|
||||
text: caption ?? null,
|
||||
mediaUrl,
|
||||
mediaSizeBytes: media.buffer.length,
|
||||
@@ -341,14 +332,19 @@ export async function deliverWebReply(params: {
|
||||
}
|
||||
whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
|
||||
rememberSendResult(
|
||||
await sendWithRetry(() => msg.reply(fallbackText, getQuote()), "media:fallback-text"),
|
||||
await sendWithRetry(
|
||||
() => msg.platform.reply(fallbackText, getQuote()),
|
||||
"media:fallback-text",
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Remaining text chunks after media
|
||||
for (const chunk of remainingText) {
|
||||
rememberSendResult(await sendWithRetry(() => msg.reply(chunk, getQuote()), "media:text"));
|
||||
rememberSendResult(
|
||||
await sendWithRetry(() => msg.platform.reply(chunk, getQuote()), "media:text"),
|
||||
);
|
||||
}
|
||||
return finishDelivery();
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
identitiesOverlap,
|
||||
type WhatsAppIdentity,
|
||||
} from "../identity.js";
|
||||
import type { WebInboundMessage } from "../inbound/types.js";
|
||||
import { isWhatsAppGroupJid } from "../normalize-target.js";
|
||||
import { isSelfChatMode, normalizeE164 } from "../text-runtime.js";
|
||||
import type { WebInboundMsg } from "./types.js";
|
||||
|
||||
export type MentionConfig = {
|
||||
mentionRegexes: RegExp[];
|
||||
@@ -35,14 +35,14 @@ export function buildMentionConfig(
|
||||
return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
|
||||
}
|
||||
|
||||
export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
|
||||
export function resolveMentionTargets(msg: WebInboundMessage, authDir?: string): MentionTargets {
|
||||
const normalizedMentions = getMentionIdentities(msg, authDir);
|
||||
const self = getSelfIdentity(msg, authDir);
|
||||
return { normalizedMentions, self };
|
||||
}
|
||||
|
||||
export function isBotMentionedFromTargets(
|
||||
msg: WebInboundMsg,
|
||||
msg: WebInboundMessage,
|
||||
mentionCfg: MentionConfig,
|
||||
targets: MentionTargets,
|
||||
): boolean {
|
||||
@@ -78,7 +78,7 @@ export function isBotMentionedFromTargets(
|
||||
} else if (hasMentions && isSelfChat) {
|
||||
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in self-chat triggers the bot.
|
||||
}
|
||||
const bodyClean = clean(msg.body);
|
||||
const bodyClean = clean(msg.payload.body);
|
||||
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) {
|
||||
return true;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export function isBotMentionedFromTargets(
|
||||
if (bodyDigits.includes(selfDigits)) {
|
||||
return true;
|
||||
}
|
||||
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
|
||||
const bodyNoSpace = msg.payload.body.replace(/[\s-]/g, "");
|
||||
const pattern = new RegExp(`\\+?${selfDigits}`, "i");
|
||||
if (pattern.test(bodyNoSpace)) {
|
||||
return true;
|
||||
@@ -103,7 +103,7 @@ export function isBotMentionedFromTargets(
|
||||
}
|
||||
|
||||
export function debugMention(
|
||||
msg: WebInboundMsg,
|
||||
msg: WebInboundMessage,
|
||||
mentionCfg: MentionConfig,
|
||||
authDir?: string,
|
||||
): { wasMentioned: boolean; details: Record<string, unknown> } {
|
||||
@@ -111,15 +111,15 @@ export function debugMention(
|
||||
const result = isBotMentionedFromTargets(msg, mentionCfg, mentionTargets);
|
||||
const details = {
|
||||
from: msg.from,
|
||||
body: msg.body,
|
||||
bodyClean: normalizeMentionText(msg.body),
|
||||
mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
|
||||
body: msg.payload.body,
|
||||
bodyClean: normalizeMentionText(msg.payload.body),
|
||||
mentionedJids: msg.group?.mentions?.jids ?? null,
|
||||
normalizedMentionedJids: mentionTargets.normalizedMentions.length
|
||||
? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity))
|
||||
: null,
|
||||
selfJid: msg.self?.jid ?? msg.selfJid ?? null,
|
||||
selfLid: msg.self?.lid ?? msg.selfLid ?? null,
|
||||
selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
|
||||
selfJid: msg.platform.self?.jid ?? msg.platform.selfJid ?? null,
|
||||
selfLid: msg.platform.self?.lid ?? msg.platform.selfLid ?? null,
|
||||
selfE164: msg.platform.self?.e164 ?? msg.platform.selfE164 ?? null,
|
||||
resolvedSelf: mentionTargets.self,
|
||||
};
|
||||
return { wasMentioned: result, details };
|
||||
|
||||
@@ -26,7 +26,9 @@ import {
|
||||
type ManagedWhatsAppListener,
|
||||
} from "../connection-controller.js";
|
||||
import { resolveWhatsAppInboundPolicy } from "../inbound-policy.js";
|
||||
import { normalizeWebInboundMessage } from "../inbound/message-aliases.js";
|
||||
import { attachWebInboxToSocket, type WhatsAppGroupMetadataCache } from "../inbound/monitor.js";
|
||||
import type { WebInboundMessageInput } from "../inbound/types.js";
|
||||
import {
|
||||
newConnectionId,
|
||||
resolveHeartbeatSeconds,
|
||||
@@ -42,7 +44,7 @@ import { createWebChannelStatusController } from "./monitor-state.js";
|
||||
import { createEchoTracker } from "./monitor/echo.js";
|
||||
import { formatWhatsAppInboundListeningLog } from "./monitor/listener-log.js";
|
||||
import { createWebOnMessageHandler } from "./monitor/on-message.js";
|
||||
import type { WebInboundMsg, WebMonitorTuning } from "./types.js";
|
||||
import type { WebMonitorTuning } from "./types.js";
|
||||
import { isLikelyWhatsAppCryptoError } from "./util.js";
|
||||
|
||||
function isNonRetryableWebCloseStatus(statusCode: unknown): boolean {
|
||||
@@ -286,17 +288,18 @@ export async function monitorWebChannel(
|
||||
accountId: account.accountId,
|
||||
}),
|
||||
});
|
||||
const shouldDebounce = (msg: WebInboundMsg) => {
|
||||
if (msg.mediaPath || msg.mediaType) {
|
||||
const shouldDebounce = (msg: WebInboundMessageInput) => {
|
||||
const normalized = normalizeWebInboundMessage(msg);
|
||||
if (normalized.payload.media?.path || normalized.payload.media?.type) {
|
||||
return false;
|
||||
}
|
||||
if (msg.location) {
|
||||
if (normalized.payload.location) {
|
||||
return false;
|
||||
}
|
||||
if (msg.replyToId || msg.replyToBody) {
|
||||
if (normalized.quote?.id || normalized.quote?.body) {
|
||||
return false;
|
||||
}
|
||||
return !isControlCommandMessage(msg.body, cfg);
|
||||
return !isControlCommandMessage(normalized.payload.body, cfg);
|
||||
};
|
||||
|
||||
let connection;
|
||||
@@ -337,11 +340,12 @@ export async function monitorWebChannel(
|
||||
disconnectRetryPolicy: reconnectPolicy,
|
||||
disconnectRetryAbortSignal: controller.getDisconnectRetryAbortSignal(),
|
||||
groupMetadataCache,
|
||||
onMessage: async (msg: WebInboundMsg) => {
|
||||
onMessage: async (msg: WebInboundMessageInput) => {
|
||||
const normalized = normalizeWebInboundMessage(msg);
|
||||
const inboundAt = Date.now();
|
||||
controller.noteInbound(inboundAt);
|
||||
statusController.noteInbound(inboundAt);
|
||||
await onMessage(msg);
|
||||
await onMessage(normalized);
|
||||
},
|
||||
sock,
|
||||
})) as ManagedWhatsAppListener;
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { resolveWhatsAppReactionLevel } from "../../reaction-level.js";
|
||||
import { sendReactionWhatsApp } from "../../send.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { resolveWhatsAppAckEmoji } from "./ack-emoji.js";
|
||||
import { resolveGroupActivationFor } from "./group-activation.js";
|
||||
|
||||
export async function maybeSendAckReaction(params: {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
conversationId: string;
|
||||
@@ -25,7 +25,7 @@ export async function maybeSendAckReaction(params: {
|
||||
info: (obj: unknown, msg: string) => void;
|
||||
warn: (obj: unknown, msg: string) => void;
|
||||
}): Promise<AckReactionHandle | null> {
|
||||
if (!params.msg.id) {
|
||||
if (!params.msg.event.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function maybeSendAckReaction(params: {
|
||||
}
|
||||
|
||||
params.info(
|
||||
{ chatId: params.msg.chatId, messageId: params.msg.id, emoji },
|
||||
{ chatId: params.msg.platform.chatJid, messageId: params.msg.event.id, emoji },
|
||||
"sending ack reaction",
|
||||
);
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
@@ -88,18 +88,27 @@ export async function maybeSendAckReaction(params: {
|
||||
};
|
||||
return createAckReactionHandle({
|
||||
ackReactionValue: emoji,
|
||||
send: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, emoji, reactionOptions),
|
||||
remove: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, "", reactionOptions),
|
||||
send: () =>
|
||||
sendReactionWhatsApp(
|
||||
params.msg.platform.chatJid,
|
||||
params.msg.event.id!,
|
||||
emoji,
|
||||
reactionOptions,
|
||||
),
|
||||
remove: () =>
|
||||
sendReactionWhatsApp(params.msg.platform.chatJid, params.msg.event.id!, "", reactionOptions),
|
||||
onSendError: (err) => {
|
||||
params.warn(
|
||||
{
|
||||
error: formatError(err),
|
||||
chatId: params.msg.chatId,
|
||||
messageId: params.msg.id,
|
||||
chatId: params.msg.platform.chatJid,
|
||||
messageId: params.msg.event.id,
|
||||
},
|
||||
"failed to send ack reaction",
|
||||
);
|
||||
logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`);
|
||||
logVerbose(
|
||||
`WhatsApp ack reaction failed for chat ${params.msg.platform.chatJid}: ${formatError(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
normalizeAgentId,
|
||||
} from "openclaw/plugin-sdk/routing";
|
||||
import { resolveWhatsAppGroupSessionRoute } from "../../group-session-key.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import { whatsappInboundLog } from "../loggers.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import type { GroupHistoryEntry } from "./inbound-context.js";
|
||||
|
||||
function buildBroadcastRouteKeys(params: {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
peerId: string;
|
||||
agentId: string;
|
||||
@@ -49,13 +49,13 @@ function buildBroadcastRouteKeys(params: {
|
||||
|
||||
export async function maybeBroadcastMessage(params: {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
peerId: string;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
groupHistoryKey: string;
|
||||
groupHistories: Map<string, GroupHistoryEntry[]>;
|
||||
processMessage: (
|
||||
msg: WebInboundMsg,
|
||||
msg: WebInboundMessage,
|
||||
route: ReturnType<typeof resolveAgentRoute>,
|
||||
groupHistoryKey: string,
|
||||
opts?: {
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
identitiesOverlap,
|
||||
} from "../../identity.js";
|
||||
import { resolveWhatsAppInboundPolicy } from "../../inbound-policy.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import type { MentionConfig } from "../mentions.js";
|
||||
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { stripMentionsForCommand } from "./commands.js";
|
||||
import { resolveGroupActivationFor } from "./group-activation.js";
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ export type GroupHistoryEntry = {
|
||||
|
||||
type ApplyGroupGatingParams = {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
mentionText?: string;
|
||||
deferMissingMention?: boolean;
|
||||
conversationId: string;
|
||||
@@ -78,7 +78,7 @@ function shouldWarnForGroupDrop(warnKey: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
|
||||
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMessage) {
|
||||
const sender = normalizeE164(getSenderIdentity(msg).e164 ?? "");
|
||||
if (!sender) {
|
||||
return false;
|
||||
@@ -88,7 +88,7 @@ function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
|
||||
}
|
||||
|
||||
function recordPendingGroupHistoryEntry(params: {
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
body?: string;
|
||||
groupHistories: Map<string, GroupHistoryEntry[]>;
|
||||
groupHistoryKey: string;
|
||||
@@ -107,10 +107,10 @@ function recordPendingGroupHistoryEntry(params: {
|
||||
limit: params.groupHistoryLimit,
|
||||
entry: {
|
||||
sender,
|
||||
body: params.body ?? params.msg.body,
|
||||
timestamp: params.msg.timestamp,
|
||||
id: params.msg.id,
|
||||
senderJid: senderIdentity.jid ?? params.msg.senderJid,
|
||||
body: params.body ?? params.msg.payload.body,
|
||||
timestamp: params.msg.event.timestamp,
|
||||
id: params.msg.event.id,
|
||||
senderJid: senderIdentity.jid ?? params.msg.platform.senderJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -178,9 +178,11 @@ export async function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
allowFrom: inboundPolicy.configuredAllowFrom,
|
||||
};
|
||||
const mentionMsg =
|
||||
params.mentionText !== undefined ? { ...params.msg, body: params.mentionText } : params.msg;
|
||||
params.mentionText !== undefined
|
||||
? { ...params.msg, payload: { ...params.msg.payload, body: params.mentionText } }
|
||||
: params.msg;
|
||||
const commandBody = stripMentionsForCommand(
|
||||
mentionMsg.body,
|
||||
mentionMsg.payload.body,
|
||||
mentionConfig.mentionRegexes,
|
||||
self.e164,
|
||||
);
|
||||
@@ -251,7 +253,7 @@ export async function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
}
|
||||
return skipGroupMessageAndStoreHistory(
|
||||
params,
|
||||
`Group message stored for context (no mention detected) in ${params.conversationId}: ${mentionMsg.body}`,
|
||||
`Group message stored for context (no mention detected) in ${params.conversationId}: ${mentionMsg.payload.body}`,
|
||||
params.mentionText,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import { filterSupplementalContextItems } from "openclaw/plugin-sdk/security-run
|
||||
import {
|
||||
getComparableIdentityValues,
|
||||
getReplyContext,
|
||||
resolveComparableIdentity,
|
||||
type WhatsAppIdentity,
|
||||
type WhatsAppReplyContext,
|
||||
} from "../../identity.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { normalizeE164 } from "../../text-runtime.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
|
||||
export type GroupHistoryEntry = {
|
||||
sender: string;
|
||||
@@ -22,12 +23,15 @@ type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote";
|
||||
|
||||
function isWhatsAppSupplementalSenderAllowed(params: {
|
||||
allowFrom: string[];
|
||||
authDir?: string;
|
||||
sender?: WhatsAppIdentity | null;
|
||||
}): boolean {
|
||||
if (params.allowFrom.includes("*")) {
|
||||
return true;
|
||||
}
|
||||
const senderValues = new Set(getComparableIdentityValues(params.sender));
|
||||
const senderValues = new Set(
|
||||
getComparableIdentityValues(resolveComparableIdentity(params.sender, params.authDir)),
|
||||
);
|
||||
if (senderValues.size === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -45,6 +49,7 @@ function isWhatsAppSupplementalSenderAllowed(params: {
|
||||
}
|
||||
|
||||
export function resolveVisibleWhatsAppGroupHistory(params: {
|
||||
authDir?: string;
|
||||
history: GroupHistoryEntry[];
|
||||
mode: ContextVisibilityMode;
|
||||
groupPolicy: "open" | "allowlist" | "disabled";
|
||||
@@ -60,13 +65,14 @@ export function resolveVisibleWhatsAppGroupHistory(params: {
|
||||
isSenderAllowed: (entry) =>
|
||||
isWhatsAppSupplementalSenderAllowed({
|
||||
allowFrom: params.groupAllowFrom,
|
||||
authDir: params.authDir,
|
||||
sender: entry.senderJid ? { jid: entry.senderJid } : null,
|
||||
}),
|
||||
}).items;
|
||||
}
|
||||
|
||||
export function resolveVisibleWhatsAppReplyContext(params: {
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
authDir?: string;
|
||||
mode: ContextVisibilityMode;
|
||||
groupPolicy: "open" | "allowlist" | "disabled";
|
||||
@@ -81,6 +87,7 @@ export function resolveVisibleWhatsAppReplyContext(params: {
|
||||
? true
|
||||
: isWhatsAppSupplementalSenderAllowed({
|
||||
allowFrom: params.groupAllowFrom,
|
||||
authDir: params.authDir,
|
||||
sender: replyTo.sender,
|
||||
});
|
||||
const visible = filterChannelInboundQuoteContext(params.mode, {
|
||||
|
||||
@@ -13,13 +13,14 @@ import { deliverInboundReplyWithMessageSendContext } from "openclaw/plugin-sdk/c
|
||||
import { buildInboundHistoryFromEntries } from "openclaw/plugin-sdk/reply-history";
|
||||
import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import {
|
||||
type DeliverableWhatsAppOutboundPayload,
|
||||
normalizeWhatsAppOutboundPayload,
|
||||
normalizeWhatsAppPayloadTextPreservingIndentation,
|
||||
} from "../../outbound-media-contract.js";
|
||||
import type { WhatsAppReplyDeliveryResult } from "../deliver-reply.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { markWhatsAppVisibleDeliveryError } from "../util.js";
|
||||
import { formatGroupMembers } from "./group-members.js";
|
||||
import type { GroupHistoryEntry } from "./inbound-context.js";
|
||||
import {
|
||||
@@ -109,25 +110,11 @@ function whatsAppReplyDeliveryVisibilityFromDurableResult(result: {
|
||||
return whatsAppReplyDeliveryVisibility(result.visibleReplySent === true);
|
||||
}
|
||||
|
||||
function markWhatsAppReplyDeliveryErrorVisible(error: unknown): unknown {
|
||||
if (typeof error === "object" && error !== null && !Array.isArray(error)) {
|
||||
try {
|
||||
Object.assign(error, { sentBeforeError: true, visibleReplySent: true });
|
||||
return error;
|
||||
} catch {
|
||||
// Fall back to a wrapper when a platform error object is non-extensible.
|
||||
}
|
||||
}
|
||||
const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
|
||||
Object.assign(visibleError, { sentBeforeError: true, visibleReplySent: true });
|
||||
return visibleError;
|
||||
}
|
||||
|
||||
function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(
|
||||
error: unknown,
|
||||
flushResult: WhatsAppMediaOnlyFlushResult,
|
||||
): unknown {
|
||||
return flushResult.delivered > 0 ? markWhatsAppReplyDeliveryErrorVisible(error) : error;
|
||||
return flushResult.delivered > 0 ? markWhatsAppVisibleDeliveryError(error) : error;
|
||||
}
|
||||
|
||||
function logWhatsAppReplyDeliveryError(params: {
|
||||
@@ -135,19 +122,19 @@ function logWhatsAppReplyDeliveryError(params: {
|
||||
info: ReplyDeliveryInfo;
|
||||
connectionId: string;
|
||||
conversationId: string;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
replyLogger: ReturnType<typeof getChildLogger>;
|
||||
}) {
|
||||
params.replyLogger.error(
|
||||
{
|
||||
err: normalizeErrForLog(params.err),
|
||||
replyKind: params.info.kind,
|
||||
correlationId: params.msg.id ?? null,
|
||||
correlationId: params.msg.event.id ?? null,
|
||||
connectionId: params.connectionId,
|
||||
conversationId: params.conversationId,
|
||||
chatId: params.msg.chatId ?? null,
|
||||
chatId: params.msg.platform.chatJid ?? null,
|
||||
to: params.msg.from ?? null,
|
||||
from: params.msg.to ?? null,
|
||||
from: params.msg.platform.recipientJid ?? null,
|
||||
},
|
||||
"auto-reply delivery failed",
|
||||
);
|
||||
@@ -288,7 +275,7 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
groupHistory?: GroupHistoryEntry[];
|
||||
groupMemberRoster?: Map<string, string>;
|
||||
groupSystemPrompt?: string;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
rawBody?: string;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
sender: SenderContext;
|
||||
@@ -296,6 +283,7 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
mediaTranscribedIndexes?: number[];
|
||||
replyThreading?: ReplyThreadingContext;
|
||||
visibleReplyTo?: VisibleReplyTarget;
|
||||
suppressMessageReceivedHooks?: boolean;
|
||||
}): Promise<FinalizedMsgContext> {
|
||||
const inboundHistory =
|
||||
params.msg.chatType === "group"
|
||||
@@ -311,12 +299,12 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
: undefined;
|
||||
|
||||
const media = toInboundMediaFacts(
|
||||
params.msg.mediaPath || params.msg.mediaUrl
|
||||
params.msg.payload.media?.path || params.msg.payload.media?.url
|
||||
? [
|
||||
{
|
||||
path: params.msg.mediaPath,
|
||||
url: params.msg.mediaUrl ?? params.msg.mediaPath,
|
||||
contentType: params.msg.mediaType,
|
||||
path: params.msg.payload.media?.path,
|
||||
url: params.msg.payload.media?.url ?? params.msg.payload.media?.path,
|
||||
contentType: params.msg.payload.media?.type,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
@@ -334,11 +322,11 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
}
|
||||
: undefined,
|
||||
groupSystemPrompt: params.groupSystemPrompt,
|
||||
untrustedContext: params.msg.untrustedStructuredContext,
|
||||
untrustedContext: params.msg.payload.untrustedStructuredContext,
|
||||
},
|
||||
media,
|
||||
messageId: params.msg.id,
|
||||
timestamp: params.msg.timestamp,
|
||||
messageId: params.msg.event.id,
|
||||
timestamp: params.msg.event.timestamp,
|
||||
from: params.msg.from,
|
||||
sender: {
|
||||
id: params.sender.id ?? params.sender.e164,
|
||||
@@ -355,15 +343,15 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
routeSessionKey: params.route.sessionKey,
|
||||
},
|
||||
reply: {
|
||||
to: params.msg.to,
|
||||
to: params.msg.platform.recipientJid,
|
||||
originatingTo: params.msg.from,
|
||||
},
|
||||
message: {
|
||||
body: params.combinedBody,
|
||||
bodyForAgent: params.bodyForAgent ?? params.msg.body,
|
||||
bodyForAgent: params.bodyForAgent ?? params.msg.payload.body,
|
||||
inboundHistory,
|
||||
rawBody: params.rawBody ?? params.msg.body,
|
||||
commandBody: params.commandBody ?? params.msg.body,
|
||||
rawBody: params.rawBody ?? params.msg.payload.body,
|
||||
commandBody: params.commandBody ?? params.msg.payload.body,
|
||||
},
|
||||
access: {
|
||||
...(params.msg.wasMentioned !== undefined
|
||||
@@ -381,9 +369,9 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
commandTurn: params.commandTurn,
|
||||
extra: {
|
||||
Transcript: params.transcript,
|
||||
GroupSubject: params.msg.groupSubject,
|
||||
GroupSubject: params.msg.group?.subject,
|
||||
GroupMembers: formatGroupMembers({
|
||||
participants: params.msg.groupParticipants,
|
||||
participants: params.msg.group?.participants,
|
||||
roster: params.groupMemberRoster,
|
||||
fallbackE164: params.sender.e164,
|
||||
}),
|
||||
@@ -394,7 +382,8 @@ export async function buildWhatsAppInboundContext(params: {
|
||||
? params.commandTurn.source
|
||||
: undefined),
|
||||
ReplyThreading: params.replyThreading,
|
||||
...(params.msg.location ? toLocationContext(params.msg.location) : {}),
|
||||
SuppressMessageReceivedHooks: params.suppressMessageReceivedHooks,
|
||||
...(params.msg.payload.location ? toLocationContext(params.msg.payload.location) : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -437,7 +426,7 @@ function normalizeCommandTurnFromContext(value: unknown): CommandTurnContext | u
|
||||
}
|
||||
|
||||
export function resolveWhatsAppDmRouteTarget(params: {
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
senderE164?: string;
|
||||
normalizeE164: (value: string) => string | null;
|
||||
}): string | undefined {
|
||||
@@ -518,7 +507,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
deliverReply: (params: {
|
||||
replyResult: ReplyPayload;
|
||||
normalizedReplyResult?: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
mediaLocalRoots: readonly string[];
|
||||
maxMediaBytes: number;
|
||||
textLimit: number;
|
||||
@@ -532,7 +521,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
groupHistoryKey: string;
|
||||
maxMediaBytes: number;
|
||||
maxMediaTextChunkLimit?: number;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
onModelSelected?: ChannelReplyOnModelSelected;
|
||||
rememberSentText: (
|
||||
text: string | undefined,
|
||||
@@ -617,12 +606,12 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
if (!delivery.providerAccepted) {
|
||||
params.replyLogger.warn(
|
||||
{
|
||||
correlationId: params.msg.id ?? null,
|
||||
correlationId: params.msg.event.id ?? null,
|
||||
connectionId: params.connectionId,
|
||||
conversationId: params.conversationId,
|
||||
chatId: params.msg.chatId,
|
||||
chatId: params.msg.platform.chatJid,
|
||||
to: params.msg.from,
|
||||
from: params.msg.to,
|
||||
from: params.msg.platform.recipientJid,
|
||||
replyKind: info.kind,
|
||||
},
|
||||
"auto-reply was not accepted by WhatsApp provider",
|
||||
@@ -704,7 +693,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
});
|
||||
if (durable.status === "failed") {
|
||||
if (durable.sentBeforeError === true) {
|
||||
throw markWhatsAppReplyDeliveryErrorVisible(durable.error);
|
||||
throw markWhatsAppVisibleDeliveryError(durable.error);
|
||||
}
|
||||
throw durable.error;
|
||||
}
|
||||
@@ -756,7 +745,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
logWhatsAppMediaOnlyFlushResult(flushResult);
|
||||
return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
|
||||
},
|
||||
onReplyStart: params.msg.sendComposing,
|
||||
onReplyStart: params.msg.platform.sendComposing,
|
||||
...(statusReactionController
|
||||
? {
|
||||
onCompactionStart: async () => {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// Whatsapp plugin module implements message line behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { getPrimaryIdentityId, getReplyContext, getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import {
|
||||
getPrimaryIdentityId,
|
||||
getReplyContext,
|
||||
getSenderIdentity,
|
||||
type WhatsAppReplyContext,
|
||||
} from "../../identity.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import {
|
||||
formatInboundEnvelope,
|
||||
resolveMessagePrefix,
|
||||
type EnvelopeFormatOptions,
|
||||
} from "./message-line.runtime.js";
|
||||
|
||||
export function formatReplyContext(msg: WebInboundMsg) {
|
||||
const replyTo = getReplyContext(msg);
|
||||
function formatReplyTarget(replyTo: WhatsAppReplyContext | null) {
|
||||
if (!replyTo?.body) {
|
||||
return null;
|
||||
}
|
||||
@@ -18,12 +22,17 @@ export function formatReplyContext(msg: WebInboundMsg) {
|
||||
return `[Replying to ${sender}${idPart}]\n${replyTo.body}\n[/Replying]`;
|
||||
}
|
||||
|
||||
export function formatReplyContext(msg: WebInboundMessage) {
|
||||
return formatReplyTarget(getReplyContext(msg));
|
||||
}
|
||||
|
||||
export function buildInboundLine(params: {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
agentId: string;
|
||||
previousTimestamp?: number;
|
||||
envelope?: EnvelopeFormatOptions;
|
||||
visibleReplyTo?: WhatsAppReplyContext | null;
|
||||
}) {
|
||||
const { cfg, msg, agentId, previousTimestamp, envelope } = params;
|
||||
// WhatsApp inbound prefix: channels.whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults
|
||||
@@ -32,15 +41,18 @@ export function buildInboundLine(params: {
|
||||
hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0,
|
||||
});
|
||||
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
||||
const replyContext = formatReplyContext(msg);
|
||||
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
||||
const replyContext =
|
||||
params.visibleReplyTo === undefined
|
||||
? formatReplyContext(msg)
|
||||
: formatReplyTarget(params.visibleReplyTo);
|
||||
const baseLine = `${prefixStr}${msg.payload.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
||||
const sender = getSenderIdentity(msg);
|
||||
|
||||
// Wrap with standardized envelope for the agent.
|
||||
return formatInboundEnvelope({
|
||||
channel: "WhatsApp",
|
||||
from: msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
||||
timestamp: msg.timestamp,
|
||||
timestamp: msg.event.timestamp,
|
||||
body: baseLine,
|
||||
chatType: msg.chatType,
|
||||
sender: {
|
||||
@@ -50,6 +62,6 @@ export function buildInboundLine(params: {
|
||||
},
|
||||
previousTimestamp,
|
||||
envelope,
|
||||
fromMe: msg.fromMe,
|
||||
fromMe: msg.platform.fromMe,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveWhatsAppAccount } from "../../accounts.js";
|
||||
import { resolveWhatsAppGroupSessionRoute } from "../../group-session-key.js";
|
||||
import { getPrimaryIdentityId, getSenderIdentity } from "../../identity.js";
|
||||
import {
|
||||
normalizeWebInboundMessage,
|
||||
withDeprecatedWebInboundMessageFlatAliases,
|
||||
} from "../../inbound/message-aliases.js";
|
||||
import type { WebInboundMessageInput } from "../../inbound/types.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { normalizeE164 } from "../../text-runtime.js";
|
||||
import { buildMentionConfig } from "../mentions.js";
|
||||
import type { MentionConfig } from "../mentions.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { maybeSendAckReaction } from "./ack-reaction.js";
|
||||
import { maybeBroadcastMessage } from "./broadcast.js";
|
||||
import type { EchoTracker } from "./echo.js";
|
||||
@@ -42,9 +47,32 @@ export function createWebOnMessageHandler(params: {
|
||||
baseMentionConfig: MentionConfig;
|
||||
account: { authDir?: string; accountId?: string; selfChatMode?: boolean };
|
||||
}) {
|
||||
const withDirectSenderPeer = (msg: WebInboundMessage, peerId: string): WebInboundMessage => {
|
||||
if (
|
||||
msg.chatType === "group" ||
|
||||
msg.platform.sender?.e164 ||
|
||||
msg.platform.senderE164 ||
|
||||
!peerId.startsWith("+")
|
||||
) {
|
||||
return msg;
|
||||
}
|
||||
const normalized = normalizeE164(peerId);
|
||||
if (!normalized) {
|
||||
return msg;
|
||||
}
|
||||
return withDeprecatedWebInboundMessageFlatAliases({
|
||||
...msg,
|
||||
platform: {
|
||||
...msg.platform,
|
||||
sender: { ...msg.platform.sender, e164: normalized },
|
||||
senderE164: normalized,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const processForRoute = async (
|
||||
cfg: OpenClawConfig,
|
||||
msg: WebInboundMsg,
|
||||
msg: WebInboundMessage,
|
||||
route: ReturnType<typeof resolveAgentRoute>,
|
||||
groupHistoryKey: string,
|
||||
opts?: {
|
||||
@@ -95,10 +123,12 @@ export function createWebOnMessageHandler(params: {
|
||||
return processMessage(processParams);
|
||||
};
|
||||
|
||||
return async (msg: WebInboundMsg) => {
|
||||
return async (rawMsg: WebInboundMessageInput) => {
|
||||
const normalizedMsg = normalizeWebInboundMessage(rawMsg);
|
||||
const cfg = params.loadConfig?.() ?? params.cfg;
|
||||
const peerId = resolvePeerId(normalizedMsg);
|
||||
const msg = withDirectSenderPeer(normalizedMsg, peerId);
|
||||
const conversationId = msg.conversationId ?? msg.from;
|
||||
const peerId = resolvePeerId(msg);
|
||||
const baseRoute = resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "whatsapp",
|
||||
@@ -126,14 +156,14 @@ export function createWebOnMessageHandler(params: {
|
||||
const baseMentionConfig = buildMentionConfig(cfg);
|
||||
|
||||
// Same-phone mode logging retained
|
||||
if (msg.from === msg.to) {
|
||||
if (msg.from === msg.platform.recipientJid) {
|
||||
logVerbose(`📱 Same-phone mode detected (from === to: ${msg.from})`);
|
||||
}
|
||||
|
||||
// Skip if this is a message we just sent (echo detection)
|
||||
if (params.echoTracker.has(msg.body)) {
|
||||
if (params.echoTracker.has(msg.payload.body)) {
|
||||
logVerbose("Skipping auto-reply: detected echo (message matches recently sent text)");
|
||||
params.echoTracker.forget(msg.body);
|
||||
params.echoTracker.forget(msg.payload.body);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +176,8 @@ export function createWebOnMessageHandler(params: {
|
||||
// undefined = preflight was not attempted (non-audio message).
|
||||
let preflightAudioTranscript: string | null | undefined;
|
||||
const hasAudioBody =
|
||||
msg.mediaType?.startsWith("audio/") === true && msg.body === "<media:audio>";
|
||||
msg.payload.media?.type?.startsWith("audio/") === true &&
|
||||
msg.payload.body === "<media:audio>";
|
||||
const canRunEarlyAudioPreflight = msg.chatType === "group" || msg.accessControlPassed === true;
|
||||
let ackAlreadySent = false;
|
||||
let ackReaction: AckReactionHandle | null = null;
|
||||
@@ -156,7 +187,7 @@ export function createWebOnMessageHandler(params: {
|
||||
preflightAudioTranscript !== undefined ||
|
||||
!canRunEarlyAudioPreflight ||
|
||||
!hasAudioBody ||
|
||||
!msg.mediaPath
|
||||
!msg.payload.media?.path
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -194,10 +225,10 @@ export function createWebOnMessageHandler(params: {
|
||||
preflightAudioTranscript =
|
||||
(await transcribeFirstAudio({
|
||||
ctx: {
|
||||
MediaPaths: [msg.mediaPath],
|
||||
MediaTypes: msg.mediaType ? [msg.mediaType] : undefined,
|
||||
MediaPaths: [msg.payload.media?.path],
|
||||
MediaTypes: msg.payload.media?.type ? [msg.payload.media?.type] : undefined,
|
||||
From: msg.from,
|
||||
To: msg.to,
|
||||
To: msg.platform.recipientJid,
|
||||
Provider: "whatsapp",
|
||||
Surface: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
@@ -216,12 +247,12 @@ export function createWebOnMessageHandler(params: {
|
||||
const sender = getSenderIdentity(msg);
|
||||
const metaCtx = {
|
||||
From: msg.from,
|
||||
To: msg.to,
|
||||
To: msg.platform.recipientJid,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: msg.chatType,
|
||||
ConversationLabel: conversationId,
|
||||
GroupSubject: msg.groupSubject,
|
||||
GroupSubject: msg.group?.subject,
|
||||
SenderName: sender.name ?? undefined,
|
||||
SenderId: getPrimaryIdentityId(sender) ?? undefined,
|
||||
SenderE164: sender.e164 ?? undefined,
|
||||
@@ -245,7 +276,7 @@ export function createWebOnMessageHandler(params: {
|
||||
let gating = await applyGroupGating({
|
||||
cfg,
|
||||
msg,
|
||||
deferMissingMention: hasAudioBody && Boolean(msg.mediaPath),
|
||||
deferMissingMention: hasAudioBody && Boolean(msg.payload.media?.path),
|
||||
conversationId,
|
||||
groupHistoryKey,
|
||||
agentId: route.agentId,
|
||||
@@ -290,13 +321,6 @@ export function createWebOnMessageHandler(params: {
|
||||
if (!gating.shouldProcess) {
|
||||
return;
|
||||
}
|
||||
} else if (!msg.sender?.e164 && !msg.senderE164 && peerId && peerId.startsWith("+")) {
|
||||
// Ensure `peerId` for DMs is stable and stored as E.164 when possible.
|
||||
const normalized = normalizeE164(peerId);
|
||||
if (normalized) {
|
||||
msg.sender = { ...msg.sender, e164: normalized };
|
||||
msg.senderE164 = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
await runAudioPreflightOnce();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Whatsapp plugin module implements peer behavior.
|
||||
import { getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { jidToE164, normalizeE164 } from "../../text-runtime.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
|
||||
export function resolvePeerId(msg: WebInboundMsg) {
|
||||
export function resolvePeerId(msg: WebInboundMessage) {
|
||||
if (msg.chatType === "group") {
|
||||
return msg.conversationId ?? msg.from;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolveWhatsAppCommandAuthorized,
|
||||
resolveWhatsAppInboundPolicy,
|
||||
} from "../../inbound-policy.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { newConnectionId } from "../../reconnect.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import {
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
} from "../../system-prompt.js";
|
||||
import { deliverWebReply } from "../deliver-reply.js";
|
||||
import { whatsappInboundLog } from "../loggers.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { elide } from "../util.js";
|
||||
import { maybeSendAckReaction } from "./ack-reaction.js";
|
||||
import {
|
||||
@@ -184,7 +184,7 @@ function resolvePinnedMainDmRecipient(params: {
|
||||
|
||||
export async function processMessage(params: {
|
||||
cfg: ReturnType<LoadConfigFn>;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
groupHistoryKey: string;
|
||||
groupHistories: Map<string, GroupHistoryEntry[]>;
|
||||
@@ -249,16 +249,21 @@ export async function processMessage(params: {
|
||||
// undefined → caller did not attempt; run internal STT
|
||||
let audioTranscript: string | undefined = params.preflightAudioTranscript ?? undefined;
|
||||
const hasAudioBody =
|
||||
params.msg.mediaType?.startsWith("audio/") === true && params.msg.body === "<media:audio>";
|
||||
if (params.preflightAudioTranscript === undefined && hasAudioBody && params.msg.mediaPath) {
|
||||
params.msg.payload.media?.type?.startsWith("audio/") === true &&
|
||||
params.msg.payload.body === "<media:audio>";
|
||||
if (
|
||||
params.preflightAudioTranscript === undefined &&
|
||||
hasAudioBody &&
|
||||
params.msg.payload.media?.path
|
||||
) {
|
||||
try {
|
||||
const { transcribeFirstAudio } = await import("./audio-preflight.runtime.js");
|
||||
audioTranscript = await transcribeFirstAudio({
|
||||
ctx: {
|
||||
MediaPaths: [params.msg.mediaPath],
|
||||
MediaTypes: params.msg.mediaType ? [params.msg.mediaType] : undefined,
|
||||
MediaPaths: [params.msg.payload.media?.path],
|
||||
MediaTypes: params.msg.payload.media?.type ? [params.msg.payload.media?.type] : undefined,
|
||||
From: params.msg.from,
|
||||
To: params.msg.to,
|
||||
To: params.msg.platform.recipientJid,
|
||||
Provider: "whatsapp",
|
||||
Surface: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
@@ -281,7 +286,16 @@ export async function processMessage(params: {
|
||||
// audio message. The transcript and transcribed media index are also stored on
|
||||
// context so downstream media understanding does not transcribe it again.
|
||||
const msgForAgent =
|
||||
audioTranscript !== undefined ? { ...params.msg, body: audioTranscript } : params.msg;
|
||||
audioTranscript !== undefined
|
||||
? { ...params.msg, payload: { ...params.msg.payload, body: audioTranscript } }
|
||||
: params.msg;
|
||||
const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
|
||||
msg: params.msg,
|
||||
authDir: account.authDir,
|
||||
mode: contextVisibilityMode,
|
||||
groupPolicy: inboundPolicy.groupPolicy,
|
||||
groupAllowFrom: inboundPolicy.groupAllowFrom,
|
||||
});
|
||||
|
||||
let combinedBody = buildInboundLine({
|
||||
cfg: params.cfg,
|
||||
@@ -289,6 +303,7 @@ export async function processMessage(params: {
|
||||
agentId: params.route.agentId,
|
||||
previousTimestamp,
|
||||
envelope: envelopeOptions,
|
||||
visibleReplyTo,
|
||||
});
|
||||
let shouldClearGroupHistory = false;
|
||||
const visibleGroupHistory =
|
||||
@@ -298,6 +313,7 @@ export async function processMessage(params: {
|
||||
mode: contextVisibilityMode,
|
||||
groupPolicy: inboundPolicy.groupPolicy,
|
||||
groupAllowFrom: inboundPolicy.groupAllowFrom,
|
||||
authDir: account.authDir,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -380,44 +396,40 @@ export async function processMessage(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const correlationId = params.msg.id ?? newConnectionId();
|
||||
const correlationId = params.msg.event.id ?? newConnectionId();
|
||||
params.replyLogger.info(
|
||||
{
|
||||
connectionId: params.connectionId,
|
||||
correlationId,
|
||||
from: params.msg.chatType === "group" ? conversationId : params.msg.from,
|
||||
to: params.msg.to,
|
||||
to: params.msg.platform.recipientJid,
|
||||
body: elide(combinedBody, 240),
|
||||
mediaType: params.msg.mediaType ?? null,
|
||||
mediaPath: params.msg.mediaPath ?? null,
|
||||
mediaType: params.msg.payload.media?.type ?? null,
|
||||
mediaPath: params.msg.payload.media?.path ?? null,
|
||||
},
|
||||
"inbound web message",
|
||||
);
|
||||
|
||||
const fromDisplay = params.msg.chatType === "group" ? conversationId : params.msg.from;
|
||||
const kindLabel = params.msg.mediaType ? `, ${params.msg.mediaType}` : "";
|
||||
const kindLabel = params.msg.payload.media?.type ? `, ${params.msg.payload.media?.type}` : "";
|
||||
whatsappInboundLog.info(
|
||||
`Inbound message ${fromDisplay} -> ${params.msg.to} (${params.msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
|
||||
`Inbound message ${fromDisplay} -> ${params.msg.platform.recipientJid} (${params.msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
|
||||
);
|
||||
if (shouldLogVerbose()) {
|
||||
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
||||
}
|
||||
|
||||
const sender = getSenderIdentity(params.msg);
|
||||
const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
|
||||
msg: params.msg,
|
||||
authDir: account.authDir,
|
||||
mode: contextVisibilityMode,
|
||||
groupPolicy: inboundPolicy.groupPolicy,
|
||||
groupAllowFrom: inboundPolicy.groupAllowFrom,
|
||||
});
|
||||
const dmRouteTarget = resolveWhatsAppDmRouteTarget({
|
||||
msg: params.msg,
|
||||
senderE164: sender.e164 ?? undefined,
|
||||
normalizeE164,
|
||||
});
|
||||
const shouldCheckCommandAuth = shouldComputeCommandAuthorized(params.msg.body, params.cfg);
|
||||
const isTextCommand = isControlCommandMessage(params.msg.body, params.cfg);
|
||||
const shouldCheckCommandAuth = shouldComputeCommandAuthorized(
|
||||
params.msg.payload.body,
|
||||
params.cfg,
|
||||
);
|
||||
const isTextCommand = isControlCommandMessage(params.msg.payload.body, params.cfg);
|
||||
const commandAuthorized = shouldCheckCommandAuth
|
||||
? await resolveWhatsAppCommandAuthorized({
|
||||
cfg: params.cfg,
|
||||
@@ -430,13 +442,13 @@ export async function processMessage(params: {
|
||||
kind: "text-slash",
|
||||
source: "text",
|
||||
authorized: Boolean(commandAuthorized),
|
||||
body: params.msg.body,
|
||||
body: params.msg.payload.body,
|
||||
}
|
||||
: {
|
||||
kind: "normal",
|
||||
source: "message",
|
||||
authorized: false,
|
||||
body: params.msg.body,
|
||||
body: params.msg.payload.body,
|
||||
};
|
||||
const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
|
||||
cfg: params.cfg,
|
||||
@@ -452,7 +464,7 @@ export async function processMessage(params: {
|
||||
});
|
||||
const replyThreading = resolveBatchedReplyThreadingPolicy(
|
||||
account.replyToMode ?? "off",
|
||||
params.msg.isBatched === true,
|
||||
params.msg.event.isBatched === true,
|
||||
);
|
||||
|
||||
// Resolve combined conversation system prompt using the group or direct surface.
|
||||
@@ -468,9 +480,9 @@ export async function processMessage(params: {
|
||||
});
|
||||
|
||||
const ctxPayload = await buildWhatsAppInboundContext({
|
||||
bodyForAgent: msgForAgent.body,
|
||||
bodyForAgent: msgForAgent.payload.body,
|
||||
combinedBody,
|
||||
commandBody: params.msg.body,
|
||||
commandBody: params.msg.payload.body,
|
||||
commandAuthorized,
|
||||
commandTurn,
|
||||
conversationId,
|
||||
@@ -478,7 +490,7 @@ export async function processMessage(params: {
|
||||
groupMemberRoster: params.groupMemberNames.get(params.groupHistoryKey),
|
||||
groupSystemPrompt: conversationSystemPrompt,
|
||||
msg: params.msg,
|
||||
rawBody: params.msg.body,
|
||||
rawBody: params.msg.payload.body,
|
||||
route: params.route,
|
||||
sender: {
|
||||
id: getPrimaryIdentityId(sender) ?? undefined,
|
||||
@@ -489,6 +501,7 @@ export async function processMessage(params: {
|
||||
...(audioTranscript !== undefined ? { mediaTranscribedIndexes: [0] } : {}),
|
||||
replyThreading,
|
||||
visibleReplyTo: visibleReplyTo ?? undefined,
|
||||
suppressMessageReceivedHooks: true,
|
||||
});
|
||||
emitWhatsAppMessageReceivedHooksIfEnabled({
|
||||
cfg: params.cfg,
|
||||
@@ -518,8 +531,8 @@ export async function processMessage(params: {
|
||||
raw: params.msg,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: params.msg.id ?? `${conversationId}:${Date.now()}`,
|
||||
timestamp: params.msg.timestamp,
|
||||
id: params.msg.event.id ?? `${conversationId}:${Date.now()}`,
|
||||
timestamp: params.msg.event.timestamp,
|
||||
rawText: ctxPayload.RawBody ?? "",
|
||||
textForAgent: ctxPayload.BodyForAgent,
|
||||
textForCommands: ctxPayload.CommandBody,
|
||||
@@ -582,7 +595,7 @@ export async function processMessage(params: {
|
||||
logAckFailure({
|
||||
log: logVerbose,
|
||||
channel: "whatsapp",
|
||||
target: `${params.msg.chatId ?? conversationId}/${params.msg.id ?? "unknown"}`,
|
||||
target: `${params.msg.platform.chatJid ?? conversationId}/${params.msg.event.id ?? "unknown"}`,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getSenderIdentity } from "../../identity.js";
|
||||
import type { WebInboundMessage } from "../../inbound/types.js";
|
||||
import { resolveWhatsAppReactionLevel } from "../../reaction-level.js";
|
||||
import { sendReactionWhatsApp } from "../../send.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { resolveWhatsAppAckEmoji } from "./ack-emoji.js";
|
||||
import { resolveGroupActivationFor } from "./group-activation.js";
|
||||
|
||||
@@ -17,7 +17,7 @@ export type { StatusReactionController };
|
||||
|
||||
export type WhatsAppStatusReactionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
msg: WebInboundMsg;
|
||||
msg: WebInboundMessage;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
conversationId: string;
|
||||
@@ -28,7 +28,7 @@ export type WhatsAppStatusReactionParams = {
|
||||
export async function createWhatsAppStatusReactionController(
|
||||
params: WhatsAppStatusReactionParams,
|
||||
): Promise<StatusReactionController | null> {
|
||||
if (!params.msg.id) {
|
||||
if (!params.msg.event.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@ export async function createWhatsAppStatusReactionController(
|
||||
...(params.accountId ? { accountId: params.accountId } : {}),
|
||||
cfg: params.cfg,
|
||||
};
|
||||
const chatId = params.msg.chatId;
|
||||
const msgId = params.msg.id;
|
||||
const chatId = params.msg.platform.chatJid;
|
||||
const msgId = params.msg.event.id;
|
||||
|
||||
return createStatusReactionController({
|
||||
enabled: true,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type WebChannelHealthState =
|
||||
| "logged-out"
|
||||
| "stopped";
|
||||
|
||||
/** @deprecated Use `WebInboundMessage`. */
|
||||
export type WebInboundMsg = WebInboundMessage;
|
||||
|
||||
export type WebChannelStatus = {
|
||||
|
||||
@@ -11,6 +11,20 @@ export function elide(text?: string, limit = 400) {
|
||||
return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
|
||||
}
|
||||
|
||||
export function markWhatsAppVisibleDeliveryError(error: unknown): unknown {
|
||||
if (typeof error === "object" && error !== null && !Array.isArray(error)) {
|
||||
try {
|
||||
Object.assign(error, { sentBeforeError: true, visibleReplySent: true });
|
||||
return error;
|
||||
} catch {
|
||||
// Fall back to a wrapper when a platform error object is non-extensible.
|
||||
}
|
||||
}
|
||||
const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
|
||||
Object.assign(visibleError, { sentBeforeError: true, visibleReplySent: true });
|
||||
return visibleError;
|
||||
}
|
||||
|
||||
export function isLikelyWhatsAppCryptoError(reason: unknown) {
|
||||
const formatReason = (value: unknown): string => {
|
||||
if (value == null) {
|
||||
|
||||
@@ -24,31 +24,42 @@ export type WhatsAppReplyContext = {
|
||||
};
|
||||
|
||||
type LegacySenderLike = {
|
||||
sender?: WhatsAppIdentity;
|
||||
senderJid?: string;
|
||||
senderE164?: string;
|
||||
senderName?: string;
|
||||
platform: {
|
||||
sender?: WhatsAppIdentity;
|
||||
senderJid?: string;
|
||||
senderE164?: string;
|
||||
senderName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LegacySelfLike = {
|
||||
self?: WhatsAppSelfIdentity;
|
||||
selfJid?: string | null;
|
||||
selfLid?: string | null;
|
||||
selfE164?: string | null;
|
||||
platform: {
|
||||
self?: WhatsAppSelfIdentity;
|
||||
selfJid?: string | null;
|
||||
selfLid?: string | null;
|
||||
selfE164?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type LegacyReplyLike = {
|
||||
replyTo?: WhatsAppReplyContext;
|
||||
replyToId?: string;
|
||||
replyToBody?: string;
|
||||
replyToSender?: string;
|
||||
replyToSenderJid?: string;
|
||||
replyToSenderE164?: string;
|
||||
quote?: {
|
||||
context?: WhatsAppReplyContext;
|
||||
id?: string;
|
||||
body?: string;
|
||||
sender?: {
|
||||
displayName?: string;
|
||||
jid?: string;
|
||||
e164?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type LegacyMentionsLike = {
|
||||
mentions?: string[];
|
||||
mentionedJids?: string[];
|
||||
group?: {
|
||||
mentions?: {
|
||||
jids?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function normalizeDeviceScopedJid(jid: string | null | undefined): string | null {
|
||||
@@ -102,10 +113,10 @@ export function identitiesOverlap(
|
||||
|
||||
export function getSenderIdentity(msg: LegacySenderLike, authDir?: string): WhatsAppIdentity {
|
||||
return resolveComparableIdentity(
|
||||
msg.sender ?? {
|
||||
jid: msg.senderJid ?? null,
|
||||
e164: msg.senderE164 ?? null,
|
||||
name: msg.senderName ?? null,
|
||||
msg.platform.sender ?? {
|
||||
jid: msg.platform.senderJid ?? null,
|
||||
e164: msg.platform.senderE164 ?? null,
|
||||
name: msg.platform.senderName ?? null,
|
||||
},
|
||||
authDir,
|
||||
);
|
||||
@@ -113,10 +124,10 @@ export function getSenderIdentity(msg: LegacySenderLike, authDir?: string): What
|
||||
|
||||
export function getSelfIdentity(msg: LegacySelfLike, authDir?: string): WhatsAppSelfIdentity {
|
||||
return resolveComparableIdentity(
|
||||
msg.self ?? {
|
||||
jid: msg.selfJid ?? null,
|
||||
lid: msg.selfLid ?? null,
|
||||
e164: msg.selfE164 ?? null,
|
||||
msg.platform.self ?? {
|
||||
jid: msg.platform.selfJid ?? null,
|
||||
lid: msg.platform.selfLid ?? null,
|
||||
e164: msg.platform.selfE164 ?? null,
|
||||
},
|
||||
authDir,
|
||||
);
|
||||
@@ -126,23 +137,23 @@ export function getReplyContext(
|
||||
msg: LegacyReplyLike,
|
||||
authDir?: string,
|
||||
): WhatsAppReplyContext | null {
|
||||
if (msg.replyTo) {
|
||||
if (msg.quote?.context) {
|
||||
return {
|
||||
...msg.replyTo,
|
||||
sender: resolveComparableIdentity(msg.replyTo.sender, authDir),
|
||||
...msg.quote.context,
|
||||
sender: resolveComparableIdentity(msg.quote.context.sender, authDir),
|
||||
};
|
||||
}
|
||||
if (!msg.replyToBody) {
|
||||
if (!msg.quote?.body) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: msg.replyToId,
|
||||
body: msg.replyToBody,
|
||||
id: msg.quote.id,
|
||||
body: msg.quote.body,
|
||||
sender: resolveComparableIdentity(
|
||||
{
|
||||
jid: msg.replyToSenderJid ?? null,
|
||||
e164: msg.replyToSenderE164 ?? null,
|
||||
label: msg.replyToSender ?? null,
|
||||
jid: msg.quote.sender?.jid ?? null,
|
||||
e164: msg.quote.sender?.e164 ?? null,
|
||||
label: msg.quote.sender?.displayName ?? null,
|
||||
},
|
||||
authDir,
|
||||
),
|
||||
@@ -150,7 +161,7 @@ export function getReplyContext(
|
||||
}
|
||||
|
||||
function getMentionJids(msg: LegacyMentionsLike): string[] {
|
||||
return msg.mentions ?? msg.mentionedJids ?? [];
|
||||
return msg.group?.mentions?.jids ?? [];
|
||||
}
|
||||
|
||||
export function getMentionIdentities(
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function resolveWhatsAppCommandAuthorized(params: {
|
||||
cfg: params.cfg,
|
||||
policy,
|
||||
isGroup,
|
||||
conversationId: params.msg.conversationId ?? params.msg.chatId ?? params.msg.from,
|
||||
conversationId: params.msg.conversationId ?? params.msg.platform.chatJid ?? params.msg.from,
|
||||
senderId: isGroup ? groupSender : dmSender,
|
||||
dmSenderId: dmSender,
|
||||
includeCommand: true,
|
||||
|
||||
@@ -2013,7 +2013,7 @@ export async function dispatchReplyFromConfig(
|
||||
}
|
||||
|
||||
// Trigger plugin hooks (fire-and-forget)
|
||||
if (hookRunner?.hasHooks("message_received")) {
|
||||
if (ctx.SuppressMessageReceivedHooks !== true && hookRunner?.hasHooks("message_received")) {
|
||||
fireAndForgetHook(
|
||||
hookRunner.runMessageReceived(
|
||||
toPluginMessageReceivedEvent(hookContext),
|
||||
@@ -2024,7 +2024,7 @@ export async function dispatchReplyFromConfig(
|
||||
}
|
||||
|
||||
// Bridge to internal hooks (HOOK.md discovery system) - refs #8807
|
||||
if (sessionKey) {
|
||||
if (ctx.SuppressMessageReceivedHooks !== true && sessionKey) {
|
||||
fireAndForgetHook(
|
||||
triggerInternalHook(
|
||||
createInternalHookEvent("message", "received", sessionKey, {
|
||||
|
||||
@@ -309,6 +309,11 @@ export type MsgContext = {
|
||||
* OriginatingChannel/OriginatingTo, rather than inheriting stale session route metadata.
|
||||
*/
|
||||
ExplicitDeliverRoute?: boolean;
|
||||
/**
|
||||
* Internal flag for channels that emit message_received through a channel-specific
|
||||
* privacy gate before entering the shared reply dispatcher.
|
||||
*/
|
||||
SuppressMessageReceivedHooks?: boolean;
|
||||
/**
|
||||
* Provider-specific parent conversation id for threaded contexts.
|
||||
* For Discord threads, this is the parent channel id.
|
||||
|
||||
Reference in New Issue
Block a user